From 397ba75479cb5804b01cc86055ecd9797da7dba7 Mon Sep 17 00:00:00 2001 From: "JSOWELL-PC\\autum" Date: Sat, 4 Mar 2023 16:29:55 +0800 Subject: [PATCH] commit --- .gitignore | 34 + bin/clean.bat | 12 + bin/package.bat | 12 + bin/run.bat | 14 + doc/2022年11月17日逻辑改动.md | 11 + doc/接口文档.md | 134 ++ doc/接口文档New.md | 621 ++++++++ doc/测试问题.md | 5 + jsowell-admin/pom.xml | 127 ++ .../java/com/jsowell/JsowellApplication.java | 21 + .../jsowell/JsowellServletInitializer.java | 16 + .../jsowell/api/uniapp/JumpController.java | 70 + .../jsowell/api/uniapp/MemberController.java | 199 +++ .../jsowell/api/uniapp/OrderController.java | 244 +++ .../com/jsowell/api/uniapp/PayController.java | 204 +++ .../api/uniapp/PersonPileController.java | 213 +++ .../jsowell/api/uniapp/PileController.java | 91 ++ .../com/jsowell/service/MemberService.java | 234 +++ .../com/jsowell/service/OrderService.java | 780 ++++++++++ .../jsowell/service/PileRemoteService.java | 203 +++ .../java/com/jsowell/service/PileService.java | 438 ++++++ .../controller/common/CaptchaController.java | 91 ++ .../controller/common/CommonController.java | 143 ++ .../web/controller/index/indexController.java | 71 + .../controller/monitor/CacheController.java | 114 ++ .../controller/monitor/ServerController.java | 25 + .../monitor/SysLogininforController.java | 72 + .../monitor/SysOperlogController.java | 65 + .../monitor/SysUserOnlineController.java | 75 + .../pile/MemberBasicInfoController.java | 159 ++ .../pile/OrderAbnormalRecordController.java | 98 ++ .../pile/OrderBasicInfoController.java | 104 ++ .../pile/PileBasicInfoController.java | 163 ++ .../pile/PileBillingTemplateController.java | 167 ++ .../pile/PileConnectorInfoController.java | 113 ++ .../pile/PileLicenceInfoController.java | 98 ++ .../pile/PileMerchantInfoController.java | 103 ++ .../pile/PileModelInfoController.java | 91 ++ .../controller/pile/PileRemoteController.java | 109 ++ .../pile/PileSimInfoController.java | 142 ++ .../pile/PileStationInfoController.java | 148 ++ .../system/SysConfigController.java | 124 ++ .../controller/system/SysDeptController.java | 140 ++ .../system/SysDictDataController.java | 113 ++ .../system/SysDictTypeController.java | 122 ++ .../controller/system/SysIndexController.java | 29 + .../controller/system/SysLoginController.java | 90 ++ .../controller/system/SysMenuController.java | 125 ++ .../system/SysNoticeController.java | 86 ++ .../controller/system/SysPostController.java | 117 ++ .../system/SysProfileController.java | 131 ++ .../system/SysRegisterController.java | 35 + .../controller/system/SysRoleController.java | 223 +++ .../controller/system/SysUserController.java | 212 +++ .../controller/tool/SwaggerController.java | 23 + .../web/controller/tool/TestController.java | 147 ++ .../web/core/config/SwaggerConfig.java | 122 ++ .../META-INF/spring-devtools.properties | 1 + .../src/main/resources/application-dev.yml | 132 ++ .../src/main/resources/application-prd.yml | 130 ++ .../src/main/resources/application.yml | 128 ++ jsowell-admin/src/main/resources/banner.txt | 9 + .../main/resources/i18n/messages.properties | 37 + jsowell-admin/src/main/resources/logback.xml | 123 ++ .../main/resources/mybatis/mybatis-config.xml | 21 + .../test/java/SpringBootTestController.java | 688 +++++++++ jsowell-common/pom.xml | 181 +++ .../jsowell/common/annotation/Anonymous.java | 18 + .../jsowell/common/annotation/DataScope.java | 27 + .../jsowell/common/annotation/DataSource.java | 28 + .../com/jsowell/common/annotation/Excel.java | 181 +++ .../com/jsowell/common/annotation/Excels.java | 17 + .../com/jsowell/common/annotation/Log.java | 41 + .../common/annotation/RateLimiter.java | 40 + .../common/annotation/RepeatSubmit.java | 29 + .../jsowell/common/config/JsowellConfig.java | 132 ++ .../common/constant/CacheConstants.java | 102 ++ .../jsowell/common/constant/Constants.java | 210 +++ .../jsowell/common/constant/GenConstants.java | 186 +++ .../jsowell/common/constant/HttpStatus.java | 88 ++ .../common/constant/ScheduleConstants.java | 56 + .../common/constant/UserConstants.java | 111 ++ .../common/constant/WeiXinConstants.java | 64 + .../core/controller/BaseController.java | 187 +++ .../common/core/domain/AjaxResult.java | 155 ++ .../common/core/domain/BaseEntity.java | 114 ++ .../com/jsowell/common/core/domain/R.java | 94 ++ .../common/core/domain/TreeEntity.java | 78 + .../common/core/domain/TreeSelect.java | 74 + .../common/core/domain/entity/SysDept.java | 203 +++ .../core/domain/entity/SysDictData.java | 175 +++ .../core/domain/entity/SysDictType.java | 96 ++ .../common/core/domain/entity/SysMenu.java | 259 ++++ .../common/core/domain/entity/SysRole.java | 222 +++ .../common/core/domain/entity/SysUser.java | 322 ++++ .../common/core/domain/model/LoginBody.java | 32 + .../common/core/domain/model/LoginUser.java | 234 +++ .../core/domain/model/RegisterBody.java | 10 + .../core/domain/ykc/LoginRequestData.java | 58 + .../core/domain/ykc/RealTimeMonitorData.java | 124 ++ .../domain/ykc/TransactionRecordsData.java | 157 ++ .../core/domain/ykc/YKCDataProtocol.java | 73 + .../core/domain/ykc/YKCFrameTypeCode.java | 203 +++ .../jsowell/common/core/page/PageDomain.java | 93 ++ .../common/core/page/PageResponse.java | 42 + .../common/core/page/TableDataInfo.java | 82 + .../common/core/page/TableSupport.java | 53 + .../jsowell/common/core/redis/RedisCache.java | 504 ++++++ .../jsowell/common/core/text/CharsetKit.java | 91 ++ .../com/jsowell/common/core/text/Convert.java | 849 +++++++++++ .../common/core/text/StrFormatter.java | 76 + .../jsowell/common/enums/BusinessStatus.java | 18 + .../jsowell/common/enums/BusinessType.java | 58 + .../jsowell/common/enums/DataSourceType.java | 18 + .../com/jsowell/common/enums/DelFlagEnum.java | 34 + .../com/jsowell/common/enums/HttpMethod.java | 32 + .../com/jsowell/common/enums/LimitType.java | 19 + .../common/enums/MemberWalletEnum.java | 53 + .../jsowell/common/enums/OperatorType.java | 23 + .../common/enums/SoftwareProtocolEnum.java | 34 + .../com/jsowell/common/enums/UserStatus.java | 28 + .../sim/SimCardStatusCorrespondEnum.java | 80 + .../common/enums/sim/SimSupplierEnum.java | 53 + .../enums/uniapp/BalanceChangesEnum.java | 70 + .../common/enums/weixin/BusinessType.java | 34 + .../common/enums/weixin/WeiXinPayParam.java | 43 + .../enums/weixin/WeiXinPayTradeStatus.java | 38 + .../common/enums/ykc/ActionTypeEnum.java | 33 + .../common/enums/ykc/BillingTimeEnum.java | 37 + .../common/enums/ykc/BusinessTypeEnum.java | 31 + .../enums/ykc/ChargingFailedReasonEnum.java | 56 + .../common/enums/ykc/OrderPayModeEnum.java | 47 + .../common/enums/ykc/OrderPayRecordEnum.java | 48 + .../common/enums/ykc/OrderPayStatusEnum.java | 35 + .../common/enums/ykc/OrderStatusEnum.java | 51 + .../jsowell/common/enums/ykc/PayModeEnum.java | 32 + .../common/enums/ykc/PileChannelEntity.java | 55 + .../ykc/PileConnectorDataBaseStatusEnum.java | 48 + .../enums/ykc/PileConnectorStatusEnum.java | 36 + .../common/enums/ykc/PileStatusEnum.java | 45 + .../common/enums/ykc/ReturnCodeEnum.java | 138 ++ .../common/enums/ykc/ScenarioEnum.java | 35 + .../ykc/StopChargingFailedReasonEnum.java | 55 + .../enums/ykc/YKCChargingStopReasonEnum.java | 167 ++ .../enums/ykc/YKCPileFaultReasonEnum.java | 64 + .../common/exception/BusinessException.java | 29 + .../common/exception/DemoModeException.java | 13 + .../common/exception/GlobalException.java | 51 + .../common/exception/ServiceException.java | 65 + .../common/exception/UtilException.java | 22 + .../common/exception/base/BaseException.java | 84 + .../common/exception/file/FileException.java | 17 + .../FileNameLengthLimitExceededException.java | 14 + .../file/FileSizeLimitExceededException.java | 14 + .../file/InvalidExtensionException.java | 69 + .../common/exception/job/TaskException.java | 29 + .../exception/user/CaptchaException.java | 14 + .../user/CaptchaExpireException.java | 14 + .../common/exception/user/UserException.java | 16 + .../user/UserPasswordNotMatchException.java | 14 + ...UserPasswordRetryLimitExceedException.java | 14 + .../filter/PropertyPreExcludeFilter.java | 20 + .../common/filter/RepeatableFilter.java | 40 + .../filter/RepeatedlyRequestWrapper.java | 67 + .../com/jsowell/common/filter/XssFilter.java | 62 + .../filter/XssHttpServletRequestWrapper.java | 97 ++ .../common/response/RestApiResponse.java | 43 + .../java/com/jsowell/common/util/AESUtil.java | 176 +++ .../java/com/jsowell/common/util/Arith.java | 113 ++ .../com/jsowell/common/util/BytesUtil.java | 706 +++++++++ .../com/jsowell/common/util/CRC16Util.java | 235 +++ .../util/Cp56Time2a/Cp56Time2aUtil.java | 91 ++ .../com/jsowell/common/util/DateUtils.java | 813 ++++++++++ .../com/jsowell/common/util/DictUtils.java | 159 ++ .../jsowell/common/util/DistanceUtils.java | 42 + .../jsowell/common/util/ExceptionUtil.java | 33 + .../com/jsowell/common/util/JWTUtils.java | 135 ++ .../com/jsowell/common/util/LogUtils.java | 15 + .../com/jsowell/common/util/MessageUtils.java | 24 + .../com/jsowell/common/util/PageUtils.java | 32 + .../com/jsowell/common/util/RandomUtil.java | 84 + .../java/com/jsowell/common/util/SMSUtil.java | 68 + .../jsowell/common/util/SecurityUtils.java | 99 ++ .../com/jsowell/common/util/ServletUtils.java | 160 ++ .../com/jsowell/common/util/StringUtils.java | 561 +++++++ .../java/com/jsowell/common/util/Threads.java | 77 + .../com/jsowell/common/util/YKCUtils.java | 136 ++ .../jsowell/common/util/bean/BeanUtils.java | 104 ++ .../common/util/bean/BeanValidators.java | 21 + .../common/util/file/FileTypeUtils.java | 64 + .../common/util/file/FileUploadUtils.java | 198 +++ .../jsowell/common/util/file/FileUtils.java | 252 +++ .../jsowell/common/util/file/ImageUtils.java | 79 + .../common/util/file/MimeTypeUtils.java | 56 + .../jsowell/common/util/html/EscapeUtil.java | 140 ++ .../jsowell/common/util/html/HTMLFilter.java | 501 ++++++ .../jsowell/common/util/http/HttpHelper.java | 44 + .../jsowell/common/util/http/HttpUtils.java | 277 ++++ .../com/jsowell/common/util/id/IdUtils.java | 101 ++ .../java/com/jsowell/common/util/id/Seq.java | 80 + .../com/jsowell/common/util/id/SnUtils.java | 77 + .../common/util/id/SnowflakeIdWorker.java | 237 +++ .../java/com/jsowell/common/util/id/UUID.java | 441 ++++++ .../jsowell/common/util/ip/AddressUtils.java | 93 ++ .../com/jsowell/common/util/ip/IpUtils.java | 224 +++ .../common/util/poi/ExcelHandlerAdapter.java | 17 + .../jsowell/common/util/poi/ExcelUtil.java | 1355 +++++++++++++++++ .../common/util/reflect/ReflectUtils.java | 329 ++++ .../com/jsowell/common/util/sign/Base64.java | 253 +++ .../com/jsowell/common/util/sign/MD5Util.java | 107 ++ .../jsowell/common/util/sim/SimCardUtils.java | 43 + .../common/util/sim/XunZhongSimUtils.java | 125 ++ .../common/util/spring/SpringUtils.java | 141 ++ .../com/jsowell/common/util/sql/SqlUtil.java | 53 + .../main/java/com/jsowell/common/xss/Xss.java | 26 + .../com/jsowell/common/xss/XssValidator.java | 31 + jsowell-framework/pom.xml | 83 + .../framework/aspectj/DataScopeAspect.java | 135 ++ .../framework/aspectj/DataSourceAspect.java | 64 + .../jsowell/framework/aspectj/LogAspect.java | 200 +++ .../framework/aspectj/RateLimiterAspect.java | 80 + .../framework/config/ApplicationConfig.java | 29 + .../framework/config/CaptchaConfig.java | 84 + .../jsowell/framework/config/DruidConfig.java | 112 ++ .../config/FastJson2JsonRedisSerializer.java | 43 + .../framework/config/FilterConfig.java | 56 + .../framework/config/KaptchaTextCreator.java | 56 + .../framework/config/MyBatisConfig.java | 110 ++ .../jsowell/framework/config/RedisConfig.java | 65 + .../framework/config/ResourcesConfig.java | 66 + .../framework/config/SecurityConfig.java | 141 ++ .../framework/config/ServerConfig.java | 30 + .../framework/config/ThreadPoolConfig.java | 62 + .../config/properties/DruidProperties.java | 75 + .../properties/PermitAllUrlProperties.java | 68 + .../datasource/DynamicDataSource.java | 24 + .../DynamicDataSourceContextHolder.java | 41 + .../interceptor/RepeatSubmitInterceptor.java | 49 + .../impl/SameUrlDataInterceptor.java | 101 ++ .../framework/manager/AsyncManager.java | 53 + .../framework/manager/ShutdownManager.java | 34 + .../manager/factory/AsyncFactory.java | 93 ++ .../context/AuthenticationContextHolder.java | 24 + .../filter/JwtAuthenticationTokenFilter.java | 42 + .../handle/AuthenticationEntryPointImpl.java | 33 + .../handle/LogoutSuccessHandlerImpl.java | 51 + .../jsowell/framework/web/domain/Server.java | 215 +++ .../framework/web/domain/server/Cpu.java | 88 ++ .../framework/web/domain/server/Jvm.java | 114 ++ .../framework/web/domain/server/Mem.java | 53 + .../framework/web/domain/server/Sys.java | 73 + .../framework/web/domain/server/SysFile.java | 99 ++ .../web/exception/GlobalExceptionHandler.java | 106 ++ .../web/service/PermissionService.java | 148 ++ .../web/service/SysLoginService.java | 125 ++ .../web/service/SysPasswordService.java | 84 + .../web/service/SysPermissionService.java | 58 + .../web/service/SysRegisterService.java | 96 ++ .../framework/web/service/TokenService.java | 211 +++ .../web/service/UserDetailsServiceImpl.java | 57 + jsowell-generator/pom.xml | 58 + .../jsowell/generator/config/GenConfig.java | 72 + .../generator/controller/GenController.java | 194 +++ .../jsowell/generator/domain/GenTable.java | 363 +++++ .../generator/domain/GenTableColumn.java | 348 +++++ .../mapper/GenTableColumnMapper.java | 62 + .../generator/mapper/GenTableMapper.java | 85 ++ .../service/IGenTableColumnService.java | 44 + .../generator/service/IGenTableService.java | 121 ++ .../impl/GenTableColumnServiceImpl.java | 65 + .../service/impl/GenTableServiceImpl.java | 456 ++++++ .../com/jsowell/generator/util/GenUtils.java | 221 +++ .../generator/util/VelocityInitializer.java | 30 + .../jsowell/generator/util/VelocityUtils.java | 348 +++++ .../src/main/resources/generator.yml | 10 + .../mapper/generator/GenTableColumnMapper.xml | 127 ++ .../mapper/generator/GenTableMapper.xml | 206 +++ .../main/resources/vm/java/controller.java.vm | 115 ++ .../src/main/resources/vm/java/domain.java.vm | 101 ++ .../src/main/resources/vm/java/mapper.java.vm | 91 ++ .../main/resources/vm/java/service.java.vm | 61 + .../resources/vm/java/serviceImpl.java.vm | 169 ++ .../main/resources/vm/java/sub-domain.java.vm | 73 + .../src/main/resources/vm/js/api.js.vm | 44 + .../src/main/resources/vm/sql/sql.vm | 22 + .../main/resources/vm/vue/index-tree.vue.vm | 502 ++++++ .../src/main/resources/vm/vue/index.vue.vm | 598 ++++++++ .../resources/vm/vue/v3/index-tree.vue.vm | 486 ++++++ .../src/main/resources/vm/vue/v3/index.vue.vm | 596 ++++++++ .../src/main/resources/vm/xml/mapper.xml.vm | 135 ++ jsowell-netty/pom.xml | 46 + .../com/jsowell/netty/client/NettyClient.java | 109 ++ .../client/NettyClientChannelInitializer.java | 23 + .../netty/client/NettyClientHandler.java | 71 + .../jsowell/netty/client/TestNettyClient.java | 14 + .../ykc/GetRealTimeMonitorDataCommand.java | 18 + .../netty/command/ykc/IssueQRCodeCommand.java | 14 + .../command/ykc/ProofreadTimeCommand.java | 17 + .../PublishPileBillingTemplateCommand.java | 17 + .../netty/command/ykc/RebootCommand.java | 14 + .../command/ykc/StartChargingCommand.java | 48 + .../command/ykc/StopChargingCommand.java | 18 + .../netty/command/ykc/UpdateFileCommand.java | 16 + .../jsowell/netty/decoder/CustomDecoder.java | 40 + .../StartAndLengthFieldFrameDecoder.java | 85 ++ .../netty/factory/YKCOperateFactory.java | 38 + .../netty/handler/AbstractHandler.java | 73 + .../BMSAbortDuringChargingPhaseHandler.java | 96 ++ .../BMSDemandAndChargerOutputHandler.java | 108 ++ .../netty/handler/BMSInformationHandler.java | 99 ++ .../BillingTemplateRequestHandler.java | 78 + .../BillingTemplateResponseHandler.java | 48 + .../BillingTemplateSettingHandler.java | 61 + ...BillingTemplateValidateRequestHandler.java | 89 ++ .../netty/handler/ChargeEndHandler.java | 121 ++ ...rgerAbortedDuringChargingPhaseHandler.java | 93 ++ .../handler/ChargingHandshakeHandler.java | 125 ++ .../ConfirmStartChargingRequestHandler.java | 135 ++ .../netty/handler/ErrorMessageHandler.java | 93 ++ .../handler/GroundLockDataUploadHandler.java | 58 + .../handler/HeartbeatRequestHandler.java | 83 + .../netty/handler/LoginRequestHandler.java | 189 +++ .../OfflineCardDataCleaningHandler.java | 45 + ...fflineCardDataCleaningResponseHandler.java | 69 + .../handler/OfflineCardDataQueryHandler.java | 46 + .../OfflineCardDataQueryResponseHandler.java | 60 + ...OfflineCardDataSynchronizationHandler.java | 46 + ...ardDataSynchronizationResponseHandler.java | 57 + .../ParameterConfigurationHandler.java | 129 ++ .../PileWorkingParameterSettingHandler.java | 45 + ...orkingParameterSettingResponseHandler.java | 52 + .../ReadRealTimeMonitorDataHandler.java | 57 + ...oteAccountBalanceUpdateRequestHandler.java | 74 + .../RemoteControlGroundLockHandler.java | 45 + ...emoteControlGroundLockResponseHandler.java | 57 + .../handler/RemoteIssuedQrCodeHandler.java | 75 + .../RemoteIssuedQrCodeResponseHandler.java | 52 + .../netty/handler/RemoteRestartHandler.java | 41 + .../handler/RemoteRestartResponseHandler.java | 61 + .../RemoteStartChargingRequestHandler.java | 98 ++ .../RemoteStopChargingRequestHandler.java | 99 ++ .../netty/handler/RemoteUpdateHandler.java | 58 + .../handler/RemoteUpdateResponseHandler.java | 52 + .../handler/TimeCheckSettingHandler.java | 41 + .../TimeCheckSettingResponseHandler.java | 52 + .../TransactionRecordsRequestHandler.java | 559 +++++++ .../handler/UploadRealTimeMonitorHandler.java | 267 ++++ .../server/yunkuaichong/NettyServer.java | 77 + .../NettyServerChannelInitializer.java | 32 + .../yunkuaichong/NettyServerHandler.java | 203 +++ .../yunkuaichong/YKCBusinessService.java | 25 + .../yunkuaichong/YKCPushCommandService.java | 64 + .../impl/YKCBusinessServiceImpl.java | 100 ++ .../impl/YKCPushCommandServiceImpl.java | 341 +++++ jsowell-pile/pom.xml | 106 ++ .../jsowell/pile/domain/MemberBasicInfo.java | 107 ++ .../pile/domain/MemberTransactionRecord.java | 78 + .../jsowell/pile/domain/MemberWalletInfo.java | 56 + .../jsowell/pile/domain/MemberWalletLog.java | 63 + .../pile/domain/OrderAbnormalRecord.java | 502 ++++++ .../jsowell/pile/domain/OrderBasicInfo.java | 185 +++ .../com/jsowell/pile/domain/OrderDetail.java | 319 ++++ .../jsowell/pile/domain/OrderPayRecord.java | 70 + .../jsowell/pile/domain/PileBasicInfo.java | 215 +++ .../pile/domain/PileBillingDetail.java | 131 ++ .../pile/domain/PileBillingRelation.java | 35 + .../pile/domain/PileBillingTemplate.java | 154 ++ .../pile/domain/PileConnectorInfo.java | 121 ++ .../jsowell/pile/domain/PileLicenceInfo.java | 114 ++ .../pile/domain/PileMemberRelation.java | 56 + .../jsowell/pile/domain/PileMerchantInfo.java | 107 ++ .../jsowell/pile/domain/PileModelInfo.java | 102 ++ .../jsowell/pile/domain/PileMsgRecord.java | 53 + .../com/jsowell/pile/domain/PileSimInfo.java | 66 + .../jsowell/pile/domain/PileStationInfo.java | 355 +++++ .../pile/domain/TransactionRecords.java | 178 +++ .../pile/domain/WxpayCallbackRecord.java | 102 ++ .../pile/domain/WxpayRefundCallback.java | 94 ++ .../com/jsowell/pile/dto/BaseMemberDTO.java | 32 + .../com/jsowell/pile/dto/BasicPileDTO.java | 42 + .../jsowell/pile/dto/BatchCreatePileDTO.java | 54 + .../com/jsowell/pile/dto/BillingTimeDTO.java | 31 + .../dto/CreateOrUpdateBillingTemplateDTO.java | 91 ++ .../pile/dto/FastCreateStationDTO.java | 40 + .../jsowell/pile/dto/GenerateOrderDTO.java | 51 + .../pile/dto/ImportBillingTemplateDTO.java | 15 + .../com/jsowell/pile/dto/IndexQueryDTO.java | 17 + .../pile/dto/MemberRegisterAndLoginDTO.java | 37 + .../jsowell/pile/dto/MemberRegisterDTO.java | 38 + .../com/jsowell/pile/dto/PayOrderDTO.java | 55 + .../pile/dto/PayOrderSuccessCallbackDTO.java | 32 + .../jsowell/pile/dto/PaymentScenarioDTO.java | 29 + .../pile/dto/PileMemberBindingDTO.java | 32 + .../pile/dto/PublishBillingTemplateDTO.java | 22 + .../jsowell/pile/dto/QueryConnectorDTO.java | 32 + .../pile/dto/QueryConnectorListDTO.java | 50 + .../com/jsowell/pile/dto/QueryOrderDTO.java | 58 + .../jsowell/pile/dto/QueryPersonPileDTO.java | 37 + .../com/jsowell/pile/dto/QueryPileDTO.java | 37 + .../com/jsowell/pile/dto/QuerySimInfoDTO.java | 32 + .../com/jsowell/pile/dto/QueryStationDTO.java | 59 + .../pile/dto/RemoteUpdatePileFileDTO.java | 26 + .../pile/dto/ReplaceMerchantStationDTO.java | 42 + .../com/jsowell/pile/dto/SettleOrderDTO.java | 27 + .../com/jsowell/pile/dto/SimRenewDTO.java | 24 + .../jsowell/pile/dto/StartChargingDTO.java | 29 + .../com/jsowell/pile/dto/StopChargingDTO.java | 26 + .../pile/dto/UniAppQueryMemberBalanceDTO.java | 33 + .../jsowell/pile/dto/UniAppQueryOrderDTO.java | 24 + .../com/jsowell/pile/dto/WechatLoginDTO.java | 18 + .../com/jsowell/pile/dto/WeixinPayDTO.java | 32 + .../pile/mapper/MemberBasicInfoMapper.java | 110 ++ .../mapper/MemberTransactionRecordMapper.java | 13 + .../pile/mapper/MemberWalletInfoMapper.java | 21 + .../pile/mapper/MemberWalletLogMapper.java | 33 + .../mapper/OrderAbnormalRecordMapper.java | 56 + .../pile/mapper/OrderBasicInfoMapper.java | 185 +++ .../pile/mapper/OrderPayRecordMapper.java | 24 + .../pile/mapper/PileBasicInfoMapper.java | 146 ++ .../mapper/PileBillingTemplateMapper.java | 144 ++ .../pile/mapper/PileConnectorInfoMapper.java | 141 ++ .../pile/mapper/PileLicenceInfoMapper.java | 61 + .../pile/mapper/PileMemberRelationMapper.java | 74 + .../pile/mapper/PileMerchantInfoMapper.java | 70 + .../pile/mapper/PileModelInfoMapper.java | 73 + .../pile/mapper/PileMsgRecordMapper.java | 32 + .../pile/mapper/PileSimInfoMapper.java | 96 ++ .../pile/mapper/PileStationInfoMapper.java | 74 + .../mapper/WxpayCallbackRecordMapper.java | 39 + .../mapper/WxpayRefundCallbackMapper.java | 17 + .../pile/service/IMemberBasicInfoService.java | 118 ++ .../IMemberTransactionRecordService.java | 18 + .../service/IMemberWalletInfoService.java | 17 + .../pile/service/IMemberWalletLogService.java | 16 + .../service/IOrderAbnormalRecordService.java | 61 + .../pile/service/IOrderBasicInfoService.java | 194 +++ .../pile/service/IOrderPayRecordService.java | 29 + .../pile/service/IPileBasicInfoService.java | 158 ++ .../service/IPileBillingTemplateService.java | 152 ++ .../service/IPileConnectorInfoService.java | 133 ++ .../pile/service/IPileLicenceInfoService.java | 62 + .../service/IPileMemberRelationService.java | 79 + .../service/IPileMerchantInfoService.java | 66 + .../pile/service/IPileModelInfoService.java | 72 + .../pile/service/IPileMsgRecordService.java | 24 + .../pile/service/IPileSimInfoService.java | 92 ++ .../pile/service/IPileStationInfoService.java | 91 ++ .../jsowell/pile/service/SimCardService.java | 468 ++++++ .../pile/service/WechatPayService.java | 43 + .../service/WxpayCallbackRecordService.java | 34 + .../service/WxpayRefundCallbackService.java | 15 + .../impl/MemberBasicInfoServiceImpl.java | 264 ++++ .../MemberTransactionRecordServiceImpl.java | 41 + .../impl/MemberWalletInfoServiceImpl.java | 46 + .../impl/MemberWalletLogServiceImpl.java | 46 + .../impl/OrderAbnormalRecordServiceImpl.java | 88 ++ .../impl/OrderBasicInfoServiceImpl.java | 852 +++++++++++ .../impl/OrderPayRecordServiceImpl.java | 58 + .../impl/PileBasicInfoServiceImpl.java | 566 +++++++ .../impl/PileBillingTemplateServiceImpl.java | 619 ++++++++ .../impl/PileConnectorInfoServiceImpl.java | 562 +++++++ .../impl/PileLicenceInfoServiceImpl.java | 108 ++ .../impl/PileMemberRelationServiceImpl.java | 118 ++ .../impl/PileMerchantInfoServiceImpl.java | 167 ++ .../impl/PileModelInfoServiceImpl.java | 113 ++ .../impl/PileMsgRecordServiceImpl.java | 109 ++ .../service/impl/PileSimInfoServiceImpl.java | 137 ++ .../impl/PileStationInfoServiceImpl.java | 352 +++++ .../service/impl/WechatPayServiceImpl.java | 353 +++++ .../impl/WxpayCallbackRecordServiceImpl.java | 86 ++ .../impl/WxpayRefundCallbackServiceImpl.java | 41 + .../dto/BillingTemplateTransactionDTO.java | 32 + .../transaction/dto/MemberTransactionDTO.java | 18 + .../transaction/dto/OrderTransactionDTO.java | 24 + .../transaction/dto/PileTransactionDTO.java | 20 + .../service/TransactionService.java | 155 ++ .../jsowell/pile/vo/base/ConnectorInfoVO.java | 44 + .../jsowell/pile/vo/base/MerchantInfoVO.java | 33 + .../com/jsowell/pile/vo/base/PileInfoVO.java | 52 + .../jsowell/pile/vo/base/StationInfoVO.java | 87 ++ .../pile/vo/uniapp/BillingPriceVO.java | 50 + .../vo/uniapp/CurrentTimePriceDetails.java | 42 + .../com/jsowell/pile/vo/uniapp/MemberVO.java | 56 + .../pile/vo/uniapp/MemberWalletLogVO.java | 47 + .../com/jsowell/pile/vo/uniapp/OrderVO.java | 84 + .../uniapp/PersonPileConnectorSumInfoVO.java | 38 + .../pile/vo/uniapp/PersonPileRealTimeVO.java | 48 + .../pile/vo/uniapp/PersonalPileInfoVO.java | 62 + .../pile/vo/uniapp/PileConnectorDetailVO.java | 54 + .../pile/vo/uniapp/PileConnectorVO.java | 39 + .../jsowell/pile/vo/uniapp/PileDetailVO.java | 41 + .../jsowell/pile/vo/uniapp/SendMessageVO.java | 55 + .../jsowell/pile/vo/uniapp/UniAppOrderVO.java | 128 ++ .../jsowell/pile/vo/web/BillingDetailVO.java | 27 + .../pile/vo/web/BillingTemplateVO.java | 150 ++ .../pile/vo/web/EchoBillingTemplateVO.java | 10 + .../pile/vo/web/IndexGeneralSituationVO.java | 37 + .../jsowell/pile/vo/web/IndexOrderInfoVO.java | 48 + .../pile/vo/web/MemberTransactionVO.java | 70 + .../pile/vo/web/OrderDetailInfoVO.java | 73 + .../com/jsowell/pile/vo/web/OrderListVO.java | 159 ++ .../pile/vo/web/OrderRealTimeInfoVO.java | 37 + .../jsowell/pile/vo/web/OrderTotalDataVO.java | 23 + .../pile/vo/web/PileCommunicationLogVO.java | 39 + .../pile/vo/web/PileConnectorInfoVO.java | 159 ++ .../com/jsowell/pile/vo/web/PileDetailVO.java | 111 ++ .../jsowell/pile/vo/web/PileModelInfoVO.java | 88 ++ .../jsowell/pile/vo/web/PileStationVO.java | 150 ++ .../jsowell/pile/vo/web/SimCardInfoVO.java | 63 + .../com/jsowell/pile/vo/web/SimCardVO.java | 84 + .../jsowell/pile/vo/web/SimRenewResultVO.java | 37 + .../pile/vo/web/UpdateMemberBalanceDTO.java | 61 + .../jsowell/pile/vo/web/WuLianSimData.java | 93 ++ .../jsowell/pile/vo/web/WuLianSimRenewVO.java | 44 + .../jsowell/pile/vo/web/XunZhongSimData.java | 50 + .../wxpay/common/WeChatPayParameter.java | 72 + .../jsowell/wxpay/config/WechatPayConfig.java | 98 ++ .../wxpay/config/WeixinLoginProperties.java | 34 + .../dto/AppletTemplateMessageSendDTO.java | 70 + .../jsowell/wxpay/dto/WeChatRefundDTO.java | 35 + .../jsowell/wxpay/dto/WechatSendMsgDTO.java | 27 + .../response/WechatPayNotifyParameter.java | 67 + .../response/WechatPayNotifyResource.java | 112 ++ .../WechatPayRefundNotifyResource.java | 83 + .../response/WechatPayRefundRequest.java | 59 + .../response/WechatPayRefundResponse.java | 161 ++ .../wxpay/service/WxAppletRemoteService.java | 288 ++++ .../java/com/jsowell/wxpay/utils/AesUtil.java | 45 + .../utils/BufferedImageLuminanceSource.java | 87 ++ .../com/jsowell/wxpay/utils/HttpUtils.java | 137 ++ .../com/jsowell/wxpay/utils/QRCodeUtil.java | 147 ++ .../jsowell/wxpay/utils/WechatPayUtils.java | 243 +++ .../src/main/java/com/jsowell/wxpay/vo/R.java | 85 ++ .../com/jsowell/wxpay/vo/ResultCodeEnum.java | 42 + .../pile/MemberTransactionRecordMapper.xml | 114 ++ .../mapper/pile/MemberBasicInfoMapper.xml | 211 +++ .../mapper/pile/MemberWalletInfoMapper.xml | 170 +++ .../mapper/pile/MemberWalletLogMapper.xml | 50 + .../mapper/pile/OrderAbnormalRecordMapper.xml | 183 +++ .../mapper/pile/OrderBasicInfoMapper.xml | 775 ++++++++++ .../mapper/pile/OrderPayRecordMapper.xml | 173 +++ .../mapper/pile/PileBasicInfoMapper.xml | 345 +++++ .../mapper/pile/PileBillingTemplateMapper.xml | 433 ++++++ .../mapper/pile/PileConnectorInfoMapper.xml | 239 +++ .../mapper/pile/PileLicenceInfoMapper.xml | 93 ++ .../mapper/pile/PileMemberRelationMapper.xml | 89 ++ .../mapper/pile/PileMerchantInfoMapper.xml | 262 ++++ .../mapper/pile/PileModelInfoMapper.xml | 147 ++ .../mapper/pile/PileMsgRecordMapper.xml | 131 ++ .../mapper/pile/PileSimInfoMapper.xml | 182 +++ .../mapper/pile/PileStationInfoMapper.xml | 352 +++++ .../mapper/pile/WxpayCallbackRecordMapper.xml | 263 ++++ .../mapper/pile/WxpayRefundCallbackMapper.xml | 227 +++ jsowell-quartz/pom.xml | 64 + .../jsowell/quartz/config/ScheduleConfig.java | 57 + .../quartz/controller/SysJobController.java | 148 ++ .../controller/SysJobLogController.java | 82 + .../com/jsowell/quartz/domain/SysJob.java | 169 ++ .../com/jsowell/quartz/domain/SysJobLog.java | 155 ++ .../quartz/mapper/SysJobLogMapper.java | 66 + .../jsowell/quartz/mapper/SysJobMapper.java | 69 + .../quartz/service/ISysJobLogService.java | 56 + .../quartz/service/ISysJobService.java | 102 ++ .../service/impl/SysJobLogServiceImpl.java | 81 + .../service/impl/SysJobServiceImpl.java | 236 +++ .../com/jsowell/quartz/task/JsowellTask.java | 39 + .../java/com/jsowell/quartz/task/RyTask.java | 24 + .../quartz/util/AbstractQuartzJob.java | 97 ++ .../com/jsowell/quartz/util/CronUtils.java | 53 + .../jsowell/quartz/util/JobInvokeUtil.java | 159 ++ .../QuartzDisallowConcurrentExecution.java | 18 + .../quartz/util/QuartzJobExecution.java | 16 + .../jsowell/quartz/util/ScheduleUtils.java | 126 ++ .../mapper/quartz/SysJobLogMapper.xml | 93 ++ .../resources/mapper/quartz/SysJobMapper.xml | 111 ++ jsowell-system/pom.xml | 50 + .../com/jsowell/system/domain/SysCache.java | 77 + .../com/jsowell/system/domain/SysConfig.java | 111 ++ .../jsowell/system/domain/SysLogininfor.java | 144 ++ .../com/jsowell/system/domain/SysNotice.java | 102 ++ .../com/jsowell/system/domain/SysOperLog.java | 255 ++++ .../com/jsowell/system/domain/SysPost.java | 123 ++ .../jsowell/system/domain/SysRoleDept.java | 45 + .../jsowell/system/domain/SysRoleMenu.java | 45 + .../jsowell/system/domain/SysUserOnline.java | 112 ++ .../jsowell/system/domain/SysUserPost.java | 45 + .../jsowell/system/domain/SysUserRole.java | 45 + .../system/mapper/SysConfigMapper.java | 68 + .../jsowell/system/mapper/SysDeptMapper.java | 118 ++ .../system/mapper/SysDictDataMapper.java | 95 ++ .../system/mapper/SysDictTypeMapper.java | 83 + .../system/mapper/SysLogininforMapper.java | 42 + .../jsowell/system/mapper/SysMenuMapper.java | 117 ++ .../system/mapper/SysNoticeMapper.java | 60 + .../system/mapper/SysOperLogMapper.java | 48 + .../jsowell/system/mapper/SysPostMapper.java | 99 ++ .../system/mapper/SysRoleDeptMapper.java | 44 + .../jsowell/system/mapper/SysRoleMapper.java | 107 ++ .../system/mapper/SysRoleMenuMapper.java | 44 + .../jsowell/system/mapper/SysUserMapper.java | 127 ++ .../system/mapper/SysUserPostMapper.java | 44 + .../system/mapper/SysUserRoleMapper.java | 62 + .../system/service/SysConfigService.java | 89 ++ .../system/service/SysDeptService.java | 116 ++ .../system/service/SysDictDataService.java | 60 + .../system/service/SysDictTypeService.java | 98 ++ .../system/service/SysLogininforService.java | 40 + .../system/service/SysMenuService.java | 136 ++ .../system/service/SysNoticeService.java | 60 + .../system/service/SysOperLogService.java | 48 + .../system/service/SysPostService.java | 99 ++ .../system/service/SysRoleService.java | 173 +++ .../system/service/SysUserOnlineService.java | 47 + .../system/service/SysUserService.java | 206 +++ .../service/impl/SysConfigServiceImpl.java | 204 +++ .../service/impl/SysDeptServiceImpl.java | 295 ++++ .../service/impl/SysDictDataServiceImpl.java | 102 ++ .../service/impl/SysDictTypeServiceImpl.java | 202 +++ .../impl/SysLogininforServiceImpl.java | 61 + .../service/impl/SysMenuServiceImpl.java | 457 ++++++ .../service/impl/SysNoticeServiceImpl.java | 86 ++ .../service/impl/SysOperLogServiceImpl.java | 71 + .../service/impl/SysPostServiceImpl.java | 163 ++ .../service/impl/SysRoleServiceImpl.java | 381 +++++ .../impl/SysUserOnlineServiceImpl.java | 86 ++ .../service/impl/SysUserServiceImpl.java | 483 ++++++ .../java/com/jsowell/system/vo/MetaVO.java | 91 ++ .../java/com/jsowell/system/vo/RouterVO.java | 130 ++ .../mapper/system/SysConfigMapper.xml | 112 ++ .../resources/mapper/system/SysDeptMapper.xml | 158 ++ .../mapper/system/SysDictDataMapper.xml | 124 ++ .../mapper/system/SysDictTypeMapper.xml | 105 ++ .../mapper/system/SysLogininforMapper.xml | 57 + .../resources/mapper/system/SysMenuMapper.xml | 195 +++ .../mapper/system/SysNoticeMapper.xml | 89 ++ .../mapper/system/SysOperLogMapper.xml | 83 + .../resources/mapper/system/SysPostMapper.xml | 122 ++ .../mapper/system/SysRoleDeptMapper.xml | 34 + .../resources/mapper/system/SysRoleMapper.xml | 152 ++ .../mapper/system/SysRoleMenuMapper.xml | 34 + .../resources/mapper/system/SysUserMapper.xml | 226 +++ .../mapper/system/SysUserPostMapper.xml | 34 + .../mapper/system/SysUserRoleMapper.xml | 44 + jsowell-thirdparty/pom.xml | 51 + .../thirdparty/service/LianLianService.java | 10 + .../service/impl/LianLianServiceImpl.java | 25 + .../jsowell/thirdparty/vo/ChargeDetail.java | 54 + .../vo/ConnectorChargeStatusInfo.java | 122 ++ .../jsowell/thirdparty/vo/ConnectorInfo.java | 64 + .../thirdparty/vo/ConnectorStatsInfo.java | 29 + .../thirdparty/vo/ConnectorStatusInfo.java | 42 + .../jsowell/thirdparty/vo/EquipmentInfo.java | 102 ++ .../thirdparty/vo/EquipmentStatsInfo.java | 36 + .../jsowell/thirdparty/vo/OperatorInfo.java | 45 + .../com/jsowell/thirdparty/vo/OrderInfo.java | 150 ++ .../jsowell/thirdparty/vo/StationInfo.java | 318 ++++ .../thirdparty/vo/StationStatusInfo.java | 29 + jsowell-ui/.editorconfig | 22 + jsowell-ui/.env.development | 11 + jsowell-ui/.env.production | 8 + jsowell-ui/.env.staging | 10 + jsowell-ui/.eslintignore | 10 + jsowell-ui/.eslintrc.js | 279 ++++ jsowell-ui/.gitignore | 23 + jsowell-ui/README.md | 26 + jsowell-ui/babel.config.js | 13 + jsowell-ui/bin/build-sit.bat | 12 + jsowell-ui/bin/build.bat | 12 + jsowell-ui/bin/package.bat | 12 + jsowell-ui/bin/run-web.bat | 12 + jsowell-ui/package.json | 89 ++ jsowell-ui/public/favicon.ico | Bin 0 -> 67646 bytes jsowell-ui/public/html/ie.html | 46 + jsowell-ui/public/index.html | 213 +++ jsowell-ui/public/robots.txt | 2 + jsowell-ui/src/App.vue | 19 + jsowell-ui/src/api/billing/template.js | 100 ++ jsowell-ui/src/api/index/index.js | 17 + jsowell-ui/src/api/login.js | 59 + jsowell-ui/src/api/member/info.js | 79 + jsowell-ui/src/api/menu.js | 9 + jsowell-ui/src/api/monitor/cache.js | 57 + jsowell-ui/src/api/monitor/job.js | 71 + jsowell-ui/src/api/monitor/jobLog.js | 26 + jsowell-ui/src/api/monitor/logininfor.js | 34 + jsowell-ui/src/api/monitor/online.js | 18 + jsowell-ui/src/api/monitor/operlog.js | 26 + jsowell-ui/src/api/monitor/server.js | 9 + jsowell-ui/src/api/order/abnormalRecord.js | 44 + jsowell-ui/src/api/order/order.js | 53 + jsowell-ui/src/api/pile/basic.js | 88 ++ jsowell-ui/src/api/pile/connector.js | 54 + jsowell-ui/src/api/pile/merchant.js | 53 + jsowell-ui/src/api/pile/model.js | 44 + jsowell-ui/src/api/pile/sim.js | 61 + jsowell-ui/src/api/pile/station.js | 74 + jsowell-ui/src/api/system/config.js | 60 + jsowell-ui/src/api/system/dept.js | 68 + jsowell-ui/src/api/system/dict/data.js | 52 + jsowell-ui/src/api/system/dict/type.js | 60 + jsowell-ui/src/api/system/menu.js | 60 + jsowell-ui/src/api/system/notice.js | 44 + jsowell-ui/src/api/system/post.js | 44 + jsowell-ui/src/api/system/role.js | 111 ++ jsowell-ui/src/api/system/user.js | 127 ++ jsowell-ui/src/api/tool/gen.js | 76 + jsowell-ui/src/assets/401_images/401.gif | Bin 0 -> 164227 bytes jsowell-ui/src/assets/404_images/404.png | Bin 0 -> 98071 bytes .../src/assets/404_images/404_cloud.png | Bin 0 -> 4766 bytes jsowell-ui/src/assets/icons/index.js | 9 + jsowell-ui/src/assets/icons/svg/404.svg | 1 + jsowell-ui/src/assets/icons/svg/bug.svg | 15 + jsowell-ui/src/assets/icons/svg/build.svg | 1 + jsowell-ui/src/assets/icons/svg/button.svg | 1 + jsowell-ui/src/assets/icons/svg/cascader.svg | 1 + .../src/assets/icons/svg/chargeandpark.svg | 16 + .../src/assets/icons/svg/chargingstation.svg | 36 + jsowell-ui/src/assets/icons/svg/chart.svg | 1 + jsowell-ui/src/assets/icons/svg/checkbox.svg | 1 + jsowell-ui/src/assets/icons/svg/clipboard.svg | 1 + jsowell-ui/src/assets/icons/svg/code.svg | 1 + jsowell-ui/src/assets/icons/svg/color.svg | 1 + jsowell-ui/src/assets/icons/svg/component.svg | 1 + jsowell-ui/src/assets/icons/svg/dashboard.svg | 1 + .../src/assets/icons/svg/date-range.svg | 1 + jsowell-ui/src/assets/icons/svg/date.svg | 1 + jsowell-ui/src/assets/icons/svg/dict.svg | 1 + .../src/assets/icons/svg/documentation.svg | 1 + jsowell-ui/src/assets/icons/svg/download.svg | 1 + jsowell-ui/src/assets/icons/svg/drag.svg | 1 + jsowell-ui/src/assets/icons/svg/druid.svg | 1 + jsowell-ui/src/assets/icons/svg/edit.svg | 1 + jsowell-ui/src/assets/icons/svg/education.svg | 1 + jsowell-ui/src/assets/icons/svg/electric.svg | 33 + jsowell-ui/src/assets/icons/svg/email.svg | 1 + jsowell-ui/src/assets/icons/svg/example.svg | 1 + jsowell-ui/src/assets/icons/svg/excel.svg | 1 + .../src/assets/icons/svg/exit-fullscreen.svg | 1 + jsowell-ui/src/assets/icons/svg/eye-open.svg | 1 + jsowell-ui/src/assets/icons/svg/eye.svg | 1 + jsowell-ui/src/assets/icons/svg/form.svg | 1 + .../src/assets/icons/svg/fullscreen.svg | 1 + jsowell-ui/src/assets/icons/svg/github.svg | 1 + jsowell-ui/src/assets/icons/svg/guide.svg | 1 + jsowell-ui/src/assets/icons/svg/icon.svg | 1 + .../src/assets/icons/svg/icons8-bookmark.svg | 1 + jsowell-ui/src/assets/icons/svg/input.svg | 1 + .../src/assets/icons/svg/international.svg | 1 + jsowell-ui/src/assets/icons/svg/job.svg | 1 + jsowell-ui/src/assets/icons/svg/language.svg | 1 + jsowell-ui/src/assets/icons/svg/link.svg | 1 + jsowell-ui/src/assets/icons/svg/list.svg | 1 + jsowell-ui/src/assets/icons/svg/lock.svg | 1 + jsowell-ui/src/assets/icons/svg/log.svg | 1 + .../src/assets/icons/svg/logininfor.svg | 1 + jsowell-ui/src/assets/icons/svg/merchant.svg | 23 + jsowell-ui/src/assets/icons/svg/message.svg | 1 + jsowell-ui/src/assets/icons/svg/money.svg | 1 + jsowell-ui/src/assets/icons/svg/monitor.svg | 2 + jsowell-ui/src/assets/icons/svg/nested.svg | 1 + jsowell-ui/src/assets/icons/svg/number.svg | 1 + jsowell-ui/src/assets/icons/svg/online.svg | 1 + jsowell-ui/src/assets/icons/svg/password.svg | 1 + jsowell-ui/src/assets/icons/svg/pdf.svg | 1 + jsowell-ui/src/assets/icons/svg/people.svg | 1 + jsowell-ui/src/assets/icons/svg/peoples.svg | 1 + jsowell-ui/src/assets/icons/svg/phone.svg | 1 + jsowell-ui/src/assets/icons/svg/post.svg | 1 + jsowell-ui/src/assets/icons/svg/qq.svg | 1 + jsowell-ui/src/assets/icons/svg/question.svg | 1 + jsowell-ui/src/assets/icons/svg/radio.svg | 1 + jsowell-ui/src/assets/icons/svg/rate.svg | 1 + .../src/assets/icons/svg/redis-list.svg | 2 + jsowell-ui/src/assets/icons/svg/redis.svg | 1 + jsowell-ui/src/assets/icons/svg/row.svg | 1 + jsowell-ui/src/assets/icons/svg/search.svg | 1 + jsowell-ui/src/assets/icons/svg/select.svg | 1 + jsowell-ui/src/assets/icons/svg/server.svg | 1 + jsowell-ui/src/assets/icons/svg/shopping.svg | 1 + jsowell-ui/src/assets/icons/svg/size.svg | 1 + jsowell-ui/src/assets/icons/svg/skill.svg | 1 + jsowell-ui/src/assets/icons/svg/slider.svg | 1 + jsowell-ui/src/assets/icons/svg/star.svg | 1 + jsowell-ui/src/assets/icons/svg/swagger.svg | 1 + jsowell-ui/src/assets/icons/svg/switch.svg | 1 + jsowell-ui/src/assets/icons/svg/system.svg | 2 + jsowell-ui/src/assets/icons/svg/tab.svg | 1 + jsowell-ui/src/assets/icons/svg/table.svg | 1 + jsowell-ui/src/assets/icons/svg/textarea.svg | 1 + jsowell-ui/src/assets/icons/svg/theme.svg | 1 + .../src/assets/icons/svg/time-range.svg | 1 + jsowell-ui/src/assets/icons/svg/time.svg | 1 + jsowell-ui/src/assets/icons/svg/tool.svg | 1 + jsowell-ui/src/assets/icons/svg/toolsbox.svg | 14 + .../src/assets/icons/svg/tree-table.svg | 1 + jsowell-ui/src/assets/icons/svg/tree.svg | 1 + jsowell-ui/src/assets/icons/svg/upload.svg | 1 + jsowell-ui/src/assets/icons/svg/user.svg | 1 + jsowell-ui/src/assets/icons/svg/validCode.svg | 1 + jsowell-ui/src/assets/icons/svg/wechat.svg | 1 + jsowell-ui/src/assets/icons/svg/zip.svg | 1 + jsowell-ui/src/assets/icons/svgo.yml | 22 + jsowell-ui/src/assets/images/dark.svg | 39 + jsowell-ui/src/assets/images/dingdan.png | Bin 0 -> 3277 bytes jsowell-ui/src/assets/images/light.svg | 39 + jsowell-ui/src/assets/images/lightning.png | Bin 0 -> 5672 bytes .../src/assets/images/login-background.jpg | Bin 0 -> 521275 bytes jsowell-ui/src/assets/images/profile.jpg | Bin 0 -> 81131 bytes jsowell-ui/src/assets/images/qianbao.png | Bin 0 -> 10664 bytes jsowell-ui/src/assets/images/shebei.png | Bin 0 -> 4675 bytes jsowell-ui/src/assets/images/yue.png | Bin 0 -> 8372 bytes jsowell-ui/src/assets/images/zongfeiyong.png | Bin 0 -> 9084 bytes jsowell-ui/src/assets/logo/logo.png | Bin 0 -> 4154 bytes jsowell-ui/src/assets/styles/btn.scss | 99 ++ jsowell-ui/src/assets/styles/common.css | 282 ++++ jsowell-ui/src/assets/styles/common.min.css | 1 + jsowell-ui/src/assets/styles/common.scss | 274 ++++ jsowell-ui/src/assets/styles/element-ui.scss | 92 ++ .../src/assets/styles/element-variables.scss | 31 + jsowell-ui/src/assets/styles/index.scss | 191 +++ jsowell-ui/src/assets/styles/mixin.scss | 66 + jsowell-ui/src/assets/styles/sidebar.scss | 227 +++ jsowell-ui/src/assets/styles/transition.scss | 48 + jsowell-ui/src/assets/styles/variables.scss | 54 + jsowell-ui/src/assets/图标.webp | Bin 0 -> 5266 bytes jsowell-ui/src/bus/bus.js | 3 + .../src/components/Breadcrumb/index.vue | 74 + jsowell-ui/src/components/Crontab/day.vue | 161 ++ jsowell-ui/src/components/Crontab/hour.vue | 114 ++ jsowell-ui/src/components/Crontab/index.vue | 430 ++++++ jsowell-ui/src/components/Crontab/min.vue | 116 ++ jsowell-ui/src/components/Crontab/month.vue | 114 ++ jsowell-ui/src/components/Crontab/result.vue | 559 +++++++ jsowell-ui/src/components/Crontab/second.vue | 117 ++ jsowell-ui/src/components/Crontab/week.vue | 202 +++ jsowell-ui/src/components/Crontab/year.vue | 131 ++ jsowell-ui/src/components/DictData/index.js | 49 + jsowell-ui/src/components/DictTag/index.vue | 58 + jsowell-ui/src/components/Editor/index.vue | 272 ++++ .../src/components/FileUpload/index.vue | 209 +++ jsowell-ui/src/components/Hamburger/index.vue | 44 + .../src/components/HeaderSearch/index.vue | 190 +++ .../src/components/IconSelect/index.vue | 68 + .../src/components/IconSelect/requireIcons.js | 11 + .../src/components/ImagePreview/index.vue | 84 + .../src/components/ImageUpload/index.vue | 212 +++ .../components/MapContainer/MapContainer.vue | 235 +++ .../MapContainer/MapContainer11111.vue | 176 +++ .../src/components/Pagination/index.vue | 114 ++ jsowell-ui/src/components/PanThumb/index.vue | 142 ++ .../src/components/ParentView/index.vue | 3 + .../src/components/RightPanel/index.vue | 112 ++ .../src/components/RightToolbar/index.vue | 104 ++ .../src/components/Screenfull/index.vue | 57 + .../src/components/SizeSelect/index.vue | 56 + jsowell-ui/src/components/SvgIcon/index.vue | 61 + .../src/components/ThemePicker/index.vue | 173 +++ jsowell-ui/src/components/TopNav/index.vue | 181 +++ jsowell-ui/src/components/iFrame/index.vue | 36 + jsowell-ui/src/directive/dialog/drag.js | 63 + jsowell-ui/src/directive/dialog/dragHeight.js | 33 + jsowell-ui/src/directive/dialog/dragWidth.js | 29 + jsowell-ui/src/directive/index.js | 23 + jsowell-ui/src/directive/module/clipboard.js | 53 + .../src/directive/permission/hasPermi.js | 27 + .../src/directive/permission/hasRole.js | 27 + jsowell-ui/src/layout/components/AppMain.vue | 57 + .../src/layout/components/InnerLink/index.vue | 27 + jsowell-ui/src/layout/components/Navbar.vue | 188 +++ .../src/layout/components/Settings/index.vue | 260 ++++ .../layout/components/Sidebar/FixiOSBug.js | 25 + .../src/layout/components/Sidebar/Item.vue | 33 + .../src/layout/components/Sidebar/Link.vue | 43 + .../src/layout/components/Sidebar/Logo.vue | 93 ++ .../layout/components/Sidebar/SidebarItem.vue | 100 ++ .../src/layout/components/Sidebar/index.vue | 57 + .../layout/components/TagsView/ScrollPane.vue | 94 ++ .../src/layout/components/TagsView/index.vue | 326 ++++ jsowell-ui/src/layout/components/index.js | 5 + jsowell-ui/src/layout/index.vue | 111 ++ jsowell-ui/src/layout/mixin/ResizeHandler.js | 45 + jsowell-ui/src/main.js | 86 ++ jsowell-ui/src/permission.js | 56 + jsowell-ui/src/plugins/auth.js | 60 + jsowell-ui/src/plugins/cache.js | 77 + jsowell-ui/src/plugins/download.js | 72 + jsowell-ui/src/plugins/index.js | 20 + jsowell-ui/src/plugins/modal.js | 83 + jsowell-ui/src/plugins/tab.js | 67 + jsowell-ui/src/router/index.js | 229 +++ jsowell-ui/src/settings.js | 44 + jsowell-ui/src/store/getters.js | 19 + jsowell-ui/src/store/index.js | 25 + jsowell-ui/src/store/modules/app.js | 66 + jsowell-ui/src/store/modules/dict.js | 50 + jsowell-ui/src/store/modules/permission.js | 133 ++ jsowell-ui/src/store/modules/settings.js | 42 + jsowell-ui/src/store/modules/tagsView.js | 207 +++ jsowell-ui/src/store/modules/user.js | 96 ++ jsowell-ui/src/utils/auth.js | 15 + jsowell-ui/src/utils/common.js | 248 +++ jsowell-ui/src/utils/dict/Dict.js | 82 + jsowell-ui/src/utils/dict/DictConverter.js | 17 + jsowell-ui/src/utils/dict/DictData.js | 13 + jsowell-ui/src/utils/dict/DictMeta.js | 38 + jsowell-ui/src/utils/dict/DictOptions.js | 51 + jsowell-ui/src/utils/dict/index.js | 33 + jsowell-ui/src/utils/errorCode.js | 6 + jsowell-ui/src/utils/generator/config.js | 438 ++++++ jsowell-ui/src/utils/generator/css.js | 18 + .../src/utils/generator/drawingDefault.js | 29 + jsowell-ui/src/utils/generator/html.js | 359 +++++ jsowell-ui/src/utils/generator/icon.json | 1 + jsowell-ui/src/utils/generator/js.js | 236 +++ jsowell-ui/src/utils/generator/render.js | 126 ++ jsowell-ui/src/utils/index.js | 390 +++++ jsowell-ui/src/utils/jsencrypt.js | 30 + jsowell-ui/src/utils/permission.js | 51 + jsowell-ui/src/utils/request.js | 158 ++ jsowell-ui/src/utils/scroll-to.js | 58 + jsowell-ui/src/utils/validate.js | 83 + jsowell-ui/src/views/base.vue | 23 + .../template/components/addBilling.vue | 728 +++++++++ .../components/addOrUpdateBilling.vue | 460 ++++++ .../billing/template/components/basic.vue | 34 + .../template/components/chargeable .vue | 133 ++ .../src/views/billing/template/index.vue | 382 +++++ .../views/components/icons/element-icons.js | 3 + .../src/views/components/icons/index.vue | 87 ++ .../src/views/components/icons/svg-icons.js | 10 + jsowell-ui/src/views/dashboard/BarChart.vue | 102 ++ jsowell-ui/src/views/dashboard/LineChart.vue | 135 ++ jsowell-ui/src/views/dashboard/PanelGroup.vue | 181 +++ jsowell-ui/src/views/dashboard/PieChart.vue | 79 + .../src/views/dashboard/RaddarChart.vue | 116 ++ .../src/views/dashboard/mixins/resize.js | 56 + jsowell-ui/src/views/error/401.vue | 88 ++ jsowell-ui/src/views/error/404.vue | 233 +++ jsowell-ui/src/views/homeIndex/homeIndex.vue | 389 +++++ .../src/views/indent/components/imgInput.vue | 202 +++ .../src/views/indent/components/middle.vue | 26 + .../src/views/indent/components/upSide.vue | 54 + jsowell-ui/src/views/indent/index.vue | 31 + jsowell-ui/src/views/index.vue | 97 ++ jsowell-ui/src/views/index_v1.vue | 95 ++ jsowell-ui/src/views/login.vue | 219 +++ jsowell-ui/src/views/member/info/detail.vue | 357 +++++ jsowell-ui/src/views/member/info/index.vue | 390 +++++ jsowell-ui/src/views/monitor/cache/index.vue | 146 ++ jsowell-ui/src/views/monitor/cache/list.vue | 241 +++ jsowell-ui/src/views/monitor/druid/index.vue | 15 + jsowell-ui/src/views/monitor/job/index.vue | 515 +++++++ jsowell-ui/src/views/monitor/job/log.vue | 295 ++++ .../src/views/monitor/logininfor/index.vue | 245 +++ jsowell-ui/src/views/monitor/online/index.vue | 122 ++ .../src/views/monitor/operlog/index.vue | 305 ++++ jsowell-ui/src/views/monitor/server/index.vue | 207 +++ .../src/views/order/abnormalRecord/index.vue | 423 +++++ jsowell-ui/src/views/order/order/index.vue | 521 +++++++ .../src/views/order/order/orderDetail.vue | 318 ++++ .../src/views/order/order/orderEcharts.vue | 241 +++ jsowell-ui/src/views/pile/basic/detail.vue | 410 +++++ jsowell-ui/src/views/pile/basic/index.vue | 570 +++++++ jsowell-ui/src/views/pile/connector/index.vue | 307 ++++ jsowell-ui/src/views/pile/merchant/edit.vue | 26 + jsowell-ui/src/views/pile/merchant/index.vue | 419 +++++ jsowell-ui/src/views/pile/model/index.vue | 427 ++++++ jsowell-ui/src/views/pile/sim/index.vue | 448 ++++++ .../pile/station/components/SiteInfo.vue | 345 +++++ .../views/pile/station/components/amend.vue | 36 + .../pile/station/components/amend/car.vue | 489 ++++++ .../components/amend/electromobile.vue | 207 +++ .../views/pile/station/components/billing.vue | 357 +++++ .../pile/station/components/bondedDevice.vue | 85 ++ .../pile/station/components/expenses.vue | 51 + .../src/views/pile/station/connectorList.vue | 106 ++ jsowell-ui/src/views/pile/station/detail.vue | 105 ++ jsowell-ui/src/views/pile/station/edit.vue | 3 + jsowell-ui/src/views/pile/station/index.vue | 528 +++++++ .../src/views/pile/station/pileList.vue | 333 ++++ .../views/pile/station/stationOrderList.vue | 373 +++++ jsowell-ui/src/views/redirect.vue | 12 + jsowell-ui/src/views/register.vue | 209 +++ jsowell-ui/src/views/system/config/index.vue | 343 +++++ jsowell-ui/src/views/system/dept/index.vue | 336 ++++ jsowell-ui/src/views/system/dict/data.vue | 402 +++++ jsowell-ui/src/views/system/dict/index.vue | 347 +++++ jsowell-ui/src/views/system/menu/index.vue | 453 ++++++ jsowell-ui/src/views/system/notice/index.vue | 312 ++++ jsowell-ui/src/views/system/post/index.vue | 309 ++++ jsowell-ui/src/views/system/role/authUser.vue | 199 +++ jsowell-ui/src/views/system/role/index.vue | 614 ++++++++ .../src/views/system/role/selectUser.vue | 138 ++ jsowell-ui/src/views/system/user/authRole.vue | 117 ++ jsowell-ui/src/views/system/user/index.vue | 673 ++++++++ .../src/views/system/user/profile/index.vue | 91 ++ .../views/system/user/profile/resetPwd.vue | 68 + .../views/system/user/profile/userAvatar.vue | 172 +++ .../views/system/user/profile/userInfo.vue | 75 + .../src/views/tool/gen/basicInfoForm.vue | 60 + jsowell-ui/src/views/tool/gen/editTable.vue | 234 +++ jsowell-ui/src/views/tool/gen/genInfoForm.vue | 299 ++++ jsowell-ui/src/views/tool/gen/importTable.vue | 120 ++ jsowell-ui/src/views/tool/gen/index.vue | 337 ++++ jsowell-ui/src/views/tool/swagger/index.vue | 15 + jsowell-ui/vue.config.js | 137 ++ pom.xml | 341 +++++ 1007 files changed, 109050 insertions(+) create mode 100644 .gitignore create mode 100644 bin/clean.bat create mode 100644 bin/package.bat create mode 100644 bin/run.bat create mode 100644 doc/2022年11月17日逻辑改动.md create mode 100644 doc/接口文档.md create mode 100644 doc/接口文档New.md create mode 100644 doc/测试问题.md create mode 100644 jsowell-admin/pom.xml create mode 100644 jsowell-admin/src/main/java/com/jsowell/JsowellApplication.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/JsowellServletInitializer.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/api/uniapp/JumpController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/api/uniapp/MemberController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/api/uniapp/OrderController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/api/uniapp/PayController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/api/uniapp/PersonPileController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/api/uniapp/PileController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/service/MemberService.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/service/OrderService.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/service/PileRemoteService.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/service/PileService.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/common/CaptchaController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/common/CommonController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/index/indexController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/monitor/CacheController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/monitor/ServerController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/monitor/SysLogininforController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/monitor/SysOperlogController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/monitor/SysUserOnlineController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/pile/MemberBasicInfoController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/pile/OrderAbnormalRecordController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/pile/OrderBasicInfoController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileBasicInfoController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileBillingTemplateController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileConnectorInfoController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileLicenceInfoController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileMerchantInfoController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileModelInfoController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileRemoteController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileSimInfoController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileStationInfoController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysConfigController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysDeptController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysDictDataController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysDictTypeController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysIndexController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysLoginController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysMenuController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysNoticeController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysPostController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysProfileController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysRegisterController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysRoleController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysUserController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/tool/SwaggerController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/tool/TestController.java create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/core/config/SwaggerConfig.java create mode 100644 jsowell-admin/src/main/resources/META-INF/spring-devtools.properties create mode 100644 jsowell-admin/src/main/resources/application-dev.yml create mode 100644 jsowell-admin/src/main/resources/application-prd.yml create mode 100644 jsowell-admin/src/main/resources/application.yml create mode 100644 jsowell-admin/src/main/resources/banner.txt create mode 100644 jsowell-admin/src/main/resources/i18n/messages.properties create mode 100644 jsowell-admin/src/main/resources/logback.xml create mode 100644 jsowell-admin/src/main/resources/mybatis/mybatis-config.xml create mode 100644 jsowell-admin/src/test/java/SpringBootTestController.java create mode 100644 jsowell-common/pom.xml create mode 100644 jsowell-common/src/main/java/com/jsowell/common/annotation/Anonymous.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/annotation/DataScope.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/annotation/DataSource.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/annotation/Excel.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/annotation/Excels.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/annotation/Log.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/annotation/RateLimiter.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/annotation/RepeatSubmit.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/config/JsowellConfig.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/constant/CacheConstants.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/constant/Constants.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/constant/GenConstants.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/constant/HttpStatus.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/constant/ScheduleConstants.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/constant/UserConstants.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/constant/WeiXinConstants.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/controller/BaseController.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/domain/AjaxResult.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/domain/BaseEntity.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/domain/R.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/domain/TreeEntity.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/domain/TreeSelect.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/domain/entity/SysDept.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/domain/entity/SysDictData.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/domain/entity/SysDictType.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/domain/entity/SysMenu.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/domain/entity/SysRole.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/domain/entity/SysUser.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/domain/model/LoginBody.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/domain/model/LoginUser.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/domain/model/RegisterBody.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/domain/ykc/LoginRequestData.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/domain/ykc/RealTimeMonitorData.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/domain/ykc/TransactionRecordsData.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/domain/ykc/YKCDataProtocol.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/domain/ykc/YKCFrameTypeCode.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/page/PageDomain.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/page/PageResponse.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/page/TableDataInfo.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/page/TableSupport.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/redis/RedisCache.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/text/CharsetKit.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/text/Convert.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/core/text/StrFormatter.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/BusinessStatus.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/BusinessType.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/DataSourceType.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/DelFlagEnum.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/HttpMethod.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/LimitType.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/MemberWalletEnum.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/OperatorType.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/SoftwareProtocolEnum.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/UserStatus.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/sim/SimCardStatusCorrespondEnum.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/sim/SimSupplierEnum.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/uniapp/BalanceChangesEnum.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/weixin/BusinessType.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/weixin/WeiXinPayParam.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/weixin/WeiXinPayTradeStatus.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/ykc/ActionTypeEnum.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/ykc/BillingTimeEnum.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/ykc/BusinessTypeEnum.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/ykc/ChargingFailedReasonEnum.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/ykc/OrderPayModeEnum.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/ykc/OrderPayRecordEnum.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/ykc/OrderPayStatusEnum.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/ykc/OrderStatusEnum.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/ykc/PayModeEnum.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/ykc/PileChannelEntity.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/ykc/PileConnectorDataBaseStatusEnum.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/ykc/PileConnectorStatusEnum.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/ykc/PileStatusEnum.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/ykc/ReturnCodeEnum.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/ykc/ScenarioEnum.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/ykc/StopChargingFailedReasonEnum.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/ykc/YKCChargingStopReasonEnum.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/enums/ykc/YKCPileFaultReasonEnum.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/exception/BusinessException.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/exception/DemoModeException.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/exception/GlobalException.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/exception/ServiceException.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/exception/UtilException.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/exception/base/BaseException.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/exception/file/FileException.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/exception/file/FileNameLengthLimitExceededException.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/exception/file/FileSizeLimitExceededException.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/exception/file/InvalidExtensionException.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/exception/job/TaskException.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/exception/user/CaptchaException.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/exception/user/CaptchaExpireException.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/exception/user/UserException.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/exception/user/UserPasswordNotMatchException.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/exception/user/UserPasswordRetryLimitExceedException.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/filter/PropertyPreExcludeFilter.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/filter/RepeatableFilter.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/filter/RepeatedlyRequestWrapper.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/filter/XssFilter.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/filter/XssHttpServletRequestWrapper.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/response/RestApiResponse.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/AESUtil.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/Arith.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/BytesUtil.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/CRC16Util.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/Cp56Time2a/Cp56Time2aUtil.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/DateUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/DictUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/DistanceUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/ExceptionUtil.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/JWTUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/LogUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/MessageUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/PageUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/RandomUtil.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/SMSUtil.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/SecurityUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/ServletUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/StringUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/Threads.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/YKCUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/bean/BeanUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/bean/BeanValidators.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/file/FileTypeUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/file/FileUploadUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/file/FileUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/file/ImageUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/file/MimeTypeUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/html/EscapeUtil.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/html/HTMLFilter.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/http/HttpHelper.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/http/HttpUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/id/IdUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/id/Seq.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/id/SnUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/id/SnowflakeIdWorker.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/id/UUID.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/ip/AddressUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/ip/IpUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/poi/ExcelHandlerAdapter.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/poi/ExcelUtil.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/reflect/ReflectUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/sign/Base64.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/sign/MD5Util.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/sim/SimCardUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/sim/XunZhongSimUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/spring/SpringUtils.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/util/sql/SqlUtil.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/xss/Xss.java create mode 100644 jsowell-common/src/main/java/com/jsowell/common/xss/XssValidator.java create mode 100644 jsowell-framework/pom.xml create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/aspectj/DataScopeAspect.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/aspectj/DataSourceAspect.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/aspectj/LogAspect.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/aspectj/RateLimiterAspect.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/config/ApplicationConfig.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/config/CaptchaConfig.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/config/DruidConfig.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/config/FastJson2JsonRedisSerializer.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/config/FilterConfig.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/config/KaptchaTextCreator.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/config/MyBatisConfig.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/config/RedisConfig.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/config/ResourcesConfig.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/config/SecurityConfig.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/config/ServerConfig.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/config/ThreadPoolConfig.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/config/properties/DruidProperties.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/config/properties/PermitAllUrlProperties.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/datasource/DynamicDataSource.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/datasource/DynamicDataSourceContextHolder.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/interceptor/RepeatSubmitInterceptor.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/interceptor/impl/SameUrlDataInterceptor.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/manager/AsyncManager.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/manager/ShutdownManager.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/manager/factory/AsyncFactory.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/security/context/AuthenticationContextHolder.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/security/filter/JwtAuthenticationTokenFilter.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/security/handle/AuthenticationEntryPointImpl.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/security/handle/LogoutSuccessHandlerImpl.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/web/domain/Server.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/web/domain/server/Cpu.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/web/domain/server/Jvm.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/web/domain/server/Mem.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/web/domain/server/Sys.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/web/domain/server/SysFile.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/web/exception/GlobalExceptionHandler.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/web/service/PermissionService.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/web/service/SysLoginService.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/web/service/SysPasswordService.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/web/service/SysPermissionService.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/web/service/SysRegisterService.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/web/service/TokenService.java create mode 100644 jsowell-framework/src/main/java/com/jsowell/framework/web/service/UserDetailsServiceImpl.java create mode 100644 jsowell-generator/pom.xml create mode 100644 jsowell-generator/src/main/java/com/jsowell/generator/config/GenConfig.java create mode 100644 jsowell-generator/src/main/java/com/jsowell/generator/controller/GenController.java create mode 100644 jsowell-generator/src/main/java/com/jsowell/generator/domain/GenTable.java create mode 100644 jsowell-generator/src/main/java/com/jsowell/generator/domain/GenTableColumn.java create mode 100644 jsowell-generator/src/main/java/com/jsowell/generator/mapper/GenTableColumnMapper.java create mode 100644 jsowell-generator/src/main/java/com/jsowell/generator/mapper/GenTableMapper.java create mode 100644 jsowell-generator/src/main/java/com/jsowell/generator/service/IGenTableColumnService.java create mode 100644 jsowell-generator/src/main/java/com/jsowell/generator/service/IGenTableService.java create mode 100644 jsowell-generator/src/main/java/com/jsowell/generator/service/impl/GenTableColumnServiceImpl.java create mode 100644 jsowell-generator/src/main/java/com/jsowell/generator/service/impl/GenTableServiceImpl.java create mode 100644 jsowell-generator/src/main/java/com/jsowell/generator/util/GenUtils.java create mode 100644 jsowell-generator/src/main/java/com/jsowell/generator/util/VelocityInitializer.java create mode 100644 jsowell-generator/src/main/java/com/jsowell/generator/util/VelocityUtils.java create mode 100644 jsowell-generator/src/main/resources/generator.yml create mode 100644 jsowell-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml create mode 100644 jsowell-generator/src/main/resources/mapper/generator/GenTableMapper.xml create mode 100644 jsowell-generator/src/main/resources/vm/java/controller.java.vm create mode 100644 jsowell-generator/src/main/resources/vm/java/domain.java.vm create mode 100644 jsowell-generator/src/main/resources/vm/java/mapper.java.vm create mode 100644 jsowell-generator/src/main/resources/vm/java/service.java.vm create mode 100644 jsowell-generator/src/main/resources/vm/java/serviceImpl.java.vm create mode 100644 jsowell-generator/src/main/resources/vm/java/sub-domain.java.vm create mode 100644 jsowell-generator/src/main/resources/vm/js/api.js.vm create mode 100644 jsowell-generator/src/main/resources/vm/sql/sql.vm create mode 100644 jsowell-generator/src/main/resources/vm/vue/index-tree.vue.vm create mode 100644 jsowell-generator/src/main/resources/vm/vue/index.vue.vm create mode 100644 jsowell-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm create mode 100644 jsowell-generator/src/main/resources/vm/vue/v3/index.vue.vm create mode 100644 jsowell-generator/src/main/resources/vm/xml/mapper.xml.vm create mode 100644 jsowell-netty/pom.xml create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/client/NettyClient.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/client/NettyClientChannelInitializer.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/client/NettyClientHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/client/TestNettyClient.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/GetRealTimeMonitorDataCommand.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/IssueQRCodeCommand.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/ProofreadTimeCommand.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/PublishPileBillingTemplateCommand.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/RebootCommand.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/StartChargingCommand.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/StopChargingCommand.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/UpdateFileCommand.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/decoder/CustomDecoder.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/decoder/StartAndLengthFieldFrameDecoder.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/factory/YKCOperateFactory.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/AbstractHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/BMSAbortDuringChargingPhaseHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/BMSDemandAndChargerOutputHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/BMSInformationHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/BillingTemplateRequestHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/BillingTemplateResponseHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/BillingTemplateSettingHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/BillingTemplateValidateRequestHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/ChargeEndHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/ChargerAbortedDuringChargingPhaseHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/ChargingHandshakeHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/ConfirmStartChargingRequestHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/ErrorMessageHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/GroundLockDataUploadHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/HeartbeatRequestHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/LoginRequestHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/OfflineCardDataCleaningHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/OfflineCardDataCleaningResponseHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/OfflineCardDataQueryHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/OfflineCardDataQueryResponseHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/OfflineCardDataSynchronizationHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/OfflineCardDataSynchronizationResponseHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/ParameterConfigurationHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/PileWorkingParameterSettingHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/PileWorkingParameterSettingResponseHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/ReadRealTimeMonitorDataHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteAccountBalanceUpdateRequestHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteControlGroundLockHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteControlGroundLockResponseHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteIssuedQrCodeHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteIssuedQrCodeResponseHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteRestartHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteRestartResponseHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteStartChargingRequestHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteStopChargingRequestHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteUpdateHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteUpdateResponseHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/TimeCheckSettingHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/TimeCheckSettingResponseHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/TransactionRecordsRequestHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/handler/UploadRealTimeMonitorHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/server/yunkuaichong/NettyServer.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/server/yunkuaichong/NettyServerChannelInitializer.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/server/yunkuaichong/NettyServerHandler.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/service/yunkuaichong/YKCBusinessService.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/service/yunkuaichong/YKCPushCommandService.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/service/yunkuaichong/impl/YKCBusinessServiceImpl.java create mode 100644 jsowell-netty/src/main/java/com/jsowell/netty/service/yunkuaichong/impl/YKCPushCommandServiceImpl.java create mode 100644 jsowell-pile/pom.xml create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/domain/MemberBasicInfo.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/domain/MemberTransactionRecord.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/domain/MemberWalletInfo.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/domain/MemberWalletLog.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/domain/OrderAbnormalRecord.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/domain/OrderBasicInfo.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/domain/OrderDetail.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/domain/OrderPayRecord.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/domain/PileBasicInfo.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/domain/PileBillingDetail.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/domain/PileBillingRelation.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/domain/PileBillingTemplate.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/domain/PileConnectorInfo.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/domain/PileLicenceInfo.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/domain/PileMemberRelation.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/domain/PileMerchantInfo.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/domain/PileModelInfo.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/domain/PileMsgRecord.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/domain/PileSimInfo.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/domain/PileStationInfo.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/domain/TransactionRecords.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/domain/WxpayCallbackRecord.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/domain/WxpayRefundCallback.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/BaseMemberDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/BasicPileDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/BatchCreatePileDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/BillingTimeDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/CreateOrUpdateBillingTemplateDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/FastCreateStationDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/GenerateOrderDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/ImportBillingTemplateDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/IndexQueryDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/MemberRegisterAndLoginDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/MemberRegisterDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/PayOrderDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/PayOrderSuccessCallbackDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/PaymentScenarioDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/PileMemberBindingDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/PublishBillingTemplateDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/QueryConnectorDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/QueryConnectorListDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/QueryOrderDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/QueryPersonPileDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/QueryPileDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/QuerySimInfoDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/QueryStationDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/RemoteUpdatePileFileDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/ReplaceMerchantStationDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/SettleOrderDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/SimRenewDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/StartChargingDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/StopChargingDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/UniAppQueryMemberBalanceDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/UniAppQueryOrderDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/WechatLoginDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/WeixinPayDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/mapper/MemberBasicInfoMapper.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/mapper/MemberTransactionRecordMapper.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/mapper/MemberWalletInfoMapper.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/mapper/MemberWalletLogMapper.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/mapper/OrderAbnormalRecordMapper.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/mapper/OrderBasicInfoMapper.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/mapper/OrderPayRecordMapper.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileBasicInfoMapper.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileBillingTemplateMapper.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileConnectorInfoMapper.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileLicenceInfoMapper.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileMemberRelationMapper.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileMerchantInfoMapper.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileModelInfoMapper.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileMsgRecordMapper.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileSimInfoMapper.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileStationInfoMapper.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/mapper/WxpayCallbackRecordMapper.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/mapper/WxpayRefundCallbackMapper.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/IMemberBasicInfoService.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/IMemberTransactionRecordService.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/IMemberWalletInfoService.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/IMemberWalletLogService.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/IOrderAbnormalRecordService.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/IOrderBasicInfoService.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/IOrderPayRecordService.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/IPileBasicInfoService.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/IPileBillingTemplateService.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/IPileConnectorInfoService.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/IPileLicenceInfoService.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/IPileMemberRelationService.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/IPileMerchantInfoService.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/IPileModelInfoService.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/IPileMsgRecordService.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/IPileSimInfoService.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/IPileStationInfoService.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/SimCardService.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/WechatPayService.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/WxpayCallbackRecordService.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/WxpayRefundCallbackService.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/impl/MemberBasicInfoServiceImpl.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/impl/MemberTransactionRecordServiceImpl.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/impl/MemberWalletInfoServiceImpl.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/impl/MemberWalletLogServiceImpl.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderAbnormalRecordServiceImpl.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderPayRecordServiceImpl.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileBasicInfoServiceImpl.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileBillingTemplateServiceImpl.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileConnectorInfoServiceImpl.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileLicenceInfoServiceImpl.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileMemberRelationServiceImpl.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileMerchantInfoServiceImpl.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileModelInfoServiceImpl.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileMsgRecordServiceImpl.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileSimInfoServiceImpl.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileStationInfoServiceImpl.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/impl/WechatPayServiceImpl.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/impl/WxpayCallbackRecordServiceImpl.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/service/impl/WxpayRefundCallbackServiceImpl.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/transaction/dto/BillingTemplateTransactionDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/transaction/dto/MemberTransactionDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/transaction/dto/OrderTransactionDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/transaction/dto/PileTransactionDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/transaction/service/TransactionService.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/base/ConnectorInfoVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/base/MerchantInfoVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/base/PileInfoVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/base/StationInfoVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/BillingPriceVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/CurrentTimePriceDetails.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/MemberVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/MemberWalletLogVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/OrderVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/PersonPileConnectorSumInfoVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/PersonPileRealTimeVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/PersonalPileInfoVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/PileConnectorDetailVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/PileConnectorVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/PileDetailVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/SendMessageVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/UniAppOrderVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/web/BillingDetailVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/web/BillingTemplateVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/web/EchoBillingTemplateVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/web/IndexGeneralSituationVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/web/IndexOrderInfoVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/web/MemberTransactionVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/web/OrderDetailInfoVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/web/OrderListVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/web/OrderRealTimeInfoVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/web/OrderTotalDataVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/web/PileCommunicationLogVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/web/PileConnectorInfoVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/web/PileDetailVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/web/PileModelInfoVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/web/PileStationVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/web/SimCardInfoVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/web/SimCardVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/web/SimRenewResultVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/web/UpdateMemberBalanceDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/web/WuLianSimData.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/web/WuLianSimRenewVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/web/XunZhongSimData.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/wxpay/common/WeChatPayParameter.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/wxpay/config/WechatPayConfig.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/wxpay/config/WeixinLoginProperties.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/wxpay/dto/AppletTemplateMessageSendDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/wxpay/dto/WeChatRefundDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/wxpay/dto/WechatSendMsgDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/wxpay/response/WechatPayNotifyParameter.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/wxpay/response/WechatPayNotifyResource.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/wxpay/response/WechatPayRefundNotifyResource.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/wxpay/response/WechatPayRefundRequest.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/wxpay/response/WechatPayRefundResponse.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/wxpay/service/WxAppletRemoteService.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/wxpay/utils/AesUtil.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/wxpay/utils/BufferedImageLuminanceSource.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/wxpay/utils/HttpUtils.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/wxpay/utils/QRCodeUtil.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/wxpay/utils/WechatPayUtils.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/wxpay/vo/R.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/wxpay/vo/ResultCodeEnum.java create mode 100644 jsowell-pile/src/main/resources/mapper/mapper/pile/MemberTransactionRecordMapper.xml create mode 100644 jsowell-pile/src/main/resources/mapper/pile/MemberBasicInfoMapper.xml create mode 100644 jsowell-pile/src/main/resources/mapper/pile/MemberWalletInfoMapper.xml create mode 100644 jsowell-pile/src/main/resources/mapper/pile/MemberWalletLogMapper.xml create mode 100644 jsowell-pile/src/main/resources/mapper/pile/OrderAbnormalRecordMapper.xml create mode 100644 jsowell-pile/src/main/resources/mapper/pile/OrderBasicInfoMapper.xml create mode 100644 jsowell-pile/src/main/resources/mapper/pile/OrderPayRecordMapper.xml create mode 100644 jsowell-pile/src/main/resources/mapper/pile/PileBasicInfoMapper.xml create mode 100644 jsowell-pile/src/main/resources/mapper/pile/PileBillingTemplateMapper.xml create mode 100644 jsowell-pile/src/main/resources/mapper/pile/PileConnectorInfoMapper.xml create mode 100644 jsowell-pile/src/main/resources/mapper/pile/PileLicenceInfoMapper.xml create mode 100644 jsowell-pile/src/main/resources/mapper/pile/PileMemberRelationMapper.xml create mode 100644 jsowell-pile/src/main/resources/mapper/pile/PileMerchantInfoMapper.xml create mode 100644 jsowell-pile/src/main/resources/mapper/pile/PileModelInfoMapper.xml create mode 100644 jsowell-pile/src/main/resources/mapper/pile/PileMsgRecordMapper.xml create mode 100644 jsowell-pile/src/main/resources/mapper/pile/PileSimInfoMapper.xml create mode 100644 jsowell-pile/src/main/resources/mapper/pile/PileStationInfoMapper.xml create mode 100644 jsowell-pile/src/main/resources/mapper/pile/WxpayCallbackRecordMapper.xml create mode 100644 jsowell-pile/src/main/resources/mapper/pile/WxpayRefundCallbackMapper.xml create mode 100644 jsowell-quartz/pom.xml create mode 100644 jsowell-quartz/src/main/java/com/jsowell/quartz/config/ScheduleConfig.java create mode 100644 jsowell-quartz/src/main/java/com/jsowell/quartz/controller/SysJobController.java create mode 100644 jsowell-quartz/src/main/java/com/jsowell/quartz/controller/SysJobLogController.java create mode 100644 jsowell-quartz/src/main/java/com/jsowell/quartz/domain/SysJob.java create mode 100644 jsowell-quartz/src/main/java/com/jsowell/quartz/domain/SysJobLog.java create mode 100644 jsowell-quartz/src/main/java/com/jsowell/quartz/mapper/SysJobLogMapper.java create mode 100644 jsowell-quartz/src/main/java/com/jsowell/quartz/mapper/SysJobMapper.java create mode 100644 jsowell-quartz/src/main/java/com/jsowell/quartz/service/ISysJobLogService.java create mode 100644 jsowell-quartz/src/main/java/com/jsowell/quartz/service/ISysJobService.java create mode 100644 jsowell-quartz/src/main/java/com/jsowell/quartz/service/impl/SysJobLogServiceImpl.java create mode 100644 jsowell-quartz/src/main/java/com/jsowell/quartz/service/impl/SysJobServiceImpl.java create mode 100644 jsowell-quartz/src/main/java/com/jsowell/quartz/task/JsowellTask.java create mode 100644 jsowell-quartz/src/main/java/com/jsowell/quartz/task/RyTask.java create mode 100644 jsowell-quartz/src/main/java/com/jsowell/quartz/util/AbstractQuartzJob.java create mode 100644 jsowell-quartz/src/main/java/com/jsowell/quartz/util/CronUtils.java create mode 100644 jsowell-quartz/src/main/java/com/jsowell/quartz/util/JobInvokeUtil.java create mode 100644 jsowell-quartz/src/main/java/com/jsowell/quartz/util/QuartzDisallowConcurrentExecution.java create mode 100644 jsowell-quartz/src/main/java/com/jsowell/quartz/util/QuartzJobExecution.java create mode 100644 jsowell-quartz/src/main/java/com/jsowell/quartz/util/ScheduleUtils.java create mode 100644 jsowell-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml create mode 100644 jsowell-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml create mode 100644 jsowell-system/pom.xml create mode 100644 jsowell-system/src/main/java/com/jsowell/system/domain/SysCache.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/domain/SysConfig.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/domain/SysLogininfor.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/domain/SysNotice.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/domain/SysOperLog.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/domain/SysPost.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/domain/SysRoleDept.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/domain/SysRoleMenu.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/domain/SysUserOnline.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/domain/SysUserPost.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/domain/SysUserRole.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/mapper/SysConfigMapper.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/mapper/SysDeptMapper.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/mapper/SysDictDataMapper.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/mapper/SysDictTypeMapper.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/mapper/SysLogininforMapper.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/mapper/SysMenuMapper.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/mapper/SysNoticeMapper.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/mapper/SysOperLogMapper.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/mapper/SysPostMapper.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/mapper/SysRoleDeptMapper.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/mapper/SysRoleMapper.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/mapper/SysRoleMenuMapper.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/mapper/SysUserMapper.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/mapper/SysUserPostMapper.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/mapper/SysUserRoleMapper.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/SysConfigService.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/SysDeptService.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/SysDictDataService.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/SysDictTypeService.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/SysLogininforService.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/SysMenuService.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/SysNoticeService.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/SysOperLogService.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/SysPostService.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/SysRoleService.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/SysUserOnlineService.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/SysUserService.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/impl/SysConfigServiceImpl.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/impl/SysDeptServiceImpl.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/impl/SysDictDataServiceImpl.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/impl/SysDictTypeServiceImpl.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/impl/SysLogininforServiceImpl.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/impl/SysMenuServiceImpl.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/impl/SysNoticeServiceImpl.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/impl/SysOperLogServiceImpl.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/impl/SysPostServiceImpl.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/impl/SysRoleServiceImpl.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/impl/SysUserOnlineServiceImpl.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/impl/SysUserServiceImpl.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/vo/MetaVO.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/vo/RouterVO.java create mode 100644 jsowell-system/src/main/resources/mapper/system/SysConfigMapper.xml create mode 100644 jsowell-system/src/main/resources/mapper/system/SysDeptMapper.xml create mode 100644 jsowell-system/src/main/resources/mapper/system/SysDictDataMapper.xml create mode 100644 jsowell-system/src/main/resources/mapper/system/SysDictTypeMapper.xml create mode 100644 jsowell-system/src/main/resources/mapper/system/SysLogininforMapper.xml create mode 100644 jsowell-system/src/main/resources/mapper/system/SysMenuMapper.xml create mode 100644 jsowell-system/src/main/resources/mapper/system/SysNoticeMapper.xml create mode 100644 jsowell-system/src/main/resources/mapper/system/SysOperLogMapper.xml create mode 100644 jsowell-system/src/main/resources/mapper/system/SysPostMapper.xml create mode 100644 jsowell-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml create mode 100644 jsowell-system/src/main/resources/mapper/system/SysRoleMapper.xml create mode 100644 jsowell-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml create mode 100644 jsowell-system/src/main/resources/mapper/system/SysUserMapper.xml create mode 100644 jsowell-system/src/main/resources/mapper/system/SysUserPostMapper.xml create mode 100644 jsowell-system/src/main/resources/mapper/system/SysUserRoleMapper.xml create mode 100644 jsowell-thirdparty/pom.xml create mode 100644 jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/service/LianLianService.java create mode 100644 jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/service/impl/LianLianServiceImpl.java create mode 100644 jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/ChargeDetail.java create mode 100644 jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/ConnectorChargeStatusInfo.java create mode 100644 jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/ConnectorInfo.java create mode 100644 jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/ConnectorStatsInfo.java create mode 100644 jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/ConnectorStatusInfo.java create mode 100644 jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/EquipmentInfo.java create mode 100644 jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/EquipmentStatsInfo.java create mode 100644 jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/OperatorInfo.java create mode 100644 jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/OrderInfo.java create mode 100644 jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/StationInfo.java create mode 100644 jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/StationStatusInfo.java create mode 100644 jsowell-ui/.editorconfig create mode 100644 jsowell-ui/.env.development create mode 100644 jsowell-ui/.env.production create mode 100644 jsowell-ui/.env.staging create mode 100644 jsowell-ui/.eslintignore create mode 100644 jsowell-ui/.eslintrc.js create mode 100644 jsowell-ui/.gitignore create mode 100644 jsowell-ui/README.md create mode 100644 jsowell-ui/babel.config.js create mode 100644 jsowell-ui/bin/build-sit.bat create mode 100644 jsowell-ui/bin/build.bat create mode 100644 jsowell-ui/bin/package.bat create mode 100644 jsowell-ui/bin/run-web.bat create mode 100644 jsowell-ui/package.json create mode 100644 jsowell-ui/public/favicon.ico create mode 100644 jsowell-ui/public/html/ie.html create mode 100644 jsowell-ui/public/index.html create mode 100644 jsowell-ui/public/robots.txt create mode 100644 jsowell-ui/src/App.vue create mode 100644 jsowell-ui/src/api/billing/template.js create mode 100644 jsowell-ui/src/api/index/index.js create mode 100644 jsowell-ui/src/api/login.js create mode 100644 jsowell-ui/src/api/member/info.js create mode 100644 jsowell-ui/src/api/menu.js create mode 100644 jsowell-ui/src/api/monitor/cache.js create mode 100644 jsowell-ui/src/api/monitor/job.js create mode 100644 jsowell-ui/src/api/monitor/jobLog.js create mode 100644 jsowell-ui/src/api/monitor/logininfor.js create mode 100644 jsowell-ui/src/api/monitor/online.js create mode 100644 jsowell-ui/src/api/monitor/operlog.js create mode 100644 jsowell-ui/src/api/monitor/server.js create mode 100644 jsowell-ui/src/api/order/abnormalRecord.js create mode 100644 jsowell-ui/src/api/order/order.js create mode 100644 jsowell-ui/src/api/pile/basic.js create mode 100644 jsowell-ui/src/api/pile/connector.js create mode 100644 jsowell-ui/src/api/pile/merchant.js create mode 100644 jsowell-ui/src/api/pile/model.js create mode 100644 jsowell-ui/src/api/pile/sim.js create mode 100644 jsowell-ui/src/api/pile/station.js create mode 100644 jsowell-ui/src/api/system/config.js create mode 100644 jsowell-ui/src/api/system/dept.js create mode 100644 jsowell-ui/src/api/system/dict/data.js create mode 100644 jsowell-ui/src/api/system/dict/type.js create mode 100644 jsowell-ui/src/api/system/menu.js create mode 100644 jsowell-ui/src/api/system/notice.js create mode 100644 jsowell-ui/src/api/system/post.js create mode 100644 jsowell-ui/src/api/system/role.js create mode 100644 jsowell-ui/src/api/system/user.js create mode 100644 jsowell-ui/src/api/tool/gen.js create mode 100644 jsowell-ui/src/assets/401_images/401.gif create mode 100644 jsowell-ui/src/assets/404_images/404.png create mode 100644 jsowell-ui/src/assets/404_images/404_cloud.png create mode 100644 jsowell-ui/src/assets/icons/index.js create mode 100644 jsowell-ui/src/assets/icons/svg/404.svg create mode 100644 jsowell-ui/src/assets/icons/svg/bug.svg create mode 100644 jsowell-ui/src/assets/icons/svg/build.svg create mode 100644 jsowell-ui/src/assets/icons/svg/button.svg create mode 100644 jsowell-ui/src/assets/icons/svg/cascader.svg create mode 100644 jsowell-ui/src/assets/icons/svg/chargeandpark.svg create mode 100644 jsowell-ui/src/assets/icons/svg/chargingstation.svg create mode 100644 jsowell-ui/src/assets/icons/svg/chart.svg create mode 100644 jsowell-ui/src/assets/icons/svg/checkbox.svg create mode 100644 jsowell-ui/src/assets/icons/svg/clipboard.svg create mode 100644 jsowell-ui/src/assets/icons/svg/code.svg create mode 100644 jsowell-ui/src/assets/icons/svg/color.svg create mode 100644 jsowell-ui/src/assets/icons/svg/component.svg create mode 100644 jsowell-ui/src/assets/icons/svg/dashboard.svg create mode 100644 jsowell-ui/src/assets/icons/svg/date-range.svg create mode 100644 jsowell-ui/src/assets/icons/svg/date.svg create mode 100644 jsowell-ui/src/assets/icons/svg/dict.svg create mode 100644 jsowell-ui/src/assets/icons/svg/documentation.svg create mode 100644 jsowell-ui/src/assets/icons/svg/download.svg create mode 100644 jsowell-ui/src/assets/icons/svg/drag.svg create mode 100644 jsowell-ui/src/assets/icons/svg/druid.svg create mode 100644 jsowell-ui/src/assets/icons/svg/edit.svg create mode 100644 jsowell-ui/src/assets/icons/svg/education.svg create mode 100644 jsowell-ui/src/assets/icons/svg/electric.svg create mode 100644 jsowell-ui/src/assets/icons/svg/email.svg create mode 100644 jsowell-ui/src/assets/icons/svg/example.svg create mode 100644 jsowell-ui/src/assets/icons/svg/excel.svg create mode 100644 jsowell-ui/src/assets/icons/svg/exit-fullscreen.svg create mode 100644 jsowell-ui/src/assets/icons/svg/eye-open.svg create mode 100644 jsowell-ui/src/assets/icons/svg/eye.svg create mode 100644 jsowell-ui/src/assets/icons/svg/form.svg create mode 100644 jsowell-ui/src/assets/icons/svg/fullscreen.svg create mode 100644 jsowell-ui/src/assets/icons/svg/github.svg create mode 100644 jsowell-ui/src/assets/icons/svg/guide.svg create mode 100644 jsowell-ui/src/assets/icons/svg/icon.svg create mode 100644 jsowell-ui/src/assets/icons/svg/icons8-bookmark.svg create mode 100644 jsowell-ui/src/assets/icons/svg/input.svg create mode 100644 jsowell-ui/src/assets/icons/svg/international.svg create mode 100644 jsowell-ui/src/assets/icons/svg/job.svg create mode 100644 jsowell-ui/src/assets/icons/svg/language.svg create mode 100644 jsowell-ui/src/assets/icons/svg/link.svg create mode 100644 jsowell-ui/src/assets/icons/svg/list.svg create mode 100644 jsowell-ui/src/assets/icons/svg/lock.svg create mode 100644 jsowell-ui/src/assets/icons/svg/log.svg create mode 100644 jsowell-ui/src/assets/icons/svg/logininfor.svg create mode 100644 jsowell-ui/src/assets/icons/svg/merchant.svg create mode 100644 jsowell-ui/src/assets/icons/svg/message.svg create mode 100644 jsowell-ui/src/assets/icons/svg/money.svg create mode 100644 jsowell-ui/src/assets/icons/svg/monitor.svg create mode 100644 jsowell-ui/src/assets/icons/svg/nested.svg create mode 100644 jsowell-ui/src/assets/icons/svg/number.svg create mode 100644 jsowell-ui/src/assets/icons/svg/online.svg create mode 100644 jsowell-ui/src/assets/icons/svg/password.svg create mode 100644 jsowell-ui/src/assets/icons/svg/pdf.svg create mode 100644 jsowell-ui/src/assets/icons/svg/people.svg create mode 100644 jsowell-ui/src/assets/icons/svg/peoples.svg create mode 100644 jsowell-ui/src/assets/icons/svg/phone.svg create mode 100644 jsowell-ui/src/assets/icons/svg/post.svg create mode 100644 jsowell-ui/src/assets/icons/svg/qq.svg create mode 100644 jsowell-ui/src/assets/icons/svg/question.svg create mode 100644 jsowell-ui/src/assets/icons/svg/radio.svg create mode 100644 jsowell-ui/src/assets/icons/svg/rate.svg create mode 100644 jsowell-ui/src/assets/icons/svg/redis-list.svg create mode 100644 jsowell-ui/src/assets/icons/svg/redis.svg create mode 100644 jsowell-ui/src/assets/icons/svg/row.svg create mode 100644 jsowell-ui/src/assets/icons/svg/search.svg create mode 100644 jsowell-ui/src/assets/icons/svg/select.svg create mode 100644 jsowell-ui/src/assets/icons/svg/server.svg create mode 100644 jsowell-ui/src/assets/icons/svg/shopping.svg create mode 100644 jsowell-ui/src/assets/icons/svg/size.svg create mode 100644 jsowell-ui/src/assets/icons/svg/skill.svg create mode 100644 jsowell-ui/src/assets/icons/svg/slider.svg create mode 100644 jsowell-ui/src/assets/icons/svg/star.svg create mode 100644 jsowell-ui/src/assets/icons/svg/swagger.svg create mode 100644 jsowell-ui/src/assets/icons/svg/switch.svg create mode 100644 jsowell-ui/src/assets/icons/svg/system.svg create mode 100644 jsowell-ui/src/assets/icons/svg/tab.svg create mode 100644 jsowell-ui/src/assets/icons/svg/table.svg create mode 100644 jsowell-ui/src/assets/icons/svg/textarea.svg create mode 100644 jsowell-ui/src/assets/icons/svg/theme.svg create mode 100644 jsowell-ui/src/assets/icons/svg/time-range.svg create mode 100644 jsowell-ui/src/assets/icons/svg/time.svg create mode 100644 jsowell-ui/src/assets/icons/svg/tool.svg create mode 100644 jsowell-ui/src/assets/icons/svg/toolsbox.svg create mode 100644 jsowell-ui/src/assets/icons/svg/tree-table.svg create mode 100644 jsowell-ui/src/assets/icons/svg/tree.svg create mode 100644 jsowell-ui/src/assets/icons/svg/upload.svg create mode 100644 jsowell-ui/src/assets/icons/svg/user.svg create mode 100644 jsowell-ui/src/assets/icons/svg/validCode.svg create mode 100644 jsowell-ui/src/assets/icons/svg/wechat.svg create mode 100644 jsowell-ui/src/assets/icons/svg/zip.svg create mode 100644 jsowell-ui/src/assets/icons/svgo.yml create mode 100644 jsowell-ui/src/assets/images/dark.svg create mode 100644 jsowell-ui/src/assets/images/dingdan.png create mode 100644 jsowell-ui/src/assets/images/light.svg create mode 100644 jsowell-ui/src/assets/images/lightning.png create mode 100644 jsowell-ui/src/assets/images/login-background.jpg create mode 100644 jsowell-ui/src/assets/images/profile.jpg create mode 100644 jsowell-ui/src/assets/images/qianbao.png create mode 100644 jsowell-ui/src/assets/images/shebei.png create mode 100644 jsowell-ui/src/assets/images/yue.png create mode 100644 jsowell-ui/src/assets/images/zongfeiyong.png create mode 100644 jsowell-ui/src/assets/logo/logo.png create mode 100644 jsowell-ui/src/assets/styles/btn.scss create mode 100644 jsowell-ui/src/assets/styles/common.css create mode 100644 jsowell-ui/src/assets/styles/common.min.css create mode 100644 jsowell-ui/src/assets/styles/common.scss create mode 100644 jsowell-ui/src/assets/styles/element-ui.scss create mode 100644 jsowell-ui/src/assets/styles/element-variables.scss create mode 100644 jsowell-ui/src/assets/styles/index.scss create mode 100644 jsowell-ui/src/assets/styles/mixin.scss create mode 100644 jsowell-ui/src/assets/styles/sidebar.scss create mode 100644 jsowell-ui/src/assets/styles/transition.scss create mode 100644 jsowell-ui/src/assets/styles/variables.scss create mode 100644 jsowell-ui/src/assets/图标.webp create mode 100644 jsowell-ui/src/bus/bus.js create mode 100644 jsowell-ui/src/components/Breadcrumb/index.vue create mode 100644 jsowell-ui/src/components/Crontab/day.vue create mode 100644 jsowell-ui/src/components/Crontab/hour.vue create mode 100644 jsowell-ui/src/components/Crontab/index.vue create mode 100644 jsowell-ui/src/components/Crontab/min.vue create mode 100644 jsowell-ui/src/components/Crontab/month.vue create mode 100644 jsowell-ui/src/components/Crontab/result.vue create mode 100644 jsowell-ui/src/components/Crontab/second.vue create mode 100644 jsowell-ui/src/components/Crontab/week.vue create mode 100644 jsowell-ui/src/components/Crontab/year.vue create mode 100644 jsowell-ui/src/components/DictData/index.js create mode 100644 jsowell-ui/src/components/DictTag/index.vue create mode 100644 jsowell-ui/src/components/Editor/index.vue create mode 100644 jsowell-ui/src/components/FileUpload/index.vue create mode 100644 jsowell-ui/src/components/Hamburger/index.vue create mode 100644 jsowell-ui/src/components/HeaderSearch/index.vue create mode 100644 jsowell-ui/src/components/IconSelect/index.vue create mode 100644 jsowell-ui/src/components/IconSelect/requireIcons.js create mode 100644 jsowell-ui/src/components/ImagePreview/index.vue create mode 100644 jsowell-ui/src/components/ImageUpload/index.vue create mode 100644 jsowell-ui/src/components/MapContainer/MapContainer.vue create mode 100644 jsowell-ui/src/components/MapContainer/MapContainer11111.vue create mode 100644 jsowell-ui/src/components/Pagination/index.vue create mode 100644 jsowell-ui/src/components/PanThumb/index.vue create mode 100644 jsowell-ui/src/components/ParentView/index.vue create mode 100644 jsowell-ui/src/components/RightPanel/index.vue create mode 100644 jsowell-ui/src/components/RightToolbar/index.vue create mode 100644 jsowell-ui/src/components/Screenfull/index.vue create mode 100644 jsowell-ui/src/components/SizeSelect/index.vue create mode 100644 jsowell-ui/src/components/SvgIcon/index.vue create mode 100644 jsowell-ui/src/components/ThemePicker/index.vue create mode 100644 jsowell-ui/src/components/TopNav/index.vue create mode 100644 jsowell-ui/src/components/iFrame/index.vue create mode 100644 jsowell-ui/src/directive/dialog/drag.js create mode 100644 jsowell-ui/src/directive/dialog/dragHeight.js create mode 100644 jsowell-ui/src/directive/dialog/dragWidth.js create mode 100644 jsowell-ui/src/directive/index.js create mode 100644 jsowell-ui/src/directive/module/clipboard.js create mode 100644 jsowell-ui/src/directive/permission/hasPermi.js create mode 100644 jsowell-ui/src/directive/permission/hasRole.js create mode 100644 jsowell-ui/src/layout/components/AppMain.vue create mode 100644 jsowell-ui/src/layout/components/InnerLink/index.vue create mode 100644 jsowell-ui/src/layout/components/Navbar.vue create mode 100644 jsowell-ui/src/layout/components/Settings/index.vue create mode 100644 jsowell-ui/src/layout/components/Sidebar/FixiOSBug.js create mode 100644 jsowell-ui/src/layout/components/Sidebar/Item.vue create mode 100644 jsowell-ui/src/layout/components/Sidebar/Link.vue create mode 100644 jsowell-ui/src/layout/components/Sidebar/Logo.vue create mode 100644 jsowell-ui/src/layout/components/Sidebar/SidebarItem.vue create mode 100644 jsowell-ui/src/layout/components/Sidebar/index.vue create mode 100644 jsowell-ui/src/layout/components/TagsView/ScrollPane.vue create mode 100644 jsowell-ui/src/layout/components/TagsView/index.vue create mode 100644 jsowell-ui/src/layout/components/index.js create mode 100644 jsowell-ui/src/layout/index.vue create mode 100644 jsowell-ui/src/layout/mixin/ResizeHandler.js create mode 100644 jsowell-ui/src/main.js create mode 100644 jsowell-ui/src/permission.js create mode 100644 jsowell-ui/src/plugins/auth.js create mode 100644 jsowell-ui/src/plugins/cache.js create mode 100644 jsowell-ui/src/plugins/download.js create mode 100644 jsowell-ui/src/plugins/index.js create mode 100644 jsowell-ui/src/plugins/modal.js create mode 100644 jsowell-ui/src/plugins/tab.js create mode 100644 jsowell-ui/src/router/index.js create mode 100644 jsowell-ui/src/settings.js create mode 100644 jsowell-ui/src/store/getters.js create mode 100644 jsowell-ui/src/store/index.js create mode 100644 jsowell-ui/src/store/modules/app.js create mode 100644 jsowell-ui/src/store/modules/dict.js create mode 100644 jsowell-ui/src/store/modules/permission.js create mode 100644 jsowell-ui/src/store/modules/settings.js create mode 100644 jsowell-ui/src/store/modules/tagsView.js create mode 100644 jsowell-ui/src/store/modules/user.js create mode 100644 jsowell-ui/src/utils/auth.js create mode 100644 jsowell-ui/src/utils/common.js create mode 100644 jsowell-ui/src/utils/dict/Dict.js create mode 100644 jsowell-ui/src/utils/dict/DictConverter.js create mode 100644 jsowell-ui/src/utils/dict/DictData.js create mode 100644 jsowell-ui/src/utils/dict/DictMeta.js create mode 100644 jsowell-ui/src/utils/dict/DictOptions.js create mode 100644 jsowell-ui/src/utils/dict/index.js create mode 100644 jsowell-ui/src/utils/errorCode.js create mode 100644 jsowell-ui/src/utils/generator/config.js create mode 100644 jsowell-ui/src/utils/generator/css.js create mode 100644 jsowell-ui/src/utils/generator/drawingDefault.js create mode 100644 jsowell-ui/src/utils/generator/html.js create mode 100644 jsowell-ui/src/utils/generator/icon.json create mode 100644 jsowell-ui/src/utils/generator/js.js create mode 100644 jsowell-ui/src/utils/generator/render.js create mode 100644 jsowell-ui/src/utils/index.js create mode 100644 jsowell-ui/src/utils/jsencrypt.js create mode 100644 jsowell-ui/src/utils/permission.js create mode 100644 jsowell-ui/src/utils/request.js create mode 100644 jsowell-ui/src/utils/scroll-to.js create mode 100644 jsowell-ui/src/utils/validate.js create mode 100644 jsowell-ui/src/views/base.vue create mode 100644 jsowell-ui/src/views/billing/template/components/addBilling.vue create mode 100644 jsowell-ui/src/views/billing/template/components/addOrUpdateBilling.vue create mode 100644 jsowell-ui/src/views/billing/template/components/basic.vue create mode 100644 jsowell-ui/src/views/billing/template/components/chargeable .vue create mode 100644 jsowell-ui/src/views/billing/template/index.vue create mode 100644 jsowell-ui/src/views/components/icons/element-icons.js create mode 100644 jsowell-ui/src/views/components/icons/index.vue create mode 100644 jsowell-ui/src/views/components/icons/svg-icons.js create mode 100644 jsowell-ui/src/views/dashboard/BarChart.vue create mode 100644 jsowell-ui/src/views/dashboard/LineChart.vue create mode 100644 jsowell-ui/src/views/dashboard/PanelGroup.vue create mode 100644 jsowell-ui/src/views/dashboard/PieChart.vue create mode 100644 jsowell-ui/src/views/dashboard/RaddarChart.vue create mode 100644 jsowell-ui/src/views/dashboard/mixins/resize.js create mode 100644 jsowell-ui/src/views/error/401.vue create mode 100644 jsowell-ui/src/views/error/404.vue create mode 100644 jsowell-ui/src/views/homeIndex/homeIndex.vue create mode 100644 jsowell-ui/src/views/indent/components/imgInput.vue create mode 100644 jsowell-ui/src/views/indent/components/middle.vue create mode 100644 jsowell-ui/src/views/indent/components/upSide.vue create mode 100644 jsowell-ui/src/views/indent/index.vue create mode 100644 jsowell-ui/src/views/index.vue create mode 100644 jsowell-ui/src/views/index_v1.vue create mode 100644 jsowell-ui/src/views/login.vue create mode 100644 jsowell-ui/src/views/member/info/detail.vue create mode 100644 jsowell-ui/src/views/member/info/index.vue create mode 100644 jsowell-ui/src/views/monitor/cache/index.vue create mode 100644 jsowell-ui/src/views/monitor/cache/list.vue create mode 100644 jsowell-ui/src/views/monitor/druid/index.vue create mode 100644 jsowell-ui/src/views/monitor/job/index.vue create mode 100644 jsowell-ui/src/views/monitor/job/log.vue create mode 100644 jsowell-ui/src/views/monitor/logininfor/index.vue create mode 100644 jsowell-ui/src/views/monitor/online/index.vue create mode 100644 jsowell-ui/src/views/monitor/operlog/index.vue create mode 100644 jsowell-ui/src/views/monitor/server/index.vue create mode 100644 jsowell-ui/src/views/order/abnormalRecord/index.vue create mode 100644 jsowell-ui/src/views/order/order/index.vue create mode 100644 jsowell-ui/src/views/order/order/orderDetail.vue create mode 100644 jsowell-ui/src/views/order/order/orderEcharts.vue create mode 100644 jsowell-ui/src/views/pile/basic/detail.vue create mode 100644 jsowell-ui/src/views/pile/basic/index.vue create mode 100644 jsowell-ui/src/views/pile/connector/index.vue create mode 100644 jsowell-ui/src/views/pile/merchant/edit.vue create mode 100644 jsowell-ui/src/views/pile/merchant/index.vue create mode 100644 jsowell-ui/src/views/pile/model/index.vue create mode 100644 jsowell-ui/src/views/pile/sim/index.vue create mode 100644 jsowell-ui/src/views/pile/station/components/SiteInfo.vue create mode 100644 jsowell-ui/src/views/pile/station/components/amend.vue create mode 100644 jsowell-ui/src/views/pile/station/components/amend/car.vue create mode 100644 jsowell-ui/src/views/pile/station/components/amend/electromobile.vue create mode 100644 jsowell-ui/src/views/pile/station/components/billing.vue create mode 100644 jsowell-ui/src/views/pile/station/components/bondedDevice.vue create mode 100644 jsowell-ui/src/views/pile/station/components/expenses.vue create mode 100644 jsowell-ui/src/views/pile/station/connectorList.vue create mode 100644 jsowell-ui/src/views/pile/station/detail.vue create mode 100644 jsowell-ui/src/views/pile/station/edit.vue create mode 100644 jsowell-ui/src/views/pile/station/index.vue create mode 100644 jsowell-ui/src/views/pile/station/pileList.vue create mode 100644 jsowell-ui/src/views/pile/station/stationOrderList.vue create mode 100644 jsowell-ui/src/views/redirect.vue create mode 100644 jsowell-ui/src/views/register.vue create mode 100644 jsowell-ui/src/views/system/config/index.vue create mode 100644 jsowell-ui/src/views/system/dept/index.vue create mode 100644 jsowell-ui/src/views/system/dict/data.vue create mode 100644 jsowell-ui/src/views/system/dict/index.vue create mode 100644 jsowell-ui/src/views/system/menu/index.vue create mode 100644 jsowell-ui/src/views/system/notice/index.vue create mode 100644 jsowell-ui/src/views/system/post/index.vue create mode 100644 jsowell-ui/src/views/system/role/authUser.vue create mode 100644 jsowell-ui/src/views/system/role/index.vue create mode 100644 jsowell-ui/src/views/system/role/selectUser.vue create mode 100644 jsowell-ui/src/views/system/user/authRole.vue create mode 100644 jsowell-ui/src/views/system/user/index.vue create mode 100644 jsowell-ui/src/views/system/user/profile/index.vue create mode 100644 jsowell-ui/src/views/system/user/profile/resetPwd.vue create mode 100644 jsowell-ui/src/views/system/user/profile/userAvatar.vue create mode 100644 jsowell-ui/src/views/system/user/profile/userInfo.vue create mode 100644 jsowell-ui/src/views/tool/gen/basicInfoForm.vue create mode 100644 jsowell-ui/src/views/tool/gen/editTable.vue create mode 100644 jsowell-ui/src/views/tool/gen/genInfoForm.vue create mode 100644 jsowell-ui/src/views/tool/gen/importTable.vue create mode 100644 jsowell-ui/src/views/tool/gen/index.vue create mode 100644 jsowell-ui/src/views/tool/swagger/index.vue create mode 100644 jsowell-ui/vue.config.js create mode 100644 pom.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..2b3b68c30 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ +.mvn/ diff --git a/bin/clean.bat b/bin/clean.bat new file mode 100644 index 000000000..24c09741e --- /dev/null +++ b/bin/clean.bat @@ -0,0 +1,12 @@ +@echo off +echo. +echo [Ϣ] target· +echo. + +%~d0 +cd %~dp0 + +cd .. +call mvn clean + +pause \ No newline at end of file diff --git a/bin/package.bat b/bin/package.bat new file mode 100644 index 000000000..c693ec067 --- /dev/null +++ b/bin/package.bat @@ -0,0 +1,12 @@ +@echo off +echo. +echo [Ϣ] Weḅwar/jarļ +echo. + +%~d0 +cd %~dp0 + +cd .. +call mvn clean package -Dmaven.test.skip=true + +pause \ No newline at end of file diff --git a/bin/run.bat b/bin/run.bat new file mode 100644 index 000000000..41efbd0f3 --- /dev/null +++ b/bin/run.bat @@ -0,0 +1,14 @@ +@echo off +echo. +echo [Ϣ] ʹJarWeb̡ +echo. + +cd %~dp0 +cd ../ruoyi-admin/target + +set JAVA_OPTS=-Xms256m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m + +java -jar %JAVA_OPTS% ruoyi-admin.jar + +cd bin +pause \ No newline at end of file diff --git a/doc/2022年11月17日逻辑改动.md b/doc/2022年11月17日逻辑改动.md new file mode 100644 index 000000000..1038e47d7 --- /dev/null +++ b/doc/2022年11月17日逻辑改动.md @@ -0,0 +1,11 @@ +充电桩 + +1. 充电桩桩类型全部都是运营桩 +2. 充电桩必须绑定计费模板才能使用 +3. 充电桩桩对白名单用户免费 +4. 创建订单时,判断是否为白名单用户 + +站点白名单 + + + diff --git a/doc/接口文档.md b/doc/接口文档.md new file mode 100644 index 000000000..b2fc1e20b --- /dev/null +++ b/doc/接口文档.md @@ -0,0 +1,134 @@ +# 根据充电桩id查询详情接口 + +>根据充电桩id查询相关信息,包括运营商信息、充电站信息、充电枪信息、协议信息、sim卡信息等。 + +入参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| ------ | ------ | -------- | -------- | +| pileId | String | Y | 充电桩id | + +反参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| ----------------- | ------ | -------- | ---------------------------- | +| sn | String | Y | sn号 | +| stationName | String | Y | 站点名称 station | +| merchantName | String | Y | 运营商名称 merchant | +| pileType | String | Y | 设备类型(1-汽车桩,2-电单车) model | +| gunNum | int | Y | 枪数量 model | +| interfaceStandard | String | Y | 接口标准 model | +| ratedPower | int | Y | 额定功率(单位W) model | +| outputCurrent | int | Y | 输出电流(单位A) model | +| ICCID | String | Y | sim卡信息 | +| registrationTime | String | Y | 注册时间 licence | +| expireTime | String | Y | 到期时间 licence | + +![image-20220924155754954](C:\Users\李苗苗\AppData\Roaming\Typora\typora-user-images\image-20220924155754954.png) + +## 站点管理列表 + +#### 反参 + +PileStationVO + +| 字段名 | 类型 | 是否必传 | 备注 | +| ----------------- | ------- | -------- | :----------: | +| id | String | | 站点id | +| stationName | string | | 站点名称 | +| areaCode | S | | 省市辖区编码 | +| area | S | | 地区 | +| address | S | | 地址 | +| PileNum | Integer | | 充电设备数量 | +| merchantId | S | | 运营商ID | +| merchantName | S | | 运营商名称 | +| merchantAdminName | S | | 运营商管理员 | +| stationStatus | Integer | | 站点状态 | +| stationType | s | | 站点类型 | +| createTime | s | | 创建时间 | +| stationTel | s | | 站点电话 | +| matchCars | s | | 适用车型描述 | +| stationLng | s | | 经度 | +| stationLat | s | | 纬度 | +| construction | s | | 建设场所 | +| businessHours | s | | 营业时间描述 | +| organizationCode | s | | 组织结构代码 | +| publicFlag | s | | 是否对外开放 | +| openFlag | s | | 是否营业中 | + +## BatchCreatePileDTO + +| 字段名 | 类型 | | 备注 | +| ---------------- | ---- | ---- | ---------------------------- | +| merchantId | s | | 运营商id | +| stationId | s | | 充电站id | +| modelId | s | | 型号id | +| softwareProtocol | s | | 软件协议(1-云快充;2-永联) | +| productionDate | Date | | 生成日期 | +| connectorNum | int | | 接口数量 | +| num | int | | 生成台数 | +| remark | s | | 备注 | + +### 计费模板时段详情 + +``` +CreateBillingTemplateDTO +``` + +| 字段名 | 类型 | | 备注 | +| ----------------- | -------------------------------------- | ---- | ---------------------------------------- | +| name | s | | 模板名称 | +| type | s | | 时段类型 | +| electricityPriceA | BigDecimal | | 尖时段电费 | +| servicePriceA | BigDecimal | | 尖时段服务费 | +| electricityPriceB | BigDecimal | | 峰时段电费 | +| servicePriceB | | | 峰时段服务费 | +| electricityPriceC | | | 平时段电费 | +| servicePriceC | | | 平时段服务费 | +| electricityPriceD | | | 谷时段电费 | +| servicePriceD | | | 谷时段服务费 | +| remark | s | | 备注 | +| 时段清单 | private List timeList; | | | +| type | String | | 时段类型(1-尖时;2-峰时;3-平时;4-谷时 | +| timeDesc | s | | 时段 例如:00:00-05:00 | + +### 快速建站DTO FastCreateStationDTO + +| 字段名 | | | 备注 | +| ----------- | ---- | ---- | ------------ | +| merchantId | S | | 所属运营商id | +| stationName | S | | 名称 | +| address | S | | 地址 | +| areaCode | S | | 区域 | + +### 站点导入计费模板dto ImportBillingTemplateDTO + +| 字段名 | 类型 | | 备注 | +| ----------------- | ---- | ---- | ---------- | +| stationId | | | 站点id | +| billingTemplateId | | | 计费模板id | + +### 查询充电枪返回前台参数 + +PileConnectorInfoVO + +| 字段名 | 类型 | | 备注 | +| ------------------ | ---- | ---- | --------------------------- | +| connectorId | S | | 充电枪口id | +| connectorCode | S | | 枪口编号,由充电桩SN+01生成 | +| connectorQrCodeUrl | S | | 充电二维码 | +| status | int | | 状态 | +| type | S | | 类型 | +| instantPower | B | | 即时功率 | +| electricity | big | | 电量 | +| SOC | S | | SOC | +| plantformOrderNum | S | | 平台订单 | +| equipmentOrderNum | S | | 设备订单号 | +| chargingTime | S | | 充电时长 | +| voltage | B | | 电压 | +| current | B | | 电流 | +| temperature | B | | 温度 | +| userInfo | S | | 用户信息 | +| orderId | S | | 订单id | +| carNo | S | | 车牌号 | + diff --git a/doc/接口文档New.md b/doc/接口文档New.md new file mode 100644 index 000000000..0c6d2f560 --- /dev/null +++ b/doc/接口文档New.md @@ -0,0 +1,621 @@ +# 小程序接口 + +## 接口返回格式 + +| 字段名 | 类型 | 是否必传 | 备注 | +| ------- | ------ | -------- | ---------- | +| resCode | String | Y | 返回码 | +| msg | String | Y | 返回信息 | +| obj | Object | Y | 返回的数据 | + +以下接口反参,指的是obj中的数据,接口返回都有resCode,msg,obj这三个字段。 + +### 示例 + +~~~json +# 入参 +{ + "pageSize": "10", + "pageNum": "1", + "stationLng": "55.96", + "stationLat": "155.77" +} + +# 反参 +{ + "resCode": "00100000", + "msg": "操作成功", + "obj": { + "pageNum": 1, + "pageSize": 10, + "list": [ + { + "stationId": "2", + "stationName": "测试仓库", + "stationAddress": "华新镇华隆路1777号6幢D座", + "distance": "10745.74", + "electricityPrice": null, + "servicePrice": null, + "fastTotal": 0, + "fastFree": 0, + "slowTotal": 3, + "slowFree": 0 + }, + { + "stationId": "1", + "stationName": "测试", + "stationAddress": "黄埔江南路278号举视新能源", + "distance": "10762.94", + "electricityPrice": null, + "servicePrice": null, + "fastTotal": 0, + "fastFree": 0, + "slowTotal": 0, + "slowFree": 0 + } + ], + "total": 2, + "pages": 1 + } +} + +# 错误反参 +{ + "resCode": "00100010", + "msg": "查询充电站信息列表异常", + "obj": null +} +~~~ + + + +## 1001 登录注册接口 + +## 1002 查询会员信息 + +> 接口地址:http://localhost:8080/uniapp/member/getMemberInfo +> +> 请求方式:GET + +### 入参 + +null,在Header中需传Authorization + +### 反参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| -------- | ------ | -------- | ------------ | +| MemberVO | Object | Y | 用户信息对象 | + +### MemberVO + +| 字段名 | 类型 | 是否必传 | 备注 | +| ------------------ | ------ | -------- | --------------------------- | +| memberId | String | Y | 会员Id | +| status | String | Y | 状态(1-正常;0-停用) | +| nickName | String | Y | 用户昵称 | +| mobileNumber | String | Y | 手机号码 | +| principalPrice | Number | Y | 本金金额 | +| giftPrice | Number | Y | 赠送金额 | +| totalAccountAmount | Number | Y | 总金额(本金金额 + 赠送金额) | + + + +## 1003 修改会员信息 + + + +## 2001 根据经纬度查询充电站列表(分页排序) + +> 接口地址:http://localhost:8080/uniapp/pile/queryStationInfos +> +> 请求类型:POST + +### 入参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| ----------- | ------ | -------- | ---------------- | +| stationLng | String | N | 经度 | +| stationLat | String | N | 纬度 | +| pageNum | Number | Y | 页码 | +| pageSize | Number | Y | 每页数量 | +| stationName | String | y | 站点名称(搜索) | + +### 反参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| -------- | ---------------- | -------- | ---------- | +| pageNum | Number | Y | 页码 | +| pageSize | Number | Y | 每页数量 | +| list | Array | Y | 充电站列表 | +| total | Number | Y | 总数 | +| pages | Number | Y | 总页数 | + +StationVO + +| 字段名 | 类型 | 是否必传 | 备注 | +| ---------------- | ------------- | -------- | --------------------- | +| stationId | String | Y | 站点id | +| stationName | String | Y | 站点名称 | +| stationAddress | String | Y | 站点地址 | +| stationImgList | Array | N | 站点图片 | +| distance | String | N | 距离 单位千米 | +| electricityPrice | String | Y | 电费 每度单价 | +| servicePrice | String | Y | 服务费 每度单价 | +| totalPrice | String | Y | 总金额(电费+服务费) | +| fastTotal | Number | Y | 快充枪口总数 | +| fastFree | Number | Y | 快充枪口空闲数 | +| slowTotal | Number | Y | 慢充枪口总数 | +| slowFree | Number | Y | 慢充枪口空闲数 | +| stationLng | String | Y | 经度 | +| stationLat | String | Y | 纬度 | + +## 3001 查询充电桩详情 + +## 3002 查询充电桩枪口详情 +> 接口地址:http://localhost:8080/uniapp/pile/selectConnectorListByParams +> +> 请求类型:POST + +### 入参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| ----------------- | ------------- | -------- | --------------------------- | +| pageNum | Number | Y | 页码 | +| pageSize | Number | Y | 每页数量 | +| merchantId | String | N | 运营商id ==接口暂未支持== | +| stationIdList | Array | N | 站点id列表 | +| pileIds | Array | N | 充电桩id列表 | +| connectorIdList | Array | N | 枪口id列表 | +| connectorCodeList | Array | N | 枪口号列表 | + +### 反参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| -------- | -------------------------- | -------- | ------------ | +| pageNum | Number | Y | 页码 | +| pageSize | Number | Y | 每页数量 | +| list | Array | Y | 充电枪口对象 | +| total | Number | Y | 总数 | +| pages | Number | Y | 总页数 | + +### PileConnectorInfoVO + +| 字段名 | 类型 | 是否必传 | 备注 | +| ------------------ | ---------- | :------: | ------------------------------------------------------------ | +| connectorId | String | Y | 充电枪口id | +| connectorCode | String | Y | 枪口编号 | +| connectorQrCodeUrl | String | Y | 枪口二维码 | +| status | Number | Y | 状态 0:离网 (默认);1:空闲;2:占用(未充电);3:占用(充电中);4:占用(预约锁定) ;255:故障 | +| stationId | String | Y | 站点id | +| merchantId | String | Y | 运营商id | +| merchantName | String | Y | 运营商名称 | +| pileSn | String | Y | 充电桩编号 | +| type | String | Y | 类型 1-直流接口 汽车桩+快充 2-交流接口 汽车桩+慢充 3-插座接口 电单车桩 | +| instantPower | BigDecimal | Y | 即时功率 | +| electricity | BigDecimal | Y | 电量 | +| equipmentOrderNum | String | Y | 设备订单号 | +| platformOrderNum | String | Y | 平台订单 | +| chargingTime | String | Y | 充电时长 | +| voltage | BigDecimal | Y | 电压 | +| current | BigDecimal | Y | 电流 | +| gunLineTemperature | String | Y | 枪线温度 | +| userInfo | String | Y | 用户信息 | +| orderId | String | Y | 订单id | +| carNo | String | Y | 车牌号 | +| soc | String | Y | SOC | +| chargingAmount | BigDecimal | Y | 充电金额 | +| chargingDegree | BigDecimal | Y | 充电度数 | +| businessType | | y | 经营类型(1-运营桩;2-个人桩) | + +### 示例: + +```json +#入参: +{ + "pageNum": 1, + "pageSize":10, + "connectorIdList":[1] +} + +#反参: +{ + "resCode": "00100000", + "msg": "操作成功", + "obj": { + "pageNum": 1, + "pageSize": 10, + "list": [ + { + "connectorId": "1", + "connectorCode": "8800000000000101", + "connectorQrCodeUrl": "http://localhost/pileConnectorInfo&code=8800000000000101", + "status": 0, + "stationId": "2", + "merchantId": "5", + "merchantName": "举视(上海)新能源科技有限公司", + "pileSn": "88000000000001", + "type": "2", + "instantPower": 0.00, + "electricity": null, + "equipmentOrderNum": null, + "platformOrderNum": null, + "chargingTime": null, + "voltage": 0.0, + "current": 0.0, + "gunLineTemperature": "0", + "userInfo": null, + "orderId": null, + "carNo": null, + "chargingAmount": 0.00, + "chargingDegree": 0.00, + "soc": "0" + } + ], + "total": 1, + "pages": 1 + } +} +``` + +## 4001 启动充电 + +> 接口地址:http://localhost:8080/uniapp/order/generateOrder +> +> 请求方式:POST + +### 入参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| ----------------- | ------ | -------- | --------------------------- | +| pileSn | String | Y | 桩编码 | +| connectorCode | String | Y | 枪口号 | +| pileConnectorCode | String | Y | 桩枪口编号(桩编码+枪口号) | +| memToken | String | Y | 用户token(写在Header中) | + +备注:pileSn + connectorCode 或 pileConnectorCode 选其一,接口都支持 + +### 反参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| --------- | ------ | -------- | ------ | +| orderCode | String | Y | 订单号 | + + + +## 4002 结束充电 + +> 接口地址:http://localhost:8080/uniapp/order/settleOrder +> +> 请求方式:POST + +### 入参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| ----------------- | ------ | -------- | --------------------------- | +| orderCode | String | Y | 订单号 | +| pileSn | String | Y | 桩编码 | +| connectorCode | String | Y | 枪口号 | +| pileConnectorCode | String | Y | 桩枪口编号(桩编码+枪口号) | + +### 反参 + +null,若成功,msg中会有“==操作成功==”提示 + + + +## 5001 查询订单列表 + +> 请求地址:http://localhost:8080/uniapp/order/getOrderList +> +> 请求方式:POST + +### 入参 +| 字段名 | 类型 | 是否必传 | 备注 | +| ----------------- | ------ | -------- | --------------------------- | +| memberId | String | Y | 会员id | +| pageSize | Number | Y | | +| pageNum | Number | Y | | +| orderStatus | String | Y | 订单状态 1-全部 2-未完成 3-已完成 | +### 反参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| ----------------- | ------ | -------- | --------------------------- | +| orderCode | String | Y | 订单号 | +| pileSn | String | Y | 桩编码 | +| connectorCode | String | Y | 枪口号 | +| stationName | String | Y | 站点名称 | +| pileConnectorCode | String | Y | 桩枪口编号(桩编码+枪口号) | +| chargingDegree | BigDecimal | Y | 充电度数| +| orderAmount | BigDecimal | Y | 订单金额 | +| orderStatus | String | Y | 订单状态(0-待支付;1-充电中;2-待结算;3-待补缴;4-异常;5-可疑;6-订单完成) | +| startTime | String | Y | 订单开始时间 | +| endTime | String | Y | 订单结束时间 | +| payAmount | BigDecimal | Y | 用户支付金额 | +| payStatus | String | Y | 支付状态(0-待支付;1-支付完成) | +| reason | String | Y | 订单异常原因 | + +## 60001 查询余额明细 + +> 请求地址: http://localhost:8080/uniapp/member/getMemberBalanceChanges +> +> 请求方式:POST + + + +### 入参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| ----------- | ------ | -------- | ------------------------ | +| memberToken | String | Y | 会员令牌 | +| type | String | Y | 交易类型 1-进账;2-出账 | +| pageSize | | | | +| pageNum | | | | + +### 反参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| ------------------ | ------ | -------- | -------------------------- | +| memberId | String | Y | 会员Id | +| principalBalance | Number | Y | 当前账户本金余额 | +| giftBalance | Number | Y | 当前账户赠送余额 | +| totalAccountAmount | Number | Y | 账户总余额 | +| type | String | Y | 交易类型 1-进账;2-出账 | +| subType | String | Y | 子类型 | +| amount | String | Y | 出账/入账金额 | +| transactionTime | String | Y | 交易时间 | +| category | String | Y | 余额类型(1-本金,2-赠送) | + + + + + +## 7001 生成订单 + +> 请求地址:http://localhost:8080/uniapp/order/generateOrder +> +> 请求方式:POST + +### 入参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| ----------------- | ------ | -------- | ---------- | +| pileConnectorCode | String | Y | 桩枪口编码 | +| chargeAmount | Number | Y | 充电金额 | + +### 反参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| --------- | ------ | -------- | ------ | +| orderCode | String | Y | 订单号 | + + + +## 7002 支付订单 + +> 请求地址:http://localhost:8080/uniapp/pay/payOrder +> +> 请求方式:POST + +### 入参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| --------- | ------ | -------- | -------- | +| orderCode | String | Y | 订单号 | +| payMode | String | Y | 支付方式 | +| payAmount | String | Y | 支付金额 | + +### 反参 + +null,提示”==操作成功==“ + +## 7003 订单停止充电 + +> 请求地址:http://localhost:8080/uniapp/order/stopCharging +> +> 请求方式:POST + +### 入参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| --------- | ------ | -------- | ------ | +| orderCode | String | Y | 订单号 | + +### 反参 + +null,提示”==操作成功==“ + + + + + + + +# 首页大数据展示 + +## 8001 概况 + +> 请求地址:http://localhost:8080/index/getGeneralSituation +> +> 请求方式:POST + +### 入参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| --------- | ------ | -------- | ------ | +| stationId | String | N | 站点id | + +### 反参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| --------------------- | ------ | -------- | ---------- | +| totalChargingDegree | String | Y | 总充电电量 | +| totalChargingAmount | String | Y | 总充电费用 | +| totalChargingQuantity | String | Y | 总充电笔数 | +| totalPileQuantity | String | Y | 总设备数量 | +| totalMemberAmount | String | Y | 总客户余额 | + + + +## 8002 订单 + +> 请求地址:http://localhost:8080/index/getOrderInfo +> +> 请求方式:POST + +### 入参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| --------- | ------ | -------- | ------ | +| stationId | String | N | 站点id | + +### 反参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| -------------------------- | ------ | -------- | -------------- | +| date | String | Y | 日期 | +| totalElectricity | String | Y | 总用电量 | +| totalOrderAmount | String | Y | 总订单金额 | +| totalSharpUsedElectricity | String | Y | 尖时段总用电量 | +| totalPeakUsedElectricity | String | Y | 峰时段总用电量 | +| totalFlatUsedElectricity | String | Y | 平时段总用电量 | +| totalValleyUsedElectricity | String | Y | 谷时段总用电量 | + + + +# 个人桩相关 + +## 9001 用户绑定个人桩 + +> 请求地址: http://localhost:8080/uniapp/personalPile/pileMemberBinding +> +> 请求方式: POST + +### 入参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| ---------------- | ------ | -------- | -------------- | +| pileSn | String | Y | 桩编码 | +| phoneNumber | String | Y | 用户手机号码 | +| verificationCode | String | Y | 用户手机验证码 | + +### 反参 + + + + + +## 9002 桩管理员下发给其他用户 + +> 请求地址:http://localhost:8080/uniapp/personalPile/adminIssuePile +> +> 请求方式: POST + +### 入参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| ----------- | ------ | -------- | -------------------- | +| PileSn | String | Y | 桩编码 | +| phoneNumber | String | Y | 另一个用户的手机号码 | + + + +## 9003 获取个人桩列表 + +> 请求地址: http://localhost:8080/uniapp/personalPile/getPersonalPileList +> +> 请求方式: GET + +### 反参 + +### List + +| 字段名 | 类型 | 是否必传 | 备注 | +| ------------ | ------ | -------- | ---------------------------- | +| pileSn | String | Y | 桩编码 | +| connectorNum | String | Y | 枪口数量==(2023.02.23新增)== | +| memberId | String | Y | 会员id | +| type | String | Y | 身份类型 | +| modelName | String | Y | 型号 | +| ratedPower | String | Y | 额定功率 | +| ratedCurrent | String | Y | 额定电流 | +| ratedVoltage | String | Y | 额定电压 | +| speedType | String | Y | 充电类型 | + + + + + +## 9004 获取枪口实时数据 + +> 请求地址:http://localhost:8080/uniapp/personalPile/getConnectorRealTimeInfo +> +> 请求方式:POST + +### 入参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| ----------------- | ------ | -------- | -------- | +| pileConnectorCode | String | Y | 桩枪口号 | + +### 反参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| -------------- | ------ | -------- | -------- | +| instantCurrent | Number | Y | 实时电流 | +| instantVoltage | Number | Y | 实时电压 | +| instantPower | Number | Y | 实时功率 | + + + +## 9005 累积充电量数据 + +> 请求地址:http://localhost:8080/uniapp/personalPile/getAccumulativeInfo +> +> 请求方式:POST + +### 入参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| ----------------- | ------ | -------- | -------- | +| pileConnectorCode | String | Y | 桩枪口号 | +| startTime | String | Y | 开始日期 | +| endTime | String | Y | 结束日期 | + +### 反参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| ---------------------- | ------ | -------- | ------------ | +| memberId | String | Y | 会员id | +| startTime | String | Y | 开始日期 | +| endTime | String | Y | 结束日期 | +| sumChargingElectricity | String | Y | 累计充电量 | +| sumChargingTime | String | Y | 累计充电时长 | + + + +## 9006 充电记录 + +请求地址:http://localhost:8080/uniapp/personalPile/getChargingRecord + +请求方式:POST + +### 入参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| ----------------- | ------ | -------- | -------- | +| pileConnectorCode | String | Y | 桩枪口号 | +| pageNum | Number | Y | 页码 | +| pageSize | Number | Y | 每页数量 | + + + +### 反参 + +| 字段名 | 类型 | 是否必传 | 备注 | +| ------------------- | ------ | -------- | -------- | +| startChargingTime | String | Y | 启动时间 | +| endChargingTime | String | Y | 结束时间 | +| chargingElectricity | String | Y | 用电量 | +| chargingTime | String | Y | 充电时长 | + diff --git a/doc/测试问题.md b/doc/测试问题.md new file mode 100644 index 000000000..af624781e --- /dev/null +++ b/doc/测试问题.md @@ -0,0 +1,5 @@ +# 测试问题 + +1、充电桩型号表 pile_model_info ==speed_type== 和==charger_pile_type==字段,在前台新增桩型号时未设置值 + +2、用户正常开始充电后,桩由于意外情况清除订单记录,此时后台该订单一直为充电中状态,用户的开始扣款金额无法退回 \ No newline at end of file diff --git a/jsowell-admin/pom.xml b/jsowell-admin/pom.xml new file mode 100644 index 000000000..b0ebfe083 --- /dev/null +++ b/jsowell-admin/pom.xml @@ -0,0 +1,127 @@ + + + 4.0.0 + + com.jsowell + jsowell-charger-web + 1.0.0 + + jar + jsowell-admin + + + web服务入口 + + + + + + + org.springframework.boot + spring-boot-devtools + true + + + + + io.springfox + springfox-boot-starter + + + + + io.swagger + swagger-models + 1.6.2 + + + + + mysql + mysql-connector-java + + + + + com.jsowell + jsowell-framework + + + + + com.jsowell + jsowell-quartz + + + + + com.jsowell + jsowell-generator + + + + com.jsowell + jsowell-pile + 1.0.0 + + + + com.jsowell + jsowell-netty + 1.0.0 + + + + org.springframework.boot + spring-boot-starter-test + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + 2.1.1.RELEASE + + true + + + + + repackage + + + + + + org.apache.maven.plugins + maven-war-plugin + 3.1.0 + + false + ${project.artifactId} + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.8 + 1.8 + + /src/test/** + + utf-8 + + + + ${project.artifactId} + + + \ No newline at end of file diff --git a/jsowell-admin/src/main/java/com/jsowell/JsowellApplication.java b/jsowell-admin/src/main/java/com/jsowell/JsowellApplication.java new file mode 100644 index 000000000..5acc5cce4 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/JsowellApplication.java @@ -0,0 +1,21 @@ +package com.jsowell; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; + +/** + * 启动程序 + * + * @author jsowell + */ + +@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) +public class JsowellApplication { + + public static void main(String[] args) { + // System.setProperty("spring.devtools.restart.enabled", "false"); + SpringApplication.run(JsowellApplication.class, args); + } + +} diff --git a/jsowell-admin/src/main/java/com/jsowell/JsowellServletInitializer.java b/jsowell-admin/src/main/java/com/jsowell/JsowellServletInitializer.java new file mode 100644 index 000000000..71aa4b195 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/JsowellServletInitializer.java @@ -0,0 +1,16 @@ +package com.jsowell; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +/** + * web容器中进行部署 + * + * @author jsowell + */ +public class JsowellServletInitializer extends SpringBootServletInitializer { + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(JsowellApplication.class); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/api/uniapp/JumpController.java b/jsowell-admin/src/main/java/com/jsowell/api/uniapp/JumpController.java new file mode 100644 index 000000000..6aadef921 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/api/uniapp/JumpController.java @@ -0,0 +1,70 @@ +package com.jsowell.api.uniapp; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.annotation.Anonymous; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.enums.ykc.ReturnCodeEnum; +import com.jsowell.common.exception.BusinessException; +import com.jsowell.common.response.RestApiResponse; +import com.jsowell.pile.vo.uniapp.PileConnectorVO; +import com.jsowell.service.PileService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; + +@Anonymous +@RestController +@RequestMapping("/app-xcx-h5") +public class JumpController extends BaseController { + + @Autowired + private PileService pileService; + + /** + * 查询充电桩详情 + * http://localhost:8080/app-xcx-h5/pile/pileDetail/{pileSn} + */ + @GetMapping("/pile/pileDetail/{pileSn}") + public RestApiResponse getPileDetail(HttpServletRequest request, @PathVariable("pileSn") String pileSn) { + logger.info("app-xcx-h5查询充电桩详情 param:{}", pileSn); + RestApiResponse response = null; + try { + PileConnectorVO vo = pileService.getPileDetailByPileSn(pileSn); + response = new RestApiResponse<>(vo); + } catch (BusinessException e) { + logger.warn("app-xcx-h5查询充电桩详情 warn", e); + response = new RestApiResponse<>(e.getCode(), e.getMessage()); + } catch (Exception e) { + logger.error("app-xcx-h5查询充电桩详情 error", e); + response = new RestApiResponse<>(ReturnCodeEnum.CODE_GET_PILE_DETAIL_ERROR); + } + logger.info("app-xcx-h5查询充电桩详情 result:{}", JSONObject.toJSONString(response)); + return response; + } + + /** + * 查询充电枪口详情 + * http://localhost:8080/app-xcx-h5/pile/connectorDetail/{pileConnectorCode} + */ + @GetMapping("/pile/connectorDetail/{pileConnectorCode}") + public RestApiResponse getConnectorDetail(HttpServletRequest request, @PathVariable("pileConnectorCode") String pileConnectorCode) { + logger.info("app-xcx-h5查询充电枪口详情 param:{}", pileConnectorCode); + RestApiResponse response = null; + try { + PileConnectorVO vo = pileService.getConnectorDetail(pileConnectorCode); + response = new RestApiResponse<>(vo); + } catch (BusinessException e) { + logger.warn("app-xcx-h5查询充电枪口详情 warn", e); + response = new RestApiResponse<>(e.getCode(), e.getMessage()); + } catch (Exception e) { + logger.error("app-xcx-h5查询充电枪口详情 error", e); + response = new RestApiResponse<>(ReturnCodeEnum.CODE_GET_PILE_DETAIL_ERROR); + } + logger.info("app-xcx-h5查询充电枪口详情 result:{}", JSONObject.toJSONString(response)); + return response; + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/api/uniapp/MemberController.java b/jsowell-admin/src/main/java/com/jsowell/api/uniapp/MemberController.java new file mode 100644 index 000000000..26cc700ab --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/api/uniapp/MemberController.java @@ -0,0 +1,199 @@ +package com.jsowell.api.uniapp; + +import com.alibaba.fastjson2.JSONObject; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.jsowell.common.annotation.Anonymous; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.page.PageResponse; +import com.jsowell.common.enums.ykc.ReturnCodeEnum; +import com.jsowell.common.exception.BusinessException; +import com.jsowell.common.exception.ServiceException; +import com.jsowell.common.response.RestApiResponse; +import com.jsowell.common.util.SMSUtil; +import com.jsowell.common.util.StringUtils; +import com.jsowell.pile.dto.MemberRegisterAndLoginDTO; +import com.jsowell.pile.dto.MemberRegisterDTO; +import com.jsowell.pile.dto.UniAppQueryMemberBalanceDTO; +import com.jsowell.pile.dto.WechatLoginDTO; +import com.jsowell.pile.dto.WeixinPayDTO; +import com.jsowell.pile.vo.uniapp.MemberVO; +import com.jsowell.service.MemberService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * 小程序接口 + */ +// 不登录直接访问 +@Anonymous +@RestController +@RequestMapping("/uniapp/member") +public class MemberController extends BaseController { + + @Autowired + private MemberService memberService; + + /** + * 下发短信接口 business + * http://localhost:8080/uniapp/member/sendSMS + */ + @PostMapping("/sendSMS") + public RestApiResponse sendSMS(HttpServletRequest request, @RequestBody MemberRegisterAndLoginDTO dto) { + logger.info("下发短信接口 param:{}", JSONObject.toJSONString(dto)); + RestApiResponse response = null; + try { + if (StringUtils.isBlank(dto.getMobileNumber())) { + throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR); + } + String sendSMSResult = SMSUtil.sendSMS(request, dto.getMobileNumber()); + response = new RestApiResponse<>(sendSMSResult); + } catch (Exception e) { + logger.error("下发短信接口 发生异常 error", e); + response = new RestApiResponse<>(ReturnCodeEnum.CODE_SEND_SMS_ERROR); + } + logger.info("下发短信接口 result:{}", JSONObject.toJSONString(response)); + return response; + } + + + /** + * 会员登录注册 + * 登录成功,返回memberToken + * http://localhost:8080/uniapp/member/memberRegisterAndLogin + */ + @PostMapping("/memberRegisterAndLogin") + public RestApiResponse memberRegisterAndLogin(HttpServletRequest request, @RequestBody MemberRegisterAndLoginDTO dto) { + logger.info("会员登录注册接口 param:{}", JSONObject.toJSONString(dto)); + RestApiResponse response = null; + try { + // 执行登录(查这个手机号在后台有没有数据,如果没有就静默注册) + String memberToken = memberService.memberRegisterAndLogin(dto); + + // 返回前端成功 + Map map = Maps.newHashMap(); + map.put("memberToken", memberToken); + response = new RestApiResponse<>(map); + } catch (ServiceException e) { + logger.warn("会员登录注册接口 warn", e); + response = new RestApiResponse<>(e.getMessage()); + } catch (Exception e) { + logger.error("会员登录注册接口 发生异常 error", e); + response = new RestApiResponse<>(ReturnCodeEnum.CODE_MEMBER_REGISTER_AND_LOGIN_ERROR); + } + return response; + } + + /** + * 微信一键登录 + * http://localhost:8080/uniapp/member/wechatLogin + */ + @PostMapping("/wechatLogin") + public RestApiResponse wechatLogin(@RequestBody WechatLoginDTO dto) { + RestApiResponse response = null; + try { + String memberToken = memberService.wechatLogin(dto); + response = new RestApiResponse<>(ImmutableMap.of("memberToken", memberToken)); + } catch (Exception e) { + logger.error("微信登录异常", e); + response = new RestApiResponse<>(ReturnCodeEnum.CODE_WECHAT_LOGIN_ERROR); + } + return response; + } + + /** + * 接收并处理前端用户信息 + * + * http://localhost:8080/uniapp/member/saveUserInfo + */ + @PostMapping("/saveUserInfo") + public RestApiResponse saveUserInfo(@RequestBody MemberRegisterDTO dto) { + logger.info("接受前端用户信息并处理 param:{}", JSONObject.toJSONString(dto)); + RestApiResponse response = null; + try { + memberService.handleUserInfo(dto); + response = new RestApiResponse<>(); + } catch (Exception e) { + logger.error("处理用户信息异常", e); + response = new RestApiResponse<>(ReturnCodeEnum.CODE_HANDLE_USER_INFO_ERROR); + } + logger.info("接受前端用户信息并处理 result:{}", response); + return response; + } + + /** + * 查询用户账户信息 + * + * http://localhost:8080/uniapp/member/getMemberInfo + * @return 用户账户信息 + */ + @GetMapping("/getMemberInfo") + public RestApiResponse getMemberInfo(HttpServletRequest request) { + RestApiResponse response = null; + try { + String memberId = getMemberIdByAuthorization(request); + logger.info("查询账户总余额 param memberId:{}", memberId); + MemberVO memberVO = memberService.getMemberInfoByMemberId(memberId); + response = new RestApiResponse<>(memberVO); + }catch (BusinessException e) { + response = new RestApiResponse<>(e.getCode(), e.getMessage()); + }catch (Exception e) { + logger.error("查询用户账户总余额异常", e); + response = new RestApiResponse<>(ReturnCodeEnum.CODE_GET_MEMBER_ACCOUNT_AMOUNT_ERROR); + } + logger.info("查询用户账户信息 result:{}", response); + return response; + } + + /** + * 获取openId + * http://localhost:8080/uniapp/member/getOpenId + */ + @PostMapping("/getOpenId") + public RestApiResponse getOpenId(HttpServletRequest request, @RequestBody WeixinPayDTO dto) { + logger.info("获取openId param:{}", dto.toString()); + RestApiResponse response; + try { + getMemberIdByAuthorization(request); + String openId = memberService.getOpenIdByCode(dto.getCode()); + response = new RestApiResponse<>(ImmutableMap.of("openId", openId)); + } catch (Exception e) { + logger.error("获取openId error", e); + response = new RestApiResponse<>(ReturnCodeEnum.CODE_GET_OPEN_ID_BY_CODE_ERROR); + } + logger.info("获取openId result:{}", response); + return response; + } + + + /** + * 获取用户账户余额变动信息 + * http://localhost:8080/uniapp/member/getMemberBalanceChanges + * @param request + * @param dto + * @return + */ + @PostMapping("/getMemberBalanceChanges") + public RestApiResponse getMemberBalanceChanges(HttpServletRequest request, @RequestBody UniAppQueryMemberBalanceDTO dto) { + logger.info("查询用户账户余额变动信息 params:{}", dto.toString() ); + RestApiResponse response = null; + try { + String memberId = getMemberIdByAuthorization(request); + dto.setMemberId(memberId); + PageResponse pageResponse = memberService.getMemberBalanceChanges(dto); + response = new RestApiResponse<>(pageResponse); + } catch (Exception e) { + logger.error("查询用户账户余额变动信息 error:", e); + response = new RestApiResponse<>(ReturnCodeEnum.CODE_GET_BALANCE_CHANGES_ERROR); + } + logger.info("查询用户账户余额变动信息 result:{}", response); + return response; + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/api/uniapp/OrderController.java b/jsowell-admin/src/main/java/com/jsowell/api/uniapp/OrderController.java new file mode 100644 index 000000000..9eb231df5 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/api/uniapp/OrderController.java @@ -0,0 +1,244 @@ +package com.jsowell.api.uniapp; + +import com.alibaba.fastjson2.JSONObject; +import com.google.common.collect.ImmutableMap; +import com.jsowell.common.annotation.Anonymous; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.page.PageResponse; +import com.jsowell.common.enums.ykc.ReturnCodeEnum; +import com.jsowell.common.exception.BusinessException; +import com.jsowell.common.response.RestApiResponse; +import com.jsowell.common.util.StringUtils; +import com.jsowell.pile.dto.GenerateOrderDTO; +import com.jsowell.pile.dto.QueryOrderDTO; +import com.jsowell.pile.dto.SettleOrderDTO; +import com.jsowell.pile.dto.StopChargingDTO; +import com.jsowell.pile.dto.UniAppQueryOrderDTO; +import com.jsowell.pile.vo.uniapp.UniAppOrderVO; +import com.jsowell.service.OrderService; +import com.jsowell.service.PileRemoteService; +import com.jsowell.wxpay.dto.WechatSendMsgDTO; +import com.jsowell.wxpay.service.WxAppletRemoteService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * 订单相关接口 + * 提供给小程序 + */ +@Anonymous +@RestController +@RequestMapping("/uniapp/order") +public class OrderController extends BaseController { + + @Autowired + private OrderService orderService; + + @Autowired + private PileRemoteService pileRemoteService; + + @Autowired + private WxAppletRemoteService wxAppletRemoteService; + + /** + * 生成订单 + * http://localhost:8080/uniapp/order/generateOrder + */ + @PostMapping("/generateOrder") + public RestApiResponse generateOrder(HttpServletRequest request, @RequestBody GenerateOrderDTO dto) { + logger.info("生成订单 param:{}", JSONObject.toJSONString(dto)); + RestApiResponse response; + try { + if ((StringUtils.isBlank(dto.getPileSn()) || StringUtils.isBlank(dto.getConnectorCode())) && StringUtils.isBlank(dto.getPileConnectorCode())) { + throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR); + } + String memberId = getMemberIdByAuthorization(request); + if (StringUtils.isEmpty(memberId)) { + throw new BusinessException(ReturnCodeEnum.CODE_TOKEN_ERROR); + } + dto.setMemberId(memberId); + // 生成订单 + dto.setStartMode(Constants.ONE); // 启动方式 1-app启动 + String orderCode = orderService.generateOrder(dto); + response = new RestApiResponse<>(ImmutableMap.of("orderCode", orderCode)); + } catch (BusinessException e) { + logger.warn("生成订单 warn", e); + response = new RestApiResponse<>(e.getCode(), e.getMessage()); + } catch (Exception e) { + logger.error("生成订单 error", e); + response = new RestApiResponse<>(ReturnCodeEnum.CODE_GENERATE_ORDER_ERROR); + } + logger.info("生成订单 result:{}", JSONObject.toJSONString(response)); + return response; + } + + /** + * 停止充电 + * http://localhost:8080/uniapp/order/stopCharging + */ + @PostMapping ("/stopCharging") + public RestApiResponse stopCharging(HttpServletRequest request, @RequestBody StopChargingDTO dto) { + logger.info("停止充电 param:{}", JSONObject.toJSONString(dto)); + RestApiResponse response; + try { + String memberId = getMemberIdByAuthorization(request); + if (StringUtils.isEmpty(memberId)) { + throw new BusinessException(ReturnCodeEnum.CODE_TOKEN_ERROR); + } + dto.setMemberId(memberId); + orderService.stopCharging(dto); + response = new RestApiResponse<>(); + } catch (BusinessException e) { + logger.warn("停止充电 warn", e); + response = new RestApiResponse<>(e.getCode(), e.getMessage()); + } catch (Exception e) { + logger.error("停止充电 error", e); + response = new RestApiResponse<>(ReturnCodeEnum.CODE_STOP_CHARGING_ERROR); + } + return response; + } + + /** + * 结算订单 + * http://localhost:8080/uniapp/order/settleOrder + * @param dto 结算订单DTO + * @return + */ + @PostMapping("/settleOrderForWeb") + public RestApiResponse settleOrder(HttpServletRequest request, @RequestBody SettleOrderDTO dto) { + logger.info("结算订单 param:{}", JSONObject.toJSONString(dto)); + RestApiResponse response; + try { + orderService.settleOrderForWeb(dto); + response = new RestApiResponse<>(); + } catch (Exception e) { + logger.error("结算订单错误", e); + response = new RestApiResponse<>(ReturnCodeEnum.CODE_SETTLE_ORDER_ERROR); + } + return response; + } + + /** + * 查询订单信息 + * http://localhost:8080/uniapp/order/getOrderList + * @return + */ + @PostMapping("/getOrderList") + public RestApiResponse getOrderInfo(HttpServletRequest request, @RequestBody UniAppQueryOrderDTO dto) { + logger.info("查询订单信息 param:{}", JSONObject.toJSONString(dto)); + RestApiResponse response = null; + try { + String memberId = getMemberIdByAuthorization(request); + if (StringUtils.isBlank(memberId)) { + throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR); + } + // 通过memberId查询某状态订单列表 + PageResponse pageInfoList = orderService.getListByMemberIdAndOrderStatus(memberId, dto); + response = new RestApiResponse<>(pageInfoList); + } catch (BusinessException e) { + logger.warn("查询订单信息 warn", e); + response = new RestApiResponse<>(e.getCode(), e.getMessage()); + } catch (Exception e) { + logger.error("查询订单信息 error", e); + response = new RestApiResponse<>(ReturnCodeEnum.CODE_GET_ORDER_INFO_BY_MEMBER_ID_ERROR); + } + logger.info("查询订单信息, result:{}", JSONObject.toJSONString(response)); + return response; + } + + /** + * 小程序获取订单详情 + * http://localhost:8080/uniapp/order/getOrderDetail + * @param request + * @param dto + * @return + */ + @PostMapping("/getOrderDetail") + public RestApiResponse getOrderDetail(HttpServletRequest request, @RequestBody UniAppQueryOrderDTO dto) { + logger.info("小程序获取订单详情 param:{}", JSONObject.toJSONString(dto)); + RestApiResponse response = null; + try { + String memberId = getMemberIdByAuthorization(request); + if (StringUtils.isBlank(memberId)) { + throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR); + } + UniAppOrderVO uniAppOrderDetail = orderService.getUniAppOrderDetail(dto.getOrderCode()); + response = new RestApiResponse<>(uniAppOrderDetail); + } catch (BusinessException e) { + logger.warn("小程序获取订单详情 warn", e); + response = new RestApiResponse<>(e.getCode(), e.getMessage()); + } catch (Exception e) { + logger.error("小程序获取订单详情 error", e); + response = new RestApiResponse<>(ReturnCodeEnum.CODE_GET_ORDER_DETAIL_ERROR); + } + logger.info("小程序获取订单详情, result:{}", JSONObject.toJSONString(response)); + return response; + } + + /** + * 根据订单号查询充电桩启动状态 + * http://localhost:8080/uniapp/order/selectPileStarterStatus + */ + @PostMapping("/selectPileStarterStatus") + public RestApiResponse selectPileStarterStatus(HttpServletRequest request, @RequestBody UniAppQueryOrderDTO dto) { + logger.info("根据订单号查询充电桩启动状态 param:{}", JSONObject.toJSONString(dto)); + RestApiResponse response = null; + try { + // String memberId = getMemberIdByAuthorization(request); + String status = orderService.selectPileStarterStatus(dto.getOrderCode()); + response = new RestApiResponse<>(ImmutableMap.of("status", status)); + } catch (Exception e) { + logger.error("根据订单号查询充电桩启动状态 error", e); + response = new RestApiResponse<>(ReturnCodeEnum.CODE_GET_ORDER_DETAIL_ERROR); + } + logger.info("根据订单号查询充电桩启动状态 result:{}", JSONObject.toJSONString(response)); + return response; + } + + /** + * 微信小程序发送启动充电推送消息 + * http://localhost:8080/uniapp/order/uniAppStartChargingSendMsg + * @param dto + * @return + */ + @PostMapping("/uniAppStartChargingSendMsg") + public RestApiResponse uniAppStartChargingSendMsg(@RequestBody WechatSendMsgDTO dto) { + logger.info("微信小程序发送启动充电推送消息 param:{}", JSONObject.toJSONString(dto)); + RestApiResponse response = null; + try { + Map resultMap = wxAppletRemoteService.startChargingSendMsg(dto); + response = new RestApiResponse<>(resultMap); + } catch (Exception e){ + logger.error("微信小程序发送启动充电推送消息 error", e); + response = new RestApiResponse<>("00300001", "微信小程序发送启动充电推送消息异常"); + } + logger.info("微信小程序发送启动充电推送消息 result:{}", response); + return response; + } + + /** + * 关闭支付未启动的订单 + * http://localhost:8080/uniapp/order/closeStartFailedOrder + */ + @PostMapping("/closeStartFailedOrder") + public RestApiResponse closeStartFailedOrder(@RequestBody QueryOrderDTO dto) { + logger.info("关闭支付未启动的订单 param:{}", JSONObject.toJSONString(dto)); + RestApiResponse response = null; + try { + orderService.closeStartFailedOrder(dto); + response = new RestApiResponse<>(); + } catch (Exception e){ + logger.error("关闭支付未启动的订单 error", e); + response = new RestApiResponse<>("00300002", "关闭支付未启动的订单异常"); + } + logger.info("关闭支付未启动的订单 result:{}", response); + return response; + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/api/uniapp/PayController.java b/jsowell-admin/src/main/java/com/jsowell/api/uniapp/PayController.java new file mode 100644 index 000000000..ca656e7d6 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/api/uniapp/PayController.java @@ -0,0 +1,204 @@ +package com.jsowell.api.uniapp; + +import com.alibaba.fastjson2.JSONObject; +import com.google.common.collect.ImmutableMap; +import com.jsowell.common.annotation.Anonymous; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.redis.RedisCache; +import com.jsowell.common.enums.ykc.ReturnCodeEnum; +import com.jsowell.common.enums.ykc.ScenarioEnum; +import com.jsowell.common.exception.BusinessException; +import com.jsowell.common.response.RestApiResponse; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.id.IdUtils; +import com.jsowell.pile.dto.PayOrderDTO; +import com.jsowell.pile.dto.PaymentScenarioDTO; +import com.jsowell.pile.dto.WeixinPayDTO; +import com.jsowell.service.MemberService; +import com.jsowell.service.OrderService; +import com.jsowell.wxpay.dto.WeChatRefundDTO; +import com.jsowell.wxpay.response.WechatPayNotifyParameter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * 支付相关controller + */ +@Anonymous +@RestController +@RequestMapping("/uniapp/pay") +public class PayController extends BaseController { + @Autowired + private MemberService memberService; + + @Autowired + private OrderService orderService; + + @Autowired + private RedisCache redisCache; + + /** + * 充值余额支付 + * 提供给小程序使用 + * http://localhost:8080/uniapp/pay/weixinPay + */ + @PostMapping("/weixinPay") + public RestApiResponse weixinPay(HttpServletRequest request, @RequestBody WeixinPayDTO dto) { + logger.info("微信支付 param:{}", dto.toString()); + RestApiResponse response; + try { + if (StringUtils.isBlank(dto.getCode()) || StringUtils.isBlank(dto.getAmount())) { + return new RestApiResponse<>(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR); + } + // 鉴权 + String memberId = getMemberIdByAuthorization(request); + if (StringUtils.isBlank(memberId)) { + throw new BusinessException(ReturnCodeEnum.CODE_TOKEN_ERROR); + } + dto.setMemberId(memberId); + String openId = memberService.getOpenIdByCode(dto.getCode()); + if (StringUtils.isBlank(openId)) { + throw new BusinessException(ReturnCodeEnum.CODE_GET_OPEN_ID_BY_CODE_ERROR); + } + dto.setOpenId(openId); + // 充值余额 附加参数 + PaymentScenarioDTO paymentScenarioDTO = new PaymentScenarioDTO(); + paymentScenarioDTO.setType(ScenarioEnum.BALANCE.getValue()); + paymentScenarioDTO.setMemberId(memberId); + dto.setAttach(JSONObject.toJSONString(paymentScenarioDTO)); + dto.setDescription("会员充值余额"); + Map weixinMap = orderService.weixinPayV3(dto); + response = new RestApiResponse<>(ImmutableMap.of("weixinMap", weixinMap)); + } catch (Exception e) { + response = new RestApiResponse<>(); + } + return response; + } + + /** + * 支付订单 + * http://localhost:8080/uniapp/pay/payOrder + * + * @param request + * @param dto + * @return + */ + @PostMapping("/payOrder") + public RestApiResponse payOrder(HttpServletRequest request, @RequestBody PayOrderDTO dto) { + logger.info("支付订单 param:{}", dto.toString()); + RestApiResponse response; + + // 支付订单加锁 + String lockKey = "pay_order_" + dto.getOrderCode(); + String lockValue = IdUtils.fastUUID(); + try { + String memberId = getMemberIdByAuthorization(request); + if (StringUtils.isBlank(memberId)) { + throw new BusinessException(ReturnCodeEnum.CODE_TOKEN_ERROR); + } + dto.setMemberId(memberId); + dto.setLockValue(lockValue); + // redis锁 + Boolean isLock = redisCache.lock(lockKey, lockValue, 60); + Map map = null; + if (isLock) { + map = orderService.payOrder(dto); + } + // Map map = orderService.payOrder(dto); + response = new RestApiResponse<>(map); + } catch (BusinessException e) { + logger.warn("支付订单 warn", e); + response = new RestApiResponse<>(e.getCode(), e.getMessage()); + } catch (Exception e) { + logger.error("支付订单 error", e); + response = new RestApiResponse<>(ReturnCodeEnum.CODE_ORDER_PAY_ERROR); + } finally { + // 支付订单解锁 + if (lockValue.equals(redisCache.getCacheObject(lockKey).toString())) { + redisCache.unLock(lockKey); + } + } + logger.info("支付订单 result:{}", JSONObject.toJSONString(response)); + return response; + } + + /** + * 微信支付回调接口 + * http://localhost:8080/uniapp/pay/callback + * https://api.jsowellcloud.com/uniapp/pay/wechatPayCallback + */ + @PostMapping("/wechatPayCallback") + public RestApiResponse wechatPayCallback(HttpServletRequest request, @RequestBody WechatPayNotifyParameter body) { + logger.info("1----------->微信支付回调开始 body:{}", JSONObject.toJSONString(body)); + RestApiResponse response; + try { + orderService.wechatPayCallback(request, body); + response = new RestApiResponse<>(); + } catch (BusinessException e) { + logger.warn("微信支付回调接口warn", e); + response = new RestApiResponse<>(e.getCode(), e.getMessage()); + } catch (Exception e) { + logger.error("微信支付回调接口error", e); + response = new RestApiResponse<>(ReturnCodeEnum.CODE_ORDER_PAY_CALLBACK_ERROR); + } + return response; + } + + /** + * 微信退款回调接口 + * @param request + * @param body + * @return + */ + @PostMapping("/wechatPayRefundCallback") + public RestApiResponse wechatPayRefundCallback(HttpServletRequest request, @RequestBody WechatPayNotifyParameter body) { + logger.info("微信退款回调接口 body:{}", JSONObject.toJSONString(body)); + RestApiResponse response; + try { + orderService.wechatPayRefundCallback(request, body); + response = new RestApiResponse<>(); + } catch (BusinessException e) { + logger.warn("微信退款回调接口warn", e); + response = new RestApiResponse<>(e.getCode(), e.getMessage()); + } catch (Exception e) { + logger.error("微信退款回调接口error", e); + response = new RestApiResponse<>(ReturnCodeEnum.CODE_ORDER_PAY_CALLBACK_ERROR); + } + return response; + } + + /** + * 微信退款接口 + * https://api.jsowellcloud.com/uniapp/pay/refund + */ + @PostMapping("/refund") + public RestApiResponse weChatRefund(HttpServletRequest request, @RequestBody WeChatRefundDTO dto) { + RestApiResponse response; + try { + if (dto.getRefundAmount() == null) { + throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR); + } + String memberId = getMemberIdByAuthorization(request); + if (StringUtils.isBlank(memberId)) { + throw new BusinessException(ReturnCodeEnum.CODE_TOKEN_ERROR); + } + dto.setMemberId(memberId); + dto.setRefundType("2"); + orderService.weChatRefund(dto); + response = new RestApiResponse<>(); + } catch (BusinessException e) { + logger.warn("微信退款接口 warn", e); + response = new RestApiResponse<>(e.getCode(), e.getMessage()); + } catch (Exception e) { + logger.error("微信退款接口 error", e); + response = new RestApiResponse<>(ReturnCodeEnum.CODE_WEIXIN_REFUND_ERROR); + } + return response; + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/api/uniapp/PersonPileController.java b/jsowell-admin/src/main/java/com/jsowell/api/uniapp/PersonPileController.java new file mode 100644 index 000000000..ccc1f7d18 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/api/uniapp/PersonPileController.java @@ -0,0 +1,213 @@ +package com.jsowell.api.uniapp; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.annotation.Anonymous; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.page.PageResponse; +import com.jsowell.common.enums.ykc.ReturnCodeEnum; +import com.jsowell.common.exception.BusinessException; +import com.jsowell.common.response.RestApiResponse; +import com.jsowell.pile.dto.PileMemberBindingDTO; +import com.jsowell.pile.dto.QueryPersonPileDTO; +import com.jsowell.pile.service.IPileBasicInfoService; +import com.jsowell.pile.service.IPileMemberRelationService; +import com.jsowell.pile.vo.uniapp.OrderVO; +import com.jsowell.pile.vo.uniapp.PersonPileConnectorSumInfoVO; +import com.jsowell.pile.vo.uniapp.PersonPileRealTimeVO; +import com.jsowell.pile.vo.uniapp.PersonalPileInfoVO; +import com.jsowell.service.PileService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +/** + * 个人桩controller + * + * @author JS-ZZA + * @date 2023/2/23 15:36 + */ +@Anonymous +@RestController +@RequestMapping("/uniapp/personalPile") +public class PersonPileController extends BaseController { + + @Autowired + private IPileMemberRelationService pileMemberRelationService; + + @Autowired + private PileService pileService; + + @Autowired + private IPileBasicInfoService pileBasicInfoService; + + /** + * 用户绑定个人桩 + * + * http://localhost:8080/uniapp/personalPile/pileMemberBinding + * + * @param dto + * @return + */ + @RequestMapping("/pileMemberBinding") + public RestApiResponse pileMemberBinding(HttpServletRequest request, @RequestBody PileMemberBindingDTO dto){ + logger.info("绑定个人桩信息 params:{}", JSONObject.toJSONString(dto)); + RestApiResponse response = null; + try { + String memberId = getMemberIdByAuthorization(request); + dto.setMemberId(memberId); + int i = pileService.pileMemberBinding(dto); + response = new RestApiResponse<>(i); + }catch (BusinessException e){ + logger.error("绑定个人桩信息 error,", e); + response = new RestApiResponse<>(e.getCode(), e.getMessage()); + }catch (Exception exception){ + logger.error("绑定个人桩信息 error,", exception); + response = new RestApiResponse<>(ReturnCodeEnum.CODE_BINDING_PERSONAL_PILE_ERROR); + } + logger.info("绑定个人桩信息 result:{}", response); + return response; + } + + /** + * 个人桩管理员下发给其他用户 + * + * http://localhost:8080/uniapp/personalPile/adminIssuePile + * + * @param request + * @param dto + * @return + */ + @RequestMapping("/adminIssuePile") + public RestApiResponse adminIssuePile(HttpServletRequest request, @RequestBody PileMemberBindingDTO dto){ + logger.info("桩管理员下发个人桩 params: {}", JSONObject.toJSONString(dto)); + RestApiResponse response = null; + try { + String memberId = getMemberIdByAuthorization(request); + dto.setMemberId(memberId); + pileService.adminIssuePile(dto); + response = new RestApiResponse<>(); + }catch (BusinessException e) { + logger.error("桩管理员下发个人桩 error", e); + response = new RestApiResponse<>(e.getCode(), e.getMessage()); + }catch (Exception e){ + logger.error("桩管理员下发个人桩 error", e); + response = new RestApiResponse<>(ReturnCodeEnum.CODE_ADMIN_ISSUE_ERROR); + } + logger.info("桩管理员下发个人桩 result: {}", response); + return response; + } + + + /** + * 通过memberId查个人桩列表 + * + * http://localhost:8080/uniapp/personalPile/getPersonalPileList + * + * @return + */ + @GetMapping("/getPersonalPileList") + public RestApiResponse getPersonalPileList(HttpServletRequest request){ + RestApiResponse response = null; + try { + String memberId = getMemberIdByAuthorization(request); + logger.info("通过memberId查个人桩列表 params: {}", memberId); + List list = pileBasicInfoService.getPileInfoByMemberId(memberId); + response = new RestApiResponse<>(list); + }catch (Exception e){ + logger.error("通过memberId查个人桩列表异常", e); + response = new RestApiResponse<>(ReturnCodeEnum.CODE_GET_PERSONAL_PILE_BY_MEMBER_ID_ERROR); + } + logger.info("通过memberId查个人桩列表 result:{}", response); + return response; + } + + + /** + * 获取枪口实时数据 + * + * http://localhost:8080/uniapp/personalPile/getConnectorRealTimeInfo + * + * @param request + * @param dto + * @return + */ + @PostMapping("/getConnectorRealTimeInfo") + public RestApiResponse getConnectorRealTimeInfo(HttpServletRequest request, @RequestBody QueryPersonPileDTO dto){ + logger.info("获取个人桩枪口实时数据 params:{}", JSONObject.toJSONString(dto)); + RestApiResponse response = null; + try { + String memberId = getMemberIdByAuthorization(request); + dto.setMemberId(memberId); + PersonPileRealTimeVO connectorRealTimeInfo = pileService.getConnectorRealTimeInfo(dto); + response = new RestApiResponse<>(connectorRealTimeInfo); + }catch (BusinessException e){ + logger.error("获取个人桩枪口实时数据 error", e); + response = new RestApiResponse<>(e.getCode(), e.getMessage()); + }catch (Exception e){ + logger.error("获取个人桩枪口实时数据 error", e); + response = new RestApiResponse<>(ReturnCodeEnum.CODE_GET_PERSONAL_PILE_CONNECTOR_INFO_ERROR); + } + logger.info("获取个人桩枪口实时数据 result:{}", response); + return response; + } + + + /** + * 获取某枪口某段时间内累计信息 + * + * http://localhost:8080/uniapp/personalPile/getAccumulativeInfo + * + * @param dto + * @return + */ + @RequestMapping("/getAccumulativeInfo") + public RestApiResponse getAccumulativeInfo(HttpServletRequest request, @RequestBody QueryPersonPileDTO dto) { + logger.info("获取某枪口某段时间内累计信息 params:{}", JSONObject.toJSONString(dto)); + RestApiResponse response = null; + try { + String memberId = getMemberIdByAuthorization(request); + dto.setMemberId(memberId); + PersonPileConnectorSumInfoVO accumulativeInfo = pileService.getAccumulativeInfo(dto); + response = new RestApiResponse<>(accumulativeInfo); + } catch (BusinessException e){ + logger.error("获取某枪口某段时间内累计信息 error", e); + response = new RestApiResponse<>(e.getCode(), e.getMessage()); + }catch (Exception e){ + logger.error("获取某枪口某段时间内累计信息 error", e); + response = new RestApiResponse<>(e); + } + logger.info("获取某枪口某段时间内累计信息 result:{}", response); + return response; + } + + /** + * 获取充电记录 + * + * http://localhost:8080/uniapp/personalPile/getChargingRecord + * + * @param request + * @param dto + * @return + */ + @RequestMapping("/getChargingRecord") + public RestApiResponse getChargingRecord(HttpServletRequest request, @RequestBody QueryPersonPileDTO dto){ + logger.info("获取个人桩充电记录 params:{}", JSONObject.toJSONString(dto)); + RestApiResponse response = null; + try { + String memberId = getMemberIdByAuthorization(request); + dto.setMemberId(memberId); + PageResponse chargingRecord = pileService.getChargingRecord(dto); + response = new RestApiResponse<>(chargingRecord); + }catch (BusinessException e){ + logger.error("获取个人桩充电记录 error", e); + response = new RestApiResponse<>(e.getCode(), e.getMessage()); + }catch (Exception e){ + logger.error("获取个人桩充电记录 error", e); + response = new RestApiResponse<>(e); + } + logger.info("获取个人桩充电记录 result:{}", response); + return response; + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/api/uniapp/PileController.java b/jsowell-admin/src/main/java/com/jsowell/api/uniapp/PileController.java new file mode 100644 index 000000000..7e854bf4e --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/api/uniapp/PileController.java @@ -0,0 +1,91 @@ +package com.jsowell.api.uniapp; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.annotation.Anonymous; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.page.PageResponse; +import com.jsowell.common.enums.ykc.ReturnCodeEnum; +import com.jsowell.common.exception.BusinessException; +import com.jsowell.common.response.RestApiResponse; +import com.jsowell.pile.dto.QueryConnectorListDTO; +import com.jsowell.pile.dto.QueryStationDTO; +import com.jsowell.pile.service.IPileConnectorInfoService; +import com.jsowell.pile.service.IPileStationInfoService; +import com.jsowell.pile.vo.uniapp.PersonalPileInfoVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; + +/** + * 充电桩相关接口 + * 提供给小程序调用 + */ +// 不登录直接访问 +@Anonymous +@RestController +@RequestMapping("/uniapp/pile") +public class PileController extends BaseController { + + + + @Autowired + private IPileStationInfoService pileStationInfoService; + + @Autowired + private IPileConnectorInfoService pileConnectorInfoService; + + + + /** + * 查询充电站信息列表(主页) + * + * http://localhost:8080/uniapp/pile/queryStationInfos + */ + @PostMapping("/queryStationInfos") + public RestApiResponse queryStationInfos(HttpServletRequest request, @RequestBody QueryStationDTO queryStationDTO) { + logger.info("查询充电站信息列表 param:{}", JSONObject.toJSONString(queryStationDTO)); + RestApiResponse response = null; + try { + getMemberIdByAuthorization(request); + // startPage(); + // List list = pileStationInfoService.uniAppQueryStationInfos(queryStationDTO); + PageResponse pageResponse = pileStationInfoService.uniAppQueryStationInfoList(queryStationDTO); + response = new RestApiResponse<>(pageResponse); + } catch (BusinessException e) { + logger.warn("查询充电站信息列表warn", e); + response = new RestApiResponse<>(e.getCode(), e.getMessage()); + } catch (Exception e) { + logger.error("查询充电站信息列表异常 error", e); + response = new RestApiResponse<>(ReturnCodeEnum.CODE_GET_PILE_STATION_INFO_ERROR); + } + logger.info("查询充电站信息列表 result:{}", JSONObject.toJSONString(response)); + return response; + } + + /** + * 通过前端参数查询充电枪口列表 + * + * http://localhost:8080/uniapp/pile/selectConnectorListByParams + * + * @param dto + * @return + */ + @PostMapping("/selectConnectorListByParams") + public RestApiResponse selectConnectorListByParams(HttpServletRequest request, @RequestBody QueryConnectorListDTO dto) { + logger.info("查询充电枪口列表 params:{}", JSONObject.toJSONString(dto)); + RestApiResponse response = null; + try { + PageResponse pageResponse = pileConnectorInfoService.getUniAppConnectorInfoListByParams(dto); + response = new RestApiResponse<>(pageResponse); + } catch (Exception e) { + logger.error("查询充电枪口列表异常", e); + response = new RestApiResponse<>(ReturnCodeEnum.CODE_GET_CONNECTOR_INFO_BY_STATION_ID_ERROR); + } + logger.info("查询充电枪口列表 result:{}", response); + return response; + } + + + +} diff --git a/jsowell-admin/src/main/java/com/jsowell/service/MemberService.java b/jsowell-admin/src/main/java/com/jsowell/service/MemberService.java new file mode 100644 index 000000000..b2c0e3996 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/service/MemberService.java @@ -0,0 +1,234 @@ +package com.jsowell.service; + +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.jsowell.common.constant.CacheConstants; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.page.PageResponse; +import com.jsowell.common.core.redis.RedisCache; +import com.jsowell.common.enums.uniapp.BalanceChangesEnum; +import com.jsowell.common.enums.ykc.ReturnCodeEnum; +import com.jsowell.common.exception.BusinessException; +import com.jsowell.common.util.JWTUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.id.IdUtils; +import com.jsowell.pile.domain.MemberBasicInfo; +import com.jsowell.pile.domain.MemberWalletInfo; +import com.jsowell.pile.dto.MemberRegisterAndLoginDTO; +import com.jsowell.pile.dto.MemberRegisterDTO; +import com.jsowell.pile.dto.UniAppQueryMemberBalanceDTO; +import com.jsowell.pile.dto.WechatLoginDTO; +import com.jsowell.pile.service.IMemberBasicInfoService; +import com.jsowell.pile.service.IPileMerchantInfoService; +import com.jsowell.pile.transaction.dto.MemberTransactionDTO; +import com.jsowell.pile.transaction.service.TransactionService; +import com.jsowell.pile.vo.uniapp.MemberVO; +import com.jsowell.pile.vo.uniapp.MemberWalletLogVO; +import com.jsowell.wxpay.service.WxAppletRemoteService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Objects; + +@Service +public class MemberService { + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + @Autowired + private RedisCache redisCache; + + @Autowired + private TransactionService transactionService; + + @Autowired + private IMemberBasicInfoService memberBasicInfoService; + + @Autowired + private IPileMerchantInfoService pileMerchantInfoService; + + @Autowired + private WxAppletRemoteService wxAppletRemoteService; + + /** + * 校验短信验证码 + * @param dto + */ + public void checkVerificationCode(MemberRegisterAndLoginDTO dto) { + // 取出缓存中的验证码进行对比,如果缓存中没有,则超时 + String captchaCode = redisCache.getCacheObject(CacheConstants.SMS_VERIFICATION_CODE_KEY + dto.getMobileNumber()); + if (StringUtils.isEmpty(captchaCode)) { + throw new BusinessException(ReturnCodeEnum.CODE_VERIFICATION_CODE_TIMEOUT_ERROR); + } + // 如果缓存中有,但与实际不一致,则为验证码错误 + if (!StringUtils.equals(captchaCode, dto.getVerificationCode())) { + throw new BusinessException(ReturnCodeEnum.CODE_VERIFICATION_CODE_ERROR); + } + } + + /** + * 短信验证码登录注册 + * @param dto + * @return + */ + public String memberRegisterAndLogin(MemberRegisterAndLoginDTO dto) { + // 校验短信验证码 两种情况不能通过校验,1-验证码错误;2-超时 验证码10分钟有效 + checkVerificationCode(dto); + String merchantId = ""; + return memberRegisterAndLogin(dto.getMobileNumber(), merchantId, null); + } + + /** + * 公共登陆注册方法 + * @param phoneNumber 手机号 + * @param merchantId 商户id + * @return + */ + private String memberRegisterAndLogin(String phoneNumber, String merchantId, String openId) { + if (StringUtils.isBlank(phoneNumber)) { + throw new BusinessException(ReturnCodeEnum.CODE_GET_MOBILE_NUMBER_BY_CODE_ERROR); + } + // if (Objects.isNull(merchantId)) { + // throw new BusinessException(ReturnCodeEnum.CODE_GET_MERCHANT_ID_BY_APP_ID_ERROR); + // } + // 查询手机号码是否注册过 + MemberBasicInfo memberBasicInfo = memberBasicInfoService.selectInfoByMobileNumberAndMerchantId(phoneNumber, merchantId); + if (Objects.isNull(memberBasicInfo)) { + // 不存在则新增数据 + String memberId = IdUtils.getMemberId(); + memberBasicInfo = new MemberBasicInfo(); + memberBasicInfo.setStatus(Constants.ONE); + memberBasicInfo.setMemberId(memberId); + memberBasicInfo.setNickName("会员" + memberId); + memberBasicInfo.setMobileNumber(phoneNumber); + memberBasicInfo.setMerchantId(Long.valueOf(merchantId)); + memberBasicInfo.setOpenId(openId); + + // 首次新建会员,同时新建会员钱包 + MemberWalletInfo memberWalletInfo = MemberWalletInfo.builder().memberId(memberId).build(); + MemberTransactionDTO memberTransactionDTO = MemberTransactionDTO.builder() + .memberBasicInfo(memberBasicInfo) + .memberWalletInfo(memberWalletInfo) + .build(); + transactionService.createMember(memberTransactionDTO); + } else { + if (StringUtils.isBlank(memberBasicInfo.getOpenId()) && StringUtils.isNotBlank(openId)) { + memberBasicInfo.setOpenId(openId); + memberBasicInfoService.updateMemberBasicInfo(memberBasicInfo); + } + } + // 服务器生成token返给前端 + String memberToken = JWTUtils.createMemberToken(memberBasicInfo.getMemberId(), memberBasicInfo.getNickName()); + // log.info("memToken:{}", memberToken); + return memberToken; + } + + /** + * 微信一键登录 + * @param dto + */ + public String wechatLogin(WechatLoginDTO dto) { + // 通过微信传的code获取手机号码 + String mobileNumber = wxAppletRemoteService.getMobileNumberByCode(dto.getCode()); + if (StringUtils.isBlank(mobileNumber)) { + throw new BusinessException(ReturnCodeEnum.CODE_GET_MOBILE_NUMBER_BY_CODE_ERROR); + } + // 通过appid获取运营商id + String merchantId = pileMerchantInfoService.getMerchantIdByAppId(dto.getAppId()); + // if (Objects.isNull(merchantId)) { + // throw new BusinessException(ReturnCodeEnum.CODE_GET_MERCHANT_ID_BY_APP_ID_ERROR); + // } + // 通过code获取openId + String openId = ""; + try { + openId = getOpenIdByCode(dto.getOpenIdCode()); + } catch (Exception e) { + log.error("getOpenIdByCode发生异常", e); + } + // 查询手机号码是否注册过 + return memberRegisterAndLogin(mobileNumber, merchantId, openId); + } + + /** + * 获取openId + * @param code + * @return + */ + public String getOpenIdByCode(String code) { + return wxAppletRemoteService.getOpenIdByCode(code); + } + + /** + * 处理用户信息 + * + * @param dto 用户个人信息 + */ + public void handleUserInfo(MemberRegisterDTO dto) { + // 通过用户手机号查询数据库,如果数据库中存在,则更新 + MemberBasicInfo memberBasicInfo = memberBasicInfoService.selectInfoByMobileNumber(dto.getMobileNumber()); + if (Objects.nonNull(memberBasicInfo)) { + MemberBasicInfo memberInfo = MemberBasicInfo.builder() + .avatarUrl(dto.getAvatarUrl()) + .mobileNumber(dto.getMobileNumber()) + .nickName(dto.getNickName()) + .build(); + memberBasicInfoService.updateMemberBasicInfo(memberInfo); + } + + } + + /** + * 通过memberToken获取用户账户信息 + * + * @param memberId + * @return + */ + public MemberVO getMemberInfoByMemberId(String memberId) { + MemberVO memberVO = memberBasicInfoService.queryMemberInfoByMemberId(memberId); + if (Objects.nonNull(memberVO)) { + memberVO.setTotalAccountAmount(memberVO.getPrincipalBalance().add(memberVO.getGiftBalance())); + } + return memberVO; + } + + /** + * 查询用户账户余额变动信息 + * + * @param dto + */ + public PageResponse getMemberBalanceChanges(UniAppQueryMemberBalanceDTO dto) { + + // 获取分页信息以及memberId + int pageNum = dto.getPageNum() == 0 ? 1 : dto.getPageNum(); + int pageSize = dto.getPageSize() == 0 ? 10 : dto.getPageSize(); + String memberId = dto.getMemberId(); + String type = dto.getType(); + + if (!StringUtils.equals("1", type) && !StringUtils.equals("2", type)) { + type = ""; + } + // 分页 + PageHelper.startPage(pageNum, pageSize); + List list = memberBasicInfoService.getMemberBalanceChanges(memberId, type); + PageInfo pageInfo = new PageInfo<>(list); + + for (MemberWalletLogVO walletLogVO : pageInfo.getList()) { + String subType = walletLogVO.getSubType(); + String subTypeValue = BalanceChangesEnum.getValueByCode(subType); + if (StringUtils.isNotBlank(subTypeValue)) { + walletLogVO.setSubType(subTypeValue); + } + // walletLogVO.setTotalAccountAmount(walletLogVO.getPrincipalBalance().add(walletLogVO.getGiftBalance())); + } + PageResponse pageResponse = PageResponse.builder() + .pageSize(pageSize) + .pageNum(pageNum) + .list(pageInfo.getList()) + .pages(pageInfo.getPages()) + .total(pageInfo.getTotal()) + .build(); + return pageResponse; + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/service/OrderService.java b/jsowell-admin/src/main/java/com/jsowell/service/OrderService.java new file mode 100644 index 000000000..cfb09e2a3 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/service/OrderService.java @@ -0,0 +1,780 @@ +package com.jsowell.service; + +import com.alibaba.fastjson2.JSONObject; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.domain.ykc.RealTimeMonitorData; +import com.jsowell.common.core.page.PageResponse; +import com.jsowell.common.core.redis.RedisCache; +import com.jsowell.common.enums.MemberWalletEnum; +import com.jsowell.common.enums.ykc.ActionTypeEnum; +import com.jsowell.common.enums.ykc.OrderPayModeEnum; +import com.jsowell.common.enums.ykc.OrderPayRecordEnum; +import com.jsowell.common.enums.ykc.OrderPayStatusEnum; +import com.jsowell.common.enums.ykc.OrderStatusEnum; +import com.jsowell.common.enums.ykc.PayModeEnum; +import com.jsowell.common.enums.ykc.ScenarioEnum; +import com.jsowell.common.enums.ykc.PileConnectorDataBaseStatusEnum; +import com.jsowell.common.enums.ykc.ReturnCodeEnum; +import com.jsowell.common.exception.BusinessException; +import com.jsowell.common.util.DateUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.id.IdUtils; +import com.jsowell.netty.command.ykc.StartChargingCommand; +import com.jsowell.netty.service.yunkuaichong.YKCPushCommandService; +import com.jsowell.pile.domain.MemberTransactionRecord; +import com.jsowell.pile.domain.OrderBasicInfo; +import com.jsowell.pile.domain.OrderDetail; +import com.jsowell.pile.domain.OrderPayRecord; +import com.jsowell.pile.domain.WxpayCallbackRecord; +import com.jsowell.pile.dto.BasicPileDTO; +import com.jsowell.pile.dto.GenerateOrderDTO; +import com.jsowell.pile.dto.PayOrderDTO; +import com.jsowell.pile.dto.PayOrderSuccessCallbackDTO; +import com.jsowell.pile.dto.PaymentScenarioDTO; +import com.jsowell.pile.dto.QueryConnectorListDTO; +import com.jsowell.pile.dto.QueryOrderDTO; +import com.jsowell.pile.dto.SettleOrderDTO; +import com.jsowell.pile.dto.StopChargingDTO; +import com.jsowell.pile.dto.UniAppQueryOrderDTO; +import com.jsowell.pile.dto.WeixinPayDTO; +import com.jsowell.pile.service.IMemberBasicInfoService; +import com.jsowell.pile.service.IMemberTransactionRecordService; +import com.jsowell.pile.service.IOrderBasicInfoService; +import com.jsowell.pile.service.IOrderPayRecordService; +import com.jsowell.pile.service.IPileBillingTemplateService; +import com.jsowell.pile.service.IPileConnectorInfoService; +import com.jsowell.pile.service.WechatPayService; +import com.jsowell.pile.service.WxpayCallbackRecordService; +import com.jsowell.pile.transaction.dto.OrderTransactionDTO; +import com.jsowell.pile.transaction.service.TransactionService; +import com.jsowell.pile.vo.base.PileInfoVO; +import com.jsowell.pile.vo.uniapp.MemberVO; +import com.jsowell.pile.vo.uniapp.OrderVO; +import com.jsowell.pile.vo.uniapp.PileConnectorDetailVO; +import com.jsowell.pile.vo.uniapp.UniAppOrderVO; +import com.jsowell.pile.vo.web.BillingTemplateVO; +import com.jsowell.pile.vo.web.OrderDetailInfoVO; +import com.jsowell.pile.vo.web.UpdateMemberBalanceDTO; +import com.jsowell.wxpay.dto.WeChatRefundDTO; +import com.jsowell.wxpay.response.WechatPayNotifyParameter; +import org.apache.commons.collections4.CollectionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.servlet.http.HttpServletRequest; +import java.math.BigDecimal; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + + +@Service +public class OrderService { + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + @Autowired + private TransactionService pileTransactionService; + + @Autowired + private IPileBillingTemplateService pileBillingTemplateService; + + @Autowired + private IOrderBasicInfoService orderBasicInfoService; + + @Autowired + private PileRemoteService pileRemoteService; + + @Autowired + private YKCPushCommandService ykcPushCommandService; + + @Autowired + private PileService pileService; + + @Autowired + private MemberService memberService; + + @Autowired + private IMemberBasicInfoService memberBasicInfoService; + + @Autowired + private IOrderPayRecordService orderPayRecordService; + + @Autowired + private WechatPayService wechatPayService; + + @Autowired + private WxpayCallbackRecordService wxpayCallbackRecordService; + + @Autowired + private IPileConnectorInfoService pileConnectorInfoService; + + @Autowired + private IMemberTransactionRecordService memberTransactionRecordService; + + @Autowired + private RedisCache redisCache; + + /** + * 生成订单 + * + * @param dto + * @return + */ + public String generateOrder(GenerateOrderDTO dto) { + log.info("generateOrder param:{}", JSONObject.toJSONString(dto)); + // 处理前端传的参数 + analysisPileParameter(dto); + + // 校验充电桩相关的信息 + checkPileInfo(dto); + + // 保存订单到数据库 saveOrder2Database + String orderCode = saveOrder2Database(dto); + return orderCode; + } + + /** + * 订单支付 + * + * @param dto + */ + public Map payOrder(PayOrderDTO dto) throws Exception { + Map resultMap = Maps.newHashMap(); + OrderBasicInfo orderInfo = orderBasicInfoService.getOrderInfoByOrderCode(dto.getOrderCode()); + if (orderInfo == null) { + throw new BusinessException(ReturnCodeEnum.CODE_QUERY_ORDER_NULL_ERROR); + } + if (!StringUtils.equals(orderInfo.getPayStatus(), "0")) { + // 订单已支付 + throw new BusinessException(ReturnCodeEnum.CODE_ORDER_IS_NOT_TO_BE_PAID_ERROR); + } + String orderCode = orderInfo.getOrderCode(); + // 该订单充电金额 + BigDecimal chargeAmount = dto.getPayAmount(); + // 记录支付流水 + List payRecordList = Lists.newArrayList(); + if (StringUtils.equals(dto.getPayMode(), OrderPayModeEnum.PAYMENT_OF_BALANCE.getValue())) { // 余额支付 + // 查询该会员的余额 + MemberVO memberVO = memberService.getMemberInfoByMemberId(dto.getMemberId()); + BigDecimal totalAccountAmount = memberVO.getTotalAccountAmount(); + + if (totalAccountAmount.compareTo(chargeAmount) < 0) { + // 总余额小于充电金额 + throw new BusinessException(ReturnCodeEnum.CODE_BALANCE_IS_INSUFFICIENT); + } + BigDecimal principalAmount = memberVO.getPrincipalBalance(); // 会员剩余本金金额 + BigDecimal giftAmount = memberVO.getGiftBalance(); // 会员剩余赠送余额 + + BigDecimal principalPay = null; // 30 + BigDecimal giftPay = null; // 10 + // 先扣除本金金额,再扣除赠送金额 + BigDecimal subtract = principalAmount.subtract(chargeAmount); + if (subtract.compareTo(BigDecimal.ZERO) >= 0) { + principalPay = chargeAmount; + } else { + if (principalAmount.compareTo(BigDecimal.ZERO) > 0) { + principalPay = principalAmount; + } + giftPay = subtract.negate(); + } + + // 更新会员钱包 + UpdateMemberBalanceDTO updateMemberBalanceDTO = UpdateMemberBalanceDTO.builder() + .memberId(dto.getMemberId()) + .type(MemberWalletEnum.TYPE_OUT.getValue()) + .subType(MemberWalletEnum.SUBTYPE_PAYMENT_FOR_ORDER.getValue()) + .updatePrincipalBalance(principalPay) + .updateGiftBalance(giftPay) + .relatedOrderCode(orderCode) + .build(); + memberBasicInfoService.updateMemberBalance(updateMemberBalanceDTO); + + // 记录流水 + if (principalPay != null) { + payRecordList.add(OrderPayRecord.builder() + .orderCode(dto.getOrderCode()) + .payMode(OrderPayRecordEnum.PRINCIPAL_BALANCE_PAYMENT.getValue()) + .payAmount(principalPay) + .createBy(dto.getMemberId()) + .build()); + } + if (giftPay != null) { + payRecordList.add(OrderPayRecord.builder() + .orderCode(dto.getOrderCode()) + .payMode(OrderPayRecordEnum.GIFT_BALANCE_PAYMENT.getValue()) + .payAmount(giftPay) + .createBy(dto.getMemberId()) + .build()); + } + // 余额支付可以直接调支付回调方法 + PayOrderSuccessCallbackDTO callbackDTO = PayOrderSuccessCallbackDTO.builder() + .orderCode(orderCode) + .payAmount(chargeAmount) + .payMode(dto.getPayMode()) + .build(); + payOrderSuccessCallback(callbackDTO); + + // 余额支付订单 记录会员交易流水 + MemberTransactionRecord record = MemberTransactionRecord.builder() + .orderCode(orderCode) + .scenarioType(ScenarioEnum.ORDER.getValue()) + .memberId(memberVO.getMemberId()) + .actionType(ActionTypeEnum.FORWARD.getValue()) + .payMode(PayModeEnum.PAYMENT_OF_BALANCE.getValue()) + .amount(dto.getPayAmount()) // 单位元 + .build(); + memberTransactionRecordService.insertSelective(record); + } else if (StringUtils.equals(dto.getPayMode(), OrderPayModeEnum.PAYMENT_OF_WECHATPAY.getValue())) { // 微信支付 + String openId = memberService.getOpenIdByCode(dto.getCode()); + if (StringUtils.isBlank(openId)) { + throw new BusinessException(ReturnCodeEnum.CODE_GET_OPEN_ID_BY_CODE_ERROR); + } + WeixinPayDTO weixinPayDTO = new WeixinPayDTO(); + weixinPayDTO.setOpenId(openId); + weixinPayDTO.setAmount(dto.getPayAmount().toString()); + // 支付订单 附加参数 + PaymentScenarioDTO paymentScenarioDTO = PaymentScenarioDTO.builder() + .type(ScenarioEnum.ORDER.getValue()) + .orderCode(dto.getOrderCode()) + .memberId(orderInfo.getMemberId()) + .build(); + weixinPayDTO.setAttach(JSONObject.toJSONString(paymentScenarioDTO)); + weixinPayDTO.setDescription("充电费用"); + Map weixinMap = this.weixinPayV3(weixinPayDTO); + // 返回微信支付参数 + resultMap.put("weixinMap", weixinMap); + } else if (StringUtils.equals(dto.getPayMode(), OrderPayModeEnum.PAYMENT_OF_ALIPAY.getValue())) { // 支付宝支付 + // TODO 返回支付宝支付参数 + } + + // 订单支付流水入库 + if (CollectionUtils.isNotEmpty(payRecordList)) { + orderPayRecordService.batchInsert(payRecordList); + } + return resultMap; + } + + /** + * 订单支付成功 支付回调 + * 支付成功后掉用这个方法 + * 1. 修改订单支付状态 + * 2. 发送启动充电指令 + */ + public void payOrderSuccessCallback(PayOrderSuccessCallbackDTO dto) { + OrderBasicInfo orderInfo = orderBasicInfoService.getOrderInfoByOrderCode(dto.getOrderCode()); + BigDecimal payAmount = dto.getPayAmount(); + + // 修改订单 + orderInfo.setPayMode(dto.getPayMode()); + orderInfo.setPayStatus("1"); + orderInfo.setPayAmount(payAmount); + orderInfo.setPayTime(new Date()); + orderBasicInfoService.updateOrderBasicInfo(orderInfo); + + // 发送启动指令 + StartChargingCommand startChargingCommand = StartChargingCommand.builder() + .pileSn(orderInfo.getPileSn()) + .connectorCode(orderInfo.getConnectorCode()) + .orderCode(dto.getOrderCode()) + .chargeAmount(payAmount) + .build(); + ykcPushCommandService.pushStartChargingCommand(startChargingCommand); + } + + /** + * 保存订单信息到数据库 + * + * @param dto + * @return + */ + private String saveOrder2Database(GenerateOrderDTO dto) { + String orderCode = IdUtils.generateOrderCode(dto.getPileSn(), dto.getConnectorCode()); + // 订单基本信息 + OrderBasicInfo orderBasicInfo = OrderBasicInfo.builder() + .orderCode(orderCode) + .orderStatus(OrderStatusEnum.NOT_START.getValue()) + .memberId(dto.getMemberId()) + .stationId(dto.getPileConnector().getStationId()) + .pileSn(dto.getPileSn()) + .connectorCode(dto.getConnectorCode()) + .pileConnectorCode(dto.getPileSn() + dto.getConnectorCode()) + .startMode(dto.getStartMode()) + .payStatus(Constants.ZERO) + .payAmount(dto.getChargeAmount()) + .payMode(dto.getPayMode()) + .orderAmount(BigDecimal.ZERO) + .build(); + + // 订单详情 + OrderDetail orderDetail = OrderDetail.builder() + .orderCode(orderCode) + .sharpElectricityPrice(dto.getBillingTemplate().getSharpElectricityPrice()) + .sharpServicePrice(dto.getBillingTemplate().getSharpServicePrice()) + .peakElectricityPrice(dto.getBillingTemplate().getPeakElectricityPrice()) + .peakServicePrice(dto.getBillingTemplate().getPeakServicePrice()) + .flatElectricityPrice(dto.getBillingTemplate().getFlatElectricityPrice()) + .flatServicePrice(dto.getBillingTemplate().getFlatServicePrice()) + .valleyElectricityPrice(dto.getBillingTemplate().getValleyElectricityPrice()) + .valleyServicePrice(dto.getBillingTemplate().getValleyServicePrice()) + .build(); + + OrderTransactionDTO createOrderTransactionDTO = OrderTransactionDTO.builder() + .orderBasicInfo(orderBasicInfo) + .orderDetail(orderDetail) + .build(); + pileTransactionService.doCreateOrder(createOrderTransactionDTO); + return orderCode; + } + + /** + * 校验充电桩相关的信息 + * + * @param dto + */ + private void checkPileInfo(GenerateOrderDTO dto) { + // 查询充电桩状态 是否空闲 枪口是否占用 + PileConnectorDetailVO pileConnector = pileService.queryPileConnectorDetail(dto.getPileSn() + dto.getConnectorCode()); + if (pileConnector == null) { + log.error("checkPileInfo充电枪口为空 pileSn:{}, connectorCode:{}", dto.getPileSn(), dto.getConnectorCode()); + throw new BusinessException(ReturnCodeEnum.CODE_CONNECTOR_INFO_NULL_ERROR); + } + // 判断枪口状态 + if (!(StringUtils.equals(pileConnector.getConnectorStatus(), PileConnectorDataBaseStatusEnum.FREE.getValue()) + || StringUtils.equals(pileConnector.getConnectorStatus(), PileConnectorDataBaseStatusEnum.OCCUPIED_NOT_CHARGED.getValue()))) { + log.error("checkPileInfo充电枪口状态不正确,当前状态为:{}", pileConnector.getConnectorStatus()); + throw new BusinessException(ReturnCodeEnum.CODE_PILE_CONNECTOR_STATUS_ERROR); + } + // 查询充电桩的计费模板 + BillingTemplateVO billingTemplateVO = pileBillingTemplateService.selectBillingTemplateDetailByPileSn(dto.getPileSn()); + if (billingTemplateVO == null) { + throw new BusinessException(ReturnCodeEnum.CODE_BILLING_TEMPLATE_NULL_ERROR); + } + dto.setPileConnector(pileConnector); + dto.setBillingTemplate(billingTemplateVO); + } + + + /** + * 处理前端传的参数 + * pileConnectorCode = pileSn + connectorCode + * + * @param dto + */ + public void analysisPileParameter(BasicPileDTO dto) { + if (StringUtils.isBlank(dto.getPileSn()) || StringUtils.isBlank(dto.getConnectorCode())) { + // 从pileConnectorCode解析 + String pileConnectorCode = dto.getPileConnectorCode(); + if (StringUtils.isNotEmpty(pileConnectorCode) && pileConnectorCode.length() == Constants.PILE_CONNECTOR_CODE_LENGTH) { + dto.setPileSn(StringUtils.substring(pileConnectorCode, 0, pileConnectorCode.length() - 2)); + dto.setConnectorCode(StringUtils.substring(pileConnectorCode, pileConnectorCode.length() - 2, pileConnectorCode.length())); + } else { + throw new BusinessException(ReturnCodeEnum.CODE_DATA_LENGTH_ERROR); + } + } else { + // 说明pileSn 和 connectorCode前端传了,那就校验一下长度 + if (dto.getPileSn().length() != Constants.PILE_SN_LENGTH || dto.getConnectorCode().length() != Constants.CONNECTOR_CODE_LENGTH) { + throw new BusinessException(ReturnCodeEnum.CODE_DATA_LENGTH_ERROR); + } + } + } + + /** + * 结算订单 + * endCharging + * + * @param dto 结算订单参数 + */ + public void settleOrderForWeb(SettleOrderDTO dto) { + analysisPileParameter(dto); + // 查询订单详情,验证订单中的桩编号是否正确 + OrderBasicInfo orderBasicInfo = orderBasicInfoService.getOrderInfoByOrderCode(dto.getOrderCode()); + if (orderBasicInfo == null) { + throw new BusinessException(ReturnCodeEnum.CODE_QUERY_ORDER_NULL_ERROR); + } + if (!(StringUtils.equals(orderBasicInfo.getPileSn(), dto.getPileSn()) + && StringUtils.equals(orderBasicInfo.getConnectorCode(), dto.getConnectorCode()))) { + throw new BusinessException(ReturnCodeEnum.CODE_ORDER_PILE_MAPPING_ERROR); + } + // push远程停机指令 + pileRemoteService.remoteStopCharging(dto.getPileSn(), dto.getConnectorCode()); + + // 桩计算的消费金额 + + // 对比一下,是否需要退款 + + + } + + /** + * 通过会员Id查询订单列表 + * + * @param memberId 会员Id + * @return 订单信息集合 + */ + public PageResponse getListByMemberIdAndOrderStatus(String memberId, UniAppQueryOrderDTO dto) throws ParseException { + String orderStatus = dto.getOrderStatus(); + if (StringUtils.isBlank(orderStatus)) { + throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR); + } + ArrayList orderStatusList = Lists.newArrayList(); + if (StringUtils.equals("2", orderStatus)) { + // 查未完成订单 + CollectionUtils.addAll(orderStatusList, "0", "1", "2", "3", "4", "5"); + } else if (StringUtils.equals("3", orderStatus)) { + // 查已完成订单 + orderStatusList.add("6"); + } + // 分页 + PageHelper.startPage(dto.getPageNum(), dto.getPageSize()); + List list = orderBasicInfoService.getListByMemberIdAndOrderStatus(memberId, orderStatusList); + + PageInfo pageInfo = new PageInfo<>(list); + + for (OrderVO orderVO : pageInfo.getList()) { + orderVO.setPileConnectorCode(orderVO.getPileSn() + orderVO.getConnectorCode()); + Date endTimeDate; + Date startTimeDate = sdf.parse(orderVO.getStartTime()); + if (StringUtils.isNotBlank(orderVO.getEndTime())) { + endTimeDate = sdf.parse(orderVO.getEndTime()); + } else { + endTimeDate = new Date(); + } + // 计算出两个时间差 + orderVO.setChargingTime(DateUtils.getDatePoor(endTimeDate, startTimeDate)); + } + + // 返回结果集 + PageResponse pageResponse = PageResponse.builder() + .pageNum(dto.getPageNum()) + .pageSize(dto.getPageSize()) + .list(pageInfo.getList()) + .pages(pageInfo.getPages()) + .total(pageInfo.getTotal()) + .build(); + return pageResponse; + } + + /** + * 微信支付v3 + * + * @param dto + * @return + * @throws Exception + */ + public Map weixinPayV3(WeixinPayDTO dto) throws Exception { + return wechatPayService.weixinPayV3(dto); + } + + /** + * 用户停止充电 + * + * @param dto + */ + public void stopCharging(StopChargingDTO dto) { + // 查订单 + OrderBasicInfo orderInfo = orderBasicInfoService.getOrderInfoByOrderCode(dto.getOrderCode()); + if (orderInfo == null) { + throw new BusinessException(ReturnCodeEnum.CODE_QUERY_ORDER_NULL_ERROR); + } + // 校验订单中的会员与操作会员是否一致 + if (!StringUtils.equals(orderInfo.getMemberId(), dto.getMemberId())) { + throw new BusinessException(ReturnCodeEnum.CODE_ORDER_MEMBER_NOT_MATCH_ERROR); + } + // 发送停止指令 + pileRemoteService.remoteStopCharging(orderInfo.getPileSn(), orderInfo.getConnectorCode()); + log.info("订单号:{}发送停机指令成功", dto.getOrderCode()); + } + + /** + * 微信支付回调 + * + * @param request + * @param body + * @throws Exception + */ + public void wechatPayCallback(HttpServletRequest request, WechatPayNotifyParameter body) throws Exception { + // 获取微信支付成功返回的信息 + Map map = wechatPayService.wechatPayCallbackInfo(request, body); + String type = (String) map.get("type"); + BigDecimal amount = (BigDecimal) map.get("amount"); // 微信给的amount单位是分 + amount = amount.divide(new BigDecimal("100"), 2, BigDecimal.ROUND_HALF_UP); // 转换为元 + + String orderCode = (String) map.get("orderCode"); + String memberId = (String) map.get("memberId"); + if (StringUtils.equals(type, ScenarioEnum.ORDER.getValue())) { // 1-订单支付 + // 支付订单成功 + // orderCode = (String) map.get("orderCode"); + PayOrderSuccessCallbackDTO callbackDTO = PayOrderSuccessCallbackDTO.builder() + .orderCode(orderCode) + .payAmount(amount) + .payMode(OrderPayModeEnum.PAYMENT_OF_WECHATPAY.getValue()) + .build(); + // 订单支付成功 支付回调 + payOrderSuccessCallback(callbackDTO); + + // 记录订单支付流水 + OrderPayRecord orderPayRecord = OrderPayRecord.builder() + .orderCode(orderCode) + .payMode(OrderPayRecordEnum.WECHATPAY_PAYMENT.getValue()) + .payAmount(amount) + .createBy(null) + .build(); + orderPayRecordService.batchInsert(Lists.newArrayList(orderPayRecord)); + } else if (StringUtils.equals(type, ScenarioEnum.BALANCE.getValue())) { // 2-充值余额 + // 充值余额成功 + // memberId = (String) map.get("memberId"); + UpdateMemberBalanceDTO dto = new UpdateMemberBalanceDTO(); + dto.setMemberId(memberId); + dto.setType(MemberWalletEnum.TYPE_IN.getValue()); + dto.setSubType(MemberWalletEnum.SUBTYPE_TOP_UP.getValue()); + dto.setUpdatePrincipalBalance(amount); + memberBasicInfoService.updateMemberBalance(dto); + } + + // 微信支付订单 记录会员交易流水 + MemberTransactionRecord record = MemberTransactionRecord.builder() + .orderCode(orderCode) + .scenarioType(type) + .memberId(memberId) + .actionType(ActionTypeEnum.FORWARD.getValue()) + .payMode(PayModeEnum.PAYMENT_OF_WECHATPAY.getValue()) + .amount(amount) // 单位元 + .outTradeNo(String.valueOf(map.get("out_trade_no"))) + .transactionId(String.valueOf(map.get("transaction_id"))) + .build(); + memberTransactionRecordService.insertSelective(record); + } + + public void weChatRefund(WeChatRefundDTO dto) { + log.info("微信退款接口 param:{}", JSONObject.toJSONString(dto)); + orderBasicInfoService.weChatRefund(dto); + } + + /** + * 查询订单详情信息 + * @param orderCode 订单编号 + * @return + */ + public OrderDetailInfoVO queryOrderDetailInfo(String orderCode) { + OrderDetailInfoVO vo = new OrderDetailInfoVO(); + + // 订单信息 + OrderBasicInfo orderBasicInfo = orderBasicInfoService.getOrderInfoByOrderCode(orderCode); + if (orderBasicInfo == null) { + return vo; + } + OrderDetailInfoVO.OrderInfo order = new OrderDetailInfoVO.OrderInfo(); + order.setOrderCode(orderBasicInfo.getOrderCode()); + order.setOrderStatus(orderBasicInfo.getOrderStatus()); + order.setStartTime(DateUtils.formatDateTime(orderBasicInfo.getChargeStartTime())); + order.setEndTime(DateUtils.formatDateTime(orderBasicInfo.getChargeEndTime())); + order.setCreateTime(DateUtils.formatDateTime(orderBasicInfo.getCreateTime())); + order.setStopReasonMsg(orderBasicInfo.getReason()); + order.setStartSOC(orderBasicInfo.getStartSOC()); + order.setEndSOC(orderBasicInfo.getEndSOC()); + vo.setOrderInfo(order); + + // 设备信息 + PileInfoVO pileInfoVO = pileService.selectPileInfoBySn(orderBasicInfo.getPileSn()); + vo.setPileInfo(pileInfoVO); + + // 枪口实时数据信息 + String pileConnectorCode = orderBasicInfo.getPileSn() + orderBasicInfo.getConnectorCode(); + QueryConnectorListDTO dto = new QueryConnectorListDTO(); + dto.setConnectorCodeList(Lists.newArrayList(pileConnectorCode)); + List chargingRealTimeDataList = orderBasicInfoService.getChargingRealTimeData(orderCode); + if (CollectionUtils.isNotEmpty(chargingRealTimeDataList)) { + List infoList = Lists.newArrayList(); + for (RealTimeMonitorData realTimeMonitorData : chargingRealTimeDataList) { + OrderDetailInfoVO.RealTimeMonitorData info = new OrderDetailInfoVO.RealTimeMonitorData(); + info.setInstantCurrent(realTimeMonitorData.getOutputCurrent()); // 电流 + info.setInstantVoltage(realTimeMonitorData.getOutputVoltage()); // 电压 + info.setInstantPower(realTimeMonitorData.getOutputPower()); // 功率 + info.setSOC(realTimeMonitorData.getSOC()); + info.setTime(realTimeMonitorData.getDateTime()); // 时间 + infoList.add(info); + } + + // 监控信息 + OrderDetailInfoVO.OrderRealTimeInfo realTimeInfo = new OrderDetailInfoVO.OrderRealTimeInfo(); + + RealTimeMonitorData realTimeMonitorData = chargingRealTimeDataList.get(0); + realTimeInfo.setOrderAmount(realTimeMonitorData.getChargingAmount()); + // realTimeInfo.setTotalElectricityAmount(); + // realTimeInfo.setTotalServiceAmount(); + realTimeInfo.setChargedDegree(realTimeMonitorData.getChargingDegree()); + // realTimeInfo.setSOC(realTimeMonitorData.getSOC()); + realTimeInfo.setChargingTime(realTimeMonitorData.getSumChargingTime()); + vo.setOrderRealTimeInfo(realTimeInfo); + + // 根据时间进行正序排序 + infoList = infoList.stream() + .sorted(Comparator.comparing(OrderDetailInfoVO.RealTimeMonitorData::getTime)) + .collect(Collectors.toList()); + vo.setRealTimeMonitorDataList(infoList); + } + + // 支付流水 + List orderPayRecordList = orderPayRecordService.getOrderPayRecordList(orderCode); + if (CollectionUtils.isNotEmpty(orderPayRecordList)) { + OrderPayRecord orderPayRecord = orderPayRecordList.get(0); + List payRecordList = Lists.newArrayList(); + OrderDetailInfoVO.PayRecord payInfo = new OrderDetailInfoVO.PayRecord(); + // 余额支付如果是由本金和赠送一起支付的,合并为一个 + BigDecimal bigDecimal = orderPayRecordList.stream() + .map(OrderPayRecord::getPayAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + payInfo.setPayAmount(bigDecimal.toString()); + payInfo.setPayStatus(orderBasicInfo.getPayStatus()); + payInfo.setPayTime(DateUtils.formatDateTime(orderBasicInfo.getPayTime())); + String payMode = orderPayRecord.getPayMode(); + if (StringUtils.equals(payMode, OrderPayRecordEnum.PRINCIPAL_BALANCE_PAYMENT.getValue()) + || StringUtils.equals(payMode, OrderPayRecordEnum.GIFT_BALANCE_PAYMENT.getValue())) { + // 使用余额支付 + payInfo.setPayMode(OrderPayModeEnum.PAYMENT_OF_BALANCE.getValue()); + payInfo.setPayModeDesc(OrderPayModeEnum.PAYMENT_OF_BALANCE.getLabel()); + } else if (StringUtils.equals(payMode, OrderPayRecordEnum.WECHATPAY_PAYMENT.getValue())){ + // 使用微信支付 + payInfo.setPayMode(OrderPayModeEnum.PAYMENT_OF_WECHATPAY.getValue()); + payInfo.setPayModeDesc(OrderPayModeEnum.PAYMENT_OF_WECHATPAY.getLabel()); + // 查微信支付回调记录 + WxpayCallbackRecord wxpayCallbackRecord = wxpayCallbackRecordService.selectByOrderCode(orderCode); + if (wxpayCallbackRecord != null) { + payInfo.setOutTradeNo(wxpayCallbackRecord.getOutTradeNo()); + payInfo.setTransactionId(wxpayCallbackRecord.getTransactionId()); + } + } else if (StringUtils.equals(payMode, OrderPayRecordEnum.WHITELIST_PAYMENT.getValue())){ + // 使用白名单支付 + payInfo.setPayMode(OrderPayModeEnum.PAYMENT_OF_WHITELIST.getValue()); + payInfo.setPayModeDesc(OrderPayModeEnum.PAYMENT_OF_WHITELIST.getLabel()); + } + payRecordList.add(payInfo); + vo.setPayRecordList(payRecordList); + } + + + + // 用户信息 + MemberVO memberVO = memberService.getMemberInfoByMemberId(orderBasicInfo.getMemberId()); + vo.setMemberInfo(memberVO); + + return vo; + } + + public void wechatPayRefundCallback(HttpServletRequest request, WechatPayNotifyParameter body) throws Exception { + // 获取微信退款成功返回的信息 + Map map = wechatPayService.wechatPayRefundCallbackInfo(request, body); + } + + /** + * 获取小程序订单详情 + * @param orderCode + * @return + */ + public UniAppOrderVO getUniAppOrderDetail(String orderCode) { + OrderBasicInfo orderBasicInfo = orderBasicInfoService.getOrderInfoByOrderCode(orderCode); + if (orderBasicInfo == null) { + throw new BusinessException(ReturnCodeEnum.CODE_QUERY_ORDER_NULL_ERROR); + } + UniAppOrderVO vo = new UniAppOrderVO(); + vo.setOrderCode(orderBasicInfo.getOrderCode()); + vo.setPileSn(orderBasicInfo.getPileSn()); + vo.setConnectorCode(orderBasicInfo.getConnectorCode()); + vo.setPileConnectorCode(orderBasicInfo.getPileSn() + orderBasicInfo.getConnectorCode()); + String orderStatus = orderBasicInfo.getOrderStatus(); + vo.setOrderStatus(orderStatus); + + // 订单状态描述 + String orderStatusDescribe; + if (StringUtils.equals(orderStatus, OrderStatusEnum.NOT_START.getValue())) { + // 未启动还有两种情况 待支付 / 支付成功 + String payStatus = orderBasicInfo.getPayStatus(); + if (StringUtils.equals(payStatus, OrderPayStatusEnum.paid.getValue())) { + // 支付成功,未启动 + orderStatusDescribe = OrderPayStatusEnum.paid.getLabel() + ", " + OrderStatusEnum.getOrderStatus(orderStatus); + } else { + // 待支付 + orderStatusDescribe = OrderPayStatusEnum.unpaid.getLabel(); + } + } else { + orderStatusDescribe = OrderStatusEnum.getOrderStatus(orderStatus); + } + vo.setOrderStatusDescribe(orderStatusDescribe); + + // 获取充电桩枪口信息 + PileConnectorDetailVO pileConnectorDetailVO = pileService.queryPileConnectorDetail(vo.getPileConnectorCode()); + if (pileConnectorDetailVO != null) { + vo.setPileConnectorStatus(pileConnectorDetailVO.getConnectorStatus()); + } + + // 获取订单充电数据 + List monitorDataList = orderBasicInfoService.getChargingRealTimeData(orderCode); + if (CollectionUtils.isNotEmpty(monitorDataList)) { + List chargingDataList = Lists.newArrayList(); + UniAppOrderVO.ChargingData data = null; + for (int i = 0; i < monitorDataList.size(); i++) { + RealTimeMonitorData monitorData = monitorDataList.get(i); + data = new UniAppOrderVO.ChargingData(); + data.setDateTime(monitorData.getDateTime()); + String outputVoltage = monitorData.getOutputVoltage(); + data.setOutputVoltage(outputVoltage); + String outputCurrent = monitorData.getOutputCurrent(); + data.setOutputCurrent(outputCurrent); + BigDecimal power = new BigDecimal(outputCurrent).multiply(new BigDecimal(outputVoltage)) + .divide(new BigDecimal("1000"), 2, BigDecimal.ROUND_HALF_UP); + data.setPower(power.toString()); + data.setSOC(monitorData.getSOC()); + data.setBatteryMaxTemperature(monitorData.getBatteryMaxTemperature()); + chargingDataList.add(data); + // vo中的实时数据,最新一条就取monitorDataList第一个 + if (i == 0) { + vo.setBatteryMaxTemperature(data.getBatteryMaxTemperature()); + vo.setOutputPower(data.getPower()); + vo.setOutputCurrent(data.getOutputCurrent()); + vo.setOutputVoltage(data.getOutputVoltage()); + vo.setSOC(data.getSOC()); + BigDecimal chargingAmount = new BigDecimal(monitorData.getChargingAmount()).setScale(2, BigDecimal.ROUND_HALF_UP); // 充电金额 + vo.setChargingAmount(chargingAmount.toString()); + BigDecimal chargingDegree = new BigDecimal(monitorData.getChargingDegree()).setScale(2, BigDecimal.ROUND_HALF_UP); // 充电度数 + vo.setChargingDegree(chargingDegree.toString()); + vo.setSumChargingTime(monitorData.getSumChargingTime()); + vo.setTimeRemaining(monitorData.getTimeRemaining()); + } + } + // monitorDataList是按照时间倒序的,chargingDataList需要按照时间正序 + Collections.reverse(chargingDataList); + vo.setChargingDataList(chargingDataList); + } + return vo; + } + + /** + * 根据订单号查询充电桩启动状态 + * @param orderCode + * @return + */ + public String selectPileStarterStatus(String orderCode) { + List chargingRealTimeData = orderBasicInfoService.getChargingRealTimeData(orderCode); + // 只有充电桩上传的实时数据中的状态为充电,才能查到实时数据列表 + return CollectionUtils.isNotEmpty(chargingRealTimeData) ? Constants.ONE : Constants.ZERO; + } + + public void closeStartFailedOrder(QueryOrderDTO dto) { + orderBasicInfoService.closeStartFailedOrder(dto.getStartTime(), dto.getEndTime()); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/service/PileRemoteService.java b/jsowell-admin/src/main/java/com/jsowell/service/PileRemoteService.java new file mode 100644 index 000000000..89de66332 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/service/PileRemoteService.java @@ -0,0 +1,203 @@ +package com.jsowell.service; + +import com.google.common.collect.Lists; +import com.jsowell.common.util.StringUtils; +import com.jsowell.netty.command.ykc.GetRealTimeMonitorDataCommand; +import com.jsowell.netty.command.ykc.IssueQRCodeCommand; +import com.jsowell.netty.command.ykc.ProofreadTimeCommand; +import com.jsowell.netty.command.ykc.PublishPileBillingTemplateCommand; +import com.jsowell.netty.command.ykc.RebootCommand; +import com.jsowell.netty.command.ykc.StartChargingCommand; +import com.jsowell.netty.command.ykc.StopChargingCommand; +import com.jsowell.netty.command.ykc.UpdateFileCommand; +import com.jsowell.netty.service.yunkuaichong.YKCPushCommandService; +import com.jsowell.pile.domain.PileBillingRelation; +import com.jsowell.pile.domain.PileBillingTemplate; +import com.jsowell.pile.dto.PublishBillingTemplateDTO; +import com.jsowell.pile.service.IPileBasicInfoService; +import com.jsowell.pile.service.IPileBillingTemplateService; +import com.jsowell.pile.vo.web.PileDetailVO; +import com.jsowell.pile.vo.web.BillingTemplateVO; +import org.apache.commons.collections4.CollectionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +@Service +public class PileRemoteService { + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + @Autowired + private IPileBillingTemplateService pileBillingTemplateService; + + @Autowired + private IPileBasicInfoService pileBasicInfoService; + + @Autowired + private YKCPushCommandService ykcPushCommandService; + + /** + * 获取充电桩实时数据信息 + * + * @param pileSn 充电桩sn + * @param connectorCode 枪口号 + */ + public void getRealTimeMonitorData(String pileSn, String connectorCode) { + if (StringUtils.isNotEmpty(pileSn) || StringUtils.isNotEmpty(connectorCode)) { + GetRealTimeMonitorDataCommand command = GetRealTimeMonitorDataCommand.builder() + .pileSn(pileSn) + .connectorCode(connectorCode) + .build(); + ykcPushCommandService.pushGetRealTimeMonitorDataCommand(command); + } + + } + + /** + * 重启指令 + * + * @param pileSn 充电桩sn + */ + public void reboot(String pileSn) { + RebootCommand command = RebootCommand.builder().pileSn(pileSn).build(); + ykcPushCommandService.pushRebootCommand(command); + } + + /** + * 远程启动充电 0x34 + * + * @param pileSn 充电桩sn + */ + // public void remoteStartCharging(String orderCode, String pileSn, String connectorCode, BigDecimal accountBalance) { + // // String s = "550314127823050120180619144446808800000000000101000000100000057300000000D14B0A54A0860100"; + // if (StringUtils.isEmpty(pileSn) || StringUtils.isEmpty(connectorCode)) { + // log.warn("远程启动充电, 充电桩编号和枪口号不能为空"); + // return; + // } + // log.info("=====平台下发指令=====: 远程启动充电, 桩号:{}, 枪口号:{}", pileSn, connectorCode); + // StartChargingCommand startChargingCommand = StartChargingCommand.builder() + // .pileSn(pileSn) + // .connectorCode(connectorCode) + // .orderCode(orderCode) + // .chargeAmount(accountBalance) + // .build(); + // ykcPushCommandService.pushStartChargingCommand(startChargingCommand); + // } + + /** + * 远程停止充电 + */ + public void remoteStopCharging(String pileSn, String connectorCode) { + StopChargingCommand command = StopChargingCommand.builder() + .pileSn(pileSn) + .connectorCode(connectorCode) + .build(); + ykcPushCommandService.pushStopChargingCommand(command); + } + + /** + * 下发充电桩二维码 + * + * @param pileSn 充电桩sn + */ + public void issueQRCode(String pileSn) { + IssueQRCodeCommand command = IssueQRCodeCommand.builder().pileSn(pileSn).build(); + ykcPushCommandService.pushIssueQRCodeCommand(command); + + } + + /** + * 充电桩对时 + * + * @param pileSn 充电桩sn + */ + public void proofreadTime(String pileSn) { + + ProofreadTimeCommand command = ProofreadTimeCommand.builder().pileSn(pileSn).build(); + ykcPushCommandService.pushProofreadTimeCommand(command); + } + + /** + * 下发充电桩计费模型 + */ + public void publishPileBillingTemplate(String pileSn, BillingTemplateVO billingTemplateVO) { + + PublishPileBillingTemplateCommand command = PublishPileBillingTemplateCommand.builder() + .billingTemplateVO(billingTemplateVO) + .pileSn(pileSn) + .build(); + ykcPushCommandService.pushPublishPileBillingTemplate(command); + } + + /** + * 下发计费模板 + * @param dto + * @return + */ + public boolean publishBillingTemplate(PublishBillingTemplateDTO dto) { + // 获取计费模板信息 + BillingTemplateVO billingTemplateVO = pileBillingTemplateService.selectBillingTemplateByTemplateId(dto.getTemplateId()); + if (billingTemplateVO == null) { + log.warn("获取计费模板信息, 通过模板id:{}查询计费模板为null", dto.getTemplateId()); + return false; + } + // 更新计费模板的发布时间 + PileBillingTemplate pileBillingTemplate = new PileBillingTemplate(); + pileBillingTemplate.setId(Long.valueOf(billingTemplateVO.getTemplateId())); + pileBillingTemplate.setPublishTime(new Date()); + pileBillingTemplateService.updatePileBillingTemplate(pileBillingTemplate); + + // 获取到站点下所有的桩 + List pileList = pileBasicInfoService.selectPileListByStationIds(Lists.newArrayList(Long.valueOf(dto.getStationId()))); + if (CollectionUtils.isEmpty(pileList)) { + log.warn("获取到站点下所有的桩, 通过站点id:{}查询充电桩列表为空", dto.getStationId()); + return false; + } + + // 保存计费模板和桩的关系 + List relationList = Lists.newArrayList(); + for (PileDetailVO pileInfoVO : pileList) { + // push + publishPileBillingTemplate(pileInfoVO.getPileSn(), billingTemplateVO); + + // 入库 + relationList.add(PileBillingRelation.builder() + .pileSn(pileInfoVO.getPileSn()) + .billingTemplateCode(billingTemplateVO.getTemplateCode()) + .stationId(dto.getStationId()) + .build()); + } + + if (CollectionUtils.isNotEmpty(relationList)) { + pileBillingTemplateService.insertPileBillingRelation(relationList); + } + return true; + } + + public static void main(String[] args) { + /*Date date = new Date(); + String encodeCP56Time2a = DateUtils.encodeCP56Time2a(date); + System.out.println(encodeCP56Time2a); + System.out.println(DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, date)); + + String decodeCP56Time2a = DateUtils.decodeCP56Time2a(encodeCP56Time2a); + System.out.println(decodeCP56Time2a);*/ + + } + + /** + * 远程更新 + * + * @param pileSns 前台传的桩号集合 + */ + public void updateFile(List pileSns) { + // + UpdateFileCommand command = UpdateFileCommand.builder().pileSnList(pileSns).build(); + ykcPushCommandService.pushUpdateFileCommand(command); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/service/PileService.java b/jsowell-admin/src/main/java/com/jsowell/service/PileService.java new file mode 100644 index 000000000..68f3798ac --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/service/PileService.java @@ -0,0 +1,438 @@ +package com.jsowell.service; + +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.google.common.collect.Lists; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.domain.ykc.RealTimeMonitorData; +import com.jsowell.common.core.page.PageResponse; +import com.jsowell.common.enums.ykc.BusinessTypeEnum; +import com.jsowell.common.enums.ykc.OrderStatusEnum; +import com.jsowell.common.enums.ykc.ReturnCodeEnum; +import com.jsowell.common.exception.BusinessException; +import com.jsowell.common.response.RestApiResponse; +import com.jsowell.common.util.DateUtils; +import com.jsowell.common.util.SecurityUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.id.SnUtils; +import com.jsowell.pile.domain.*; +import com.jsowell.pile.dto.BatchCreatePileDTO; +import com.jsowell.pile.dto.MemberRegisterAndLoginDTO; +import com.jsowell.pile.dto.PileMemberBindingDTO; +import com.jsowell.pile.dto.QueryPersonPileDTO; +import com.jsowell.pile.service.*; +import com.jsowell.pile.transaction.dto.PileTransactionDTO; +import com.jsowell.pile.transaction.service.TransactionService; +import com.jsowell.pile.vo.base.ConnectorInfoVO; +import com.jsowell.pile.vo.base.MerchantInfoVO; +import com.jsowell.pile.vo.base.PileInfoVO; +import com.jsowell.pile.vo.uniapp.*; +import com.jsowell.pile.vo.web.PileStationVO; +import org.apache.commons.collections4.CollectionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +@Service +public class PileService { + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + @Autowired + private TransactionService pileTransactionService; + + @Resource + private SnUtils snUtils; + + @Autowired + private IPileBasicInfoService pileBasicInfoService; + + @Autowired + private IPileConnectorInfoService pileConnectorInfoService; + + @Autowired + private IPileStationInfoService pileStationInfoService; + + @Autowired + private IPileMerchantInfoService pileMerchantInfoService; + + @Autowired + private IPileBillingTemplateService pileBillingTemplateService; + + @Autowired + private IPileMemberRelationService pileMemberRelationService; + + @Autowired + private IMemberBasicInfoService memberBasicInfoService; + + @Autowired + private MemberService memberService; + + @Autowired + private IOrderBasicInfoService orderBasicInfoService; + + /** + * 查询设备信息 + * + * @param pileConnectorCode 充电枪编号 880000000000000101 + * @return 设备信息集合 + */ + public PileConnectorDetailVO queryPileConnectorDetail(String pileConnectorCode) { + return pileBasicInfoService.queryPileConnectorDetail(pileConnectorCode); + } + + /** + * 查询设备信息 + * + * @param pileSn 充电枪编号 8800000000000001 + * @param connectorCode 充电枪口号 01 + * @return 设备信息集合 + */ + public PileConnectorDetailVO queryPileConnectorDetail(String pileSn, String connectorCode) { + return queryPileConnectorDetail(pileSn + connectorCode); + } + + public int batchCreatePile(BatchCreatePileDTO dto) { + // 批量生成sn号 + List snList = snUtils.generateSN(dto.getNum()); + + // + List basicInfoList = Lists.newArrayList(); + List connectorInfoList = Lists.newArrayList(); + PileBasicInfo basicInfo = null; + PileConnectorInfo connectorInfo = null; + for (String sn : snList) { + // 组装pile_basic_info表数据 + basicInfo = new PileBasicInfo(); + basicInfo.setSn(sn); // sn号 + basicInfo.setBusinessType(BusinessTypeEnum.OPERATING_PILE.getValue()); // 经营类型 默认运营桩 + basicInfo.setSoftwareProtocol(dto.getSoftwareProtocol()); // 软件协议 + basicInfo.setMerchantId(Long.valueOf(dto.getMerchantId())); // 运营商id + basicInfo.setStationId(Long.valueOf(dto.getStationId())); // 站点id + basicInfo.setModelId(Long.valueOf(dto.getModelId())); // 型号id + basicInfo.setProductionDate(dto.getProductionDate()); // 生产日期 + basicInfo.setLicenceId(null); // TODO 证书编号 + basicInfo.setSimId(null); // TODO sim卡 + basicInfo.setRemark(dto.getRemark()); // 备注 + basicInfo.setCreateBy(SecurityUtils.getUsername()); // 创建人 + basicInfo.setDelFlag(Constants.DEL_FLAG_NORMAL); // 删除标识 + basicInfoList.add(basicInfo); + + for (int i = 1; i < dto.getConnectorNum() + 1; i++) { + // 组装pile_connector_info表数据 + connectorInfo = new PileConnectorInfo(); + connectorInfo.setPileSn(sn); // sn号 + String connectorCode = String.format("%1$02d", i); + connectorInfo.setPileConnectorCode(sn + connectorCode); // 枪口号 + connectorInfo.setStatus(Constants.ZERO); //状态,默认 0-离网 + connectorInfo.setCreateBy(SecurityUtils.getUsername()); // 创建人 + connectorInfo.setDelFlag(Constants.DEL_FLAG_NORMAL); // 删除标识 + connectorInfoList.add(connectorInfo); + } + } + + // 批量入库 + PileTransactionDTO transactionDTO = PileTransactionDTO.builder() + .pileBasicInfoList(basicInfoList) + .pileConnectorInfoList(connectorInfoList) + .build(); + return pileTransactionService.doCreatePileTransaction(transactionDTO); + } + + /** + * 前端扫码跳转接口 + */ + public PileConnectorVO getPileDetailByPileSn(String param) throws ExecutionException, InterruptedException { + if (StringUtils.isBlank(param)) { + throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR); + } + String pileSn; + if (param.length() == 16) { + pileSn = StringUtils.substring(param, 0, param.length() - 2); + } else { + pileSn = param; + } + // 查询充电桩信息 + PileInfoVO pileInfoVO = pileBasicInfoService.selectPileInfoBySn(pileSn); + if (pileInfoVO == null) { + return null; + } + + // 查询充电桩下枪口信息 + CompletableFuture> connectorInfoListFuture = CompletableFuture.supplyAsync(() -> pileConnectorInfoService.selectConnectorInfoList(pileSn)); + + // 查计费模板信息 + CompletableFuture> billingPriceFuture = CompletableFuture.supplyAsync(() -> pileBillingTemplateService.queryBillingPrice(pileInfoVO.getStationId())); + + // 查询站点信息 + CompletableFuture pileStationVOFuture = CompletableFuture.supplyAsync(() -> pileStationInfoService.getStationInfo(pileInfoVO.getStationId())); + + // 查询运营商信息 + CompletableFuture merchantInfoVOFuture = CompletableFuture.supplyAsync(() -> pileMerchantInfoService.getMerchantInfo(pileInfoVO.getMerchantId())); + + CompletableFuture all = CompletableFuture.allOf(connectorInfoListFuture, pileStationVOFuture, merchantInfoVOFuture, billingPriceFuture); + // .join()和.get()都会阻塞并获取线程的执行情况 + // .join()会抛出未经检查的异常,不会强制开发者处理异常 .get()会抛出检查异常,需要开发者处理 + all.join(); + all.get(); + List connectorInfoList = connectorInfoListFuture.get(); + PileStationVO pileStationVO = pileStationVOFuture.get(); + MerchantInfoVO merchantInfoVO = merchantInfoVOFuture.get(); + List billingPriceVOS = billingPriceFuture.get(); + + PileConnectorVO vo = PileConnectorVO.builder() + .pileInfo(pileInfoVO) + .connectorInfoList(connectorInfoList) + .merchantInfo(merchantInfoVO) + .stationInfo(pileStationVO) + .billingPriceList(billingPriceVOS) + .build(); + return vo; + } + + public PileConnectorVO getConnectorDetail(String pileConnectorCode) throws ExecutionException, InterruptedException { + PileConnectorDetailVO pileConnectorDetailVO = queryPileConnectorDetail(pileConnectorCode); + if (pileConnectorDetailVO == null) { + return null; + } + String pileSn = pileConnectorDetailVO.getPileSn(); + PileConnectorVO resultVO = getPileDetailByPileSn(pileSn); + List connectorInfoList = resultVO.getConnectorInfoList(); + if (connectorInfoList.size() > 1) { + List list = Lists.newArrayList(); + // 枪口大于1个,此充电桩非单枪设备,根据参数展示对应枪口的信息 + for (ConnectorInfoVO connectorInfoVO : connectorInfoList) { + if (StringUtils.equals(pileConnectorCode, pileSn + connectorInfoVO.getConnectorCode())) { + list.add(connectorInfoVO); + } + } + resultVO.setConnectorInfoList(list); + } + return resultVO; + } + + public PileInfoVO selectPileInfoBySn(String pileSn) { + return pileBasicInfoService.selectPileInfoBySn(pileSn); + } + + /** + * 用户绑定个人桩 + * + * @param dto + * @return + */ + public int pileMemberBinding(PileMemberBindingDTO dto){ + // 校验短信验证码 + MemberRegisterAndLoginDTO registerAndLoginDTO = MemberRegisterAndLoginDTO.builder() + .mobileNumber(dto.getPhoneNumber()) + .verificationCode(dto.getVerificationCode()) + .build(); + memberService.checkVerificationCode(registerAndLoginDTO); + + // 检查桩是否已经被绑定 + PileMemberRelation pileMemberRelation = new PileMemberRelation(); + pileMemberRelation.setPileSn(dto.getPileSn()); + List list = pileMemberRelationService.selectPileMemberRelationList(pileMemberRelation); + if (CollectionUtils.isNotEmpty(list)){ + // 说明已经被绑定过,抛出异常 + throw new BusinessException(ReturnCodeEnum.CODE_PILE_HAS_BEEN_BINDING_ERROR); + } + // 如果没被绑定,则此用户为管理员 + pileMemberRelation.setMemberId(dto.getMemberId()); + pileMemberRelation.setType("1"); // 1-管理员 + return pileMemberRelationService.insertPileMemberRelation(pileMemberRelation); + } + + /** + * 个人桩管理员下发给其他用户使用 + * + * @param dto + */ + public void adminIssuePile(PileMemberBindingDTO dto) { + List relationList = pileMemberRelationService.selectPileMemberRelationByPileSn(dto.getPileSn()); + if (CollectionUtils.isEmpty(relationList)) { + // 充电桩没有绑定任何人 + } + + List adminList = relationList.stream() + .filter(x -> x.getType().equals("1")) + .map(PileMemberRelation::getMemberId) + .collect(Collectors.toList()); + if (CollectionUtils.isEmpty(adminList)) { + // 没有管理员 + } + + // 校验身份 + if (!adminList.contains(dto.getMemberId())) { + // 如果为空,说明此用户身份有误,不是管理员,抛出异常 + throw new BusinessException(ReturnCodeEnum.CODE_AUTHENTICATION_ERROR); + } + + List userList = relationList.stream() + .filter(x -> !x.getType().equals("1")) + .map(PileMemberRelation::getMemberId) + .collect(Collectors.toList()); + + + // 通过前端传的手机号查询是否有此用户 + MemberBasicInfo memberBasicInfo = memberBasicInfoService.selectInfoByMobileNumber(dto.getPhoneNumber()); + if (memberBasicInfo == null) { + // 为空说明此用户未注册平台账号 + throw new BusinessException(ReturnCodeEnum.CODE_USER_IS_NOT_REGISTER); + } + + if (userList.contains(memberBasicInfo.getMemberId())) { + // 不为空说明已绑定 + throw new BusinessException(ReturnCodeEnum.CODE_USER_HAS_BEEN_THIS_PILE); + } else { + // 进行绑定,此用户为普通用户 + PileMemberRelation info = new PileMemberRelation(); + info.setPileSn(dto.getPileSn()); + info.setMemberId(memberBasicInfo.getMemberId()); + info.setType("2"); + pileMemberRelationService.insertPileMemberRelation(info); + } + } + + /** + * 获取枪口实时数据 + * @param dto + * @return + */ + public PersonPileRealTimeVO getConnectorRealTimeInfo(QueryPersonPileDTO dto) { + // 根据memberId查出该用户 正在充电、个人桩启动(白名单支付方式)的订单号 + OrderBasicInfo orderBasicInfo = OrderBasicInfo.builder() + .memberId(dto.getMemberId()) + .orderStatus(OrderStatusEnum.IN_THE_CHARGING.getValue()) + .pileConnectorCode(dto.getPileConnectorCode()) + .startMode("3") // 3- 白名单支付 + .build(); + OrderBasicInfo basicInfo = orderBasicInfoService.getOrderBasicInfo(orderBasicInfo); + if (basicInfo == null){ + throw new BusinessException(ReturnCodeEnum.CODE_NO_CHARGING_ORDER_ERROR); + } + String orderCode = basicInfo.getOrderCode(); + // 根据订单号从redis中获取实时数据信息(默认时间倒叙排列,所以取第一条) + List chargingRealTimeData = orderBasicInfoService.getChargingRealTimeData(orderCode); + if (CollectionUtils.isEmpty(chargingRealTimeData)) { + throw new BusinessException(ReturnCodeEnum.CODE_NO_REAL_TIME_INFO); + } + RealTimeMonitorData realTimeMonitorData = chargingRealTimeData.get(0); + // 将需要的数据set + PersonPileRealTimeVO vo = PersonPileRealTimeVO.builder() + .chargingDegree(realTimeMonitorData.getChargingDegree()) + .chargingTime(realTimeMonitorData.getSumChargingTime()) + .startTime(DateUtils.formatDateTime(orderBasicInfo.getChargeStartTime())) + .instantCurrent(realTimeMonitorData.getOutputCurrent()) + .instantVoltage(realTimeMonitorData.getOutputVoltage()) + .instantPower(realTimeMonitorData.getOutputPower()) + .build(); + return vo; + } + + /** + * 获取某枪口某段时间内累计信息 + * @param dto + * @return + */ + public PersonPileConnectorSumInfoVO getAccumulativeInfo(QueryPersonPileDTO dto) { + List accumulativeInfo = orderBasicInfoService.getAccumulativeInfo(dto); + if (CollectionUtils.isEmpty(accumulativeInfo)) { + throw new BusinessException("00400011", "未查到相关订单信息!"); + } + // BigDecimal sumChargingTime = BigDecimal.ZERO; + // BigDecimal sumUsedElectricity = BigDecimal.ZERO; + // // 将返回的结果进行for循环,将总充电量、总充电时长进行累加(充电时长需要通过 充电开始时间 和 充电结束时间 进行计算) + // for (PersonPileConnectorSumInfoVO personPileConnectorSumInfoVO : accumulativeInfo) { + // if (StringUtils.isNotBlank(personPileConnectorSumInfoVO.getChargeStartTime()) && StringUtils.isNotBlank(personPileConnectorSumInfoVO.getChargeEndTime())){ + // long longChargingTime = DateUtils.intervalTime(personPileConnectorSumInfoVO.getChargeStartTime(), personPileConnectorSumInfoVO.getChargeEndTime()); + // BigDecimal chargingTime = new BigDecimal(String.valueOf(longChargingTime)); + // sumChargingTime = sumChargingTime.add(chargingTime); + // } + // BigDecimal chargingDegree = StringUtils.isBlank(personPileConnectorSumInfoVO.getSumChargingDegree()) + // ? BigDecimal.ZERO + // : new BigDecimal(personPileConnectorSumInfoVO.getSumChargingDegree()); + // sumUsedElectricity = sumUsedElectricity.add(chargingDegree); + // } + Map sumInfo = getSumInfo(accumulativeInfo); + // set 对象 + PersonPileConnectorSumInfoVO vo = new PersonPileConnectorSumInfoVO(); + vo.setStartTime(dto.getStartTime()); + vo.setEndTime(dto.getEndTime()); + vo.setMemberId(dto.getMemberId()); + vo.setSumChargingDegree(sumInfo.get("sumUsedElectricity")); + vo.setSumChargingTime(sumInfo.get("sumChargingTime")); + return vo; + } + + /** + * 获取充电记录(默认30天) + * + * @param dto + */ + public PageResponse getChargingRecord(QueryPersonPileDTO dto) { + int pageNum = dto.getPageNum() == 0 ? 1 : dto.getPageNum(); + int pageSize = dto.getPageSize() == 0 ? 10 : dto.getPageSize(); + // 获取三十天前的数据 + Date date = DateUtils.addMonths(new Date(), -1); + String dateStr = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, date); + dto.setStartTime(dateStr); + dto.setEndTime(DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, new Date())); + + // 分页查询 + PageHelper.startPage(pageNum, pageSize); + List accumulativeInfo = orderBasicInfoService.getAccumulativeInfo(dto); + if (CollectionUtils.isEmpty(accumulativeInfo)) { + throw new BusinessException("00400011", "未查到相关订单信息!"); + } + PageInfo pageInfo = new PageInfo<>(accumulativeInfo); + for (PersonPileConnectorSumInfoVO personPileConnectorSumInfoVO : pageInfo.getList()) { + if (StringUtils.isNotBlank(personPileConnectorSumInfoVO.getChargeStartTime()) && StringUtils.isNotBlank(personPileConnectorSumInfoVO.getChargeEndTime())){ + String datePoor = DateUtils.getDatePoor(DateUtils.parseDate(personPileConnectorSumInfoVO.getChargeEndTime()), + DateUtils.parseDate(personPileConnectorSumInfoVO.getChargeStartTime())); + personPileConnectorSumInfoVO.setSumChargingTime(datePoor); + } + } + return PageResponse.builder() + .pageNum(pageInfo.getPageNum()) + .pageSize(pageInfo.getPageSize()) + .list(pageInfo.getList()) + .pages(pageInfo.getPages()) + .total(pageInfo.getTotal()) + .build(); + } + + /** + * 获取总充电时长、总充电量 + * + * @param accumulativeInfo + * @return + */ + private Map getSumInfo(List accumulativeInfo){ + Map resultMap = new HashMap<>(); + BigDecimal sumChargingTime = BigDecimal.ZERO; + BigDecimal sumUsedElectricity = BigDecimal.ZERO; + // 将返回的结果进行for循环,将总充电量、总充电时长进行累加(充电时长需要通过 充电开始时间 和 充电结束时间 进行计算) + for (PersonPileConnectorSumInfoVO personPileConnectorSumInfoVO : accumulativeInfo) { + if (StringUtils.isNotBlank(personPileConnectorSumInfoVO.getChargeStartTime()) && StringUtils.isNotBlank(personPileConnectorSumInfoVO.getChargeEndTime())){ + long longChargingTime = DateUtils.intervalTime(personPileConnectorSumInfoVO.getChargeStartTime(), personPileConnectorSumInfoVO.getChargeEndTime()); + BigDecimal chargingTime = new BigDecimal(String.valueOf(longChargingTime)); + sumChargingTime = sumChargingTime.add(chargingTime); + } + BigDecimal chargingDegree = StringUtils.isBlank(personPileConnectorSumInfoVO.getSumChargingDegree()) + ? BigDecimal.ZERO + : new BigDecimal(personPileConnectorSumInfoVO.getSumChargingDegree()); + sumUsedElectricity = sumUsedElectricity.add(chargingDegree); + } + resultMap.put("sumChargingTime", String.valueOf(sumChargingTime)); + resultMap.put("sumUsedElectricity", String.valueOf(sumUsedElectricity)); + return resultMap; + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/common/CaptchaController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/common/CaptchaController.java new file mode 100644 index 000000000..ab6cf0260 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/common/CaptchaController.java @@ -0,0 +1,91 @@ +package com.jsowell.web.controller.common; + +import com.google.code.kaptcha.Producer; +import com.jsowell.common.config.JsowellConfig; +import com.jsowell.common.constant.CacheConstants; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.redis.RedisCache; +import com.jsowell.common.util.sign.Base64; +import com.jsowell.common.util.id.IdUtils; +import com.jsowell.system.service.SysConfigService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.FastByteArrayOutputStream; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.imageio.ImageIO; +import javax.servlet.http.HttpServletResponse; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +/** + * 验证码操作处理 + * + * @author jsowell + */ +@RestController +public class CaptchaController { + @Resource(name = "captchaProducer") + private Producer captchaProducer; + + @Resource(name = "captchaProducerMath") + private Producer captchaProducerMath; + + @Autowired + private RedisCache redisCache; + + @Autowired + private SysConfigService configService; + + Logger log = LoggerFactory.getLogger(CaptchaController.class); + + /** + * 生成验证码 + */ + @GetMapping("/captchaImage") + public AjaxResult getCode(HttpServletResponse response) throws IOException { + AjaxResult ajax = AjaxResult.success(); + boolean captchaEnabled = configService.selectCaptchaEnabled(); + ajax.put("captchaEnabled", captchaEnabled); + if (!captchaEnabled) { + return ajax; + } + + // 保存验证码信息 + String uuid = IdUtils.simpleUUID(); + String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid; + + String capStr = null, code = null; + BufferedImage image = null; + + // 生成验证码 + String captchaType = JsowellConfig.getCaptchaType(); + if ("math".equals(captchaType)) { + String capText = captchaProducerMath.createText(); + capStr = capText.substring(0, capText.lastIndexOf("@")); + code = capText.substring(capText.lastIndexOf("@") + 1); + image = captchaProducerMath.createImage(capStr); + } else if ("char".equals(captchaType)) { + capStr = code = captchaProducer.createText(); + image = captchaProducer.createImage(capStr); + } + log.info("获取验证码 uuid:{}, captcha:{}", uuid, capStr); + redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES); + // 转换流信息写出 + FastByteArrayOutputStream os = new FastByteArrayOutputStream(); + try { + ImageIO.write(image, "jpg", os); + } catch (IOException e) { + return AjaxResult.error(e.getMessage()); + } + + ajax.put("uuid", uuid); + ajax.put("img", Base64.encode(os.toByteArray())); + return ajax; + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/common/CommonController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/common/CommonController.java new file mode 100644 index 000000000..edf07688c --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/common/CommonController.java @@ -0,0 +1,143 @@ +package com.jsowell.web.controller.common; + +import com.jsowell.common.config.JsowellConfig; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.file.FileUploadUtils; +import com.jsowell.common.util.file.FileUtils; +import com.jsowell.framework.config.ServerConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.List; + +/** + * 通用请求处理 + * + * @author jsowell + */ +@RestController +@RequestMapping("/common") +public class CommonController { + private static final Logger log = LoggerFactory.getLogger(CommonController.class); + + @Autowired + private ServerConfig serverConfig; + + private static final String FILE_DELIMETER = ","; + + /** + * 通用下载请求 + * + * @param fileName 文件名称 + * @param delete 是否删除 + */ + @GetMapping("/download") + public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request) { + try { + if (!FileUtils.checkAllowDownload(fileName)) { + throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName)); + } + String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1); + String filePath = JsowellConfig.getDownloadPath() + fileName; + + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + FileUtils.setAttachmentResponseHeader(response, realFileName); + FileUtils.writeBytes(filePath, response.getOutputStream()); + if (delete) { + FileUtils.deleteFile(filePath); + } + } catch (Exception e) { + log.error("下载文件失败", e); + } + } + + /** + * 通用上传请求(单个) + */ + @PostMapping("/upload") + public AjaxResult uploadFile(MultipartFile file) throws Exception { + try { + // 上传文件路径 + String filePath = JsowellConfig.getUploadPath(); + // 上传并返回新文件名称 + String fileName = FileUploadUtils.upload(filePath, file); + String url = serverConfig.getUrl() + fileName; + AjaxResult ajax = AjaxResult.success(); + ajax.put("url", url); + ajax.put("fileName", fileName); + ajax.put("newFileName", FileUtils.getName(fileName)); + ajax.put("originalFilename", file.getOriginalFilename()); + return ajax; + } catch (Exception e) { + return AjaxResult.error(e.getMessage()); + } + } + + /** + * 通用上传请求(多个) + */ + @PostMapping("/uploads") + public AjaxResult uploadFiles(List files) throws Exception { + try { + // 上传文件路径 + String filePath = JsowellConfig.getUploadPath(); + List urls = new ArrayList(); + List fileNames = new ArrayList(); + List newFileNames = new ArrayList(); + List originalFilenames = new ArrayList(); + for (MultipartFile file : files) { + // 上传并返回新文件名称 + String fileName = FileUploadUtils.upload(filePath, file); + String url = serverConfig.getUrl() + fileName; + urls.add(url); + fileNames.add(fileName); + newFileNames.add(FileUtils.getName(fileName)); + originalFilenames.add(file.getOriginalFilename()); + } + AjaxResult ajax = AjaxResult.success(); + ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER)); + ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER)); + ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMETER)); + ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMETER)); + return ajax; + } catch (Exception e) { + return AjaxResult.error(e.getMessage()); + } + } + + /** + * 本地资源通用下载 + */ + @GetMapping("/download/resource") + public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response) + throws Exception { + try { + if (!FileUtils.checkAllowDownload(resource)) { + throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource)); + } + // 本地资源路径 + String localPath = JsowellConfig.getProfile(); + // 数据库资源地址 + String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX); + // 下载名称 + String downloadName = StringUtils.substringAfterLast(downloadPath, "/"); + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + FileUtils.setAttachmentResponseHeader(response, downloadName); + FileUtils.writeBytes(downloadPath, response.getOutputStream()); + } catch (Exception e) { + log.error("下载文件失败", e); + } + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/index/indexController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/index/indexController.java new file mode 100644 index 000000000..04e2efc7c --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/index/indexController.java @@ -0,0 +1,71 @@ +package com.jsowell.web.controller.index; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.response.RestApiResponse; +import com.jsowell.pile.service.IOrderBasicInfoService; +import com.jsowell.pile.vo.web.IndexGeneralSituationVO; +import com.jsowell.pile.dto.IndexQueryDTO; +import com.jsowell.pile.service.IPileBasicInfoService; +import com.jsowell.pile.vo.web.IndexOrderInfoVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 首页数据展示Controller + * + * @author JS-ZZA + * @date 2023/2/3 10:11 + */ +@RestController +@RequestMapping("/index") +public class indexController extends BaseController { + @Autowired + IPileBasicInfoService pileBasicInfoService; + + @Autowired + IOrderBasicInfoService orderBasicInfoService; + + @PostMapping("/getGeneralSituation") + public RestApiResponse getGeneralSituation(@RequestBody IndexQueryDTO dto) { + logger.info("首页基础数据查询 param:{}", JSONObject.toJSONString(dto)); + RestApiResponse response; + try { + IndexGeneralSituationVO generalSituation = pileBasicInfoService.getGeneralSituation(dto); + response = new RestApiResponse<>(generalSituation); + }catch (Exception e) { + logger.error("首页数据查询错误:{}", e.getMessage()); + response = new RestApiResponse<>("00200001", "首页基础数据查询错误"); + } + logger.info("首页数据查询 result:{}", JSONObject.toJSONString(response)); + return response; + } + + /** + * 首页订单数据汇总信息 + * + * @param dto + * @return + */ + @PostMapping("/getOrderInfo") + public RestApiResponse getOrderInfo(@RequestBody IndexQueryDTO dto) { + logger.info("首页订单相关信息查询 param:{}", JSONObject.toJSONString(dto)); + RestApiResponse response; + try { + List indexOrderInfo = orderBasicInfoService.getIndexOrderInfo(dto); + response = new RestApiResponse<>(indexOrderInfo); + } catch (Exception e) { + logger.error("首页订单相关信息查询错误!{}", e.getMessage()); + response = new RestApiResponse<>("00200002", "首页订单数据查询错误"); + } + logger.info("首页订单相关信息查询 result:{}", JSONObject.toJSONString(response)); + return response; + } + + +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/monitor/CacheController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/monitor/CacheController.java new file mode 100644 index 000000000..58c46e9bf --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/monitor/CacheController.java @@ -0,0 +1,114 @@ +package com.jsowell.web.controller.monitor; + +import com.jsowell.common.constant.CacheConstants; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.util.StringUtils; +import com.jsowell.system.domain.SysCache; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +/** + * 缓存监控 + * + * @author jsowell + */ +@RestController +@RequestMapping("/monitor/cache") +public class CacheController { + @Autowired + private RedisTemplate redisTemplate; + + private final static List caches = new ArrayList(); + + { + caches.add(new SysCache(CacheConstants.LOGIN_TOKEN_KEY, "用户信息")); + caches.add(new SysCache(CacheConstants.SYS_CONFIG_KEY, "配置信息")); + caches.add(new SysCache(CacheConstants.SYS_DICT_KEY, "数据字典")); + caches.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "验证码")); + caches.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "防重提交")); + caches.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理")); + caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数")); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @GetMapping() + public AjaxResult getInfo() throws Exception { + Properties info = (Properties) redisTemplate.execute((RedisCallback) connection -> connection.info()); + Properties commandStats = (Properties) redisTemplate.execute((RedisCallback) connection -> connection.info("commandstats")); + Object dbSize = redisTemplate.execute((RedisCallback) connection -> connection.dbSize()); + + Map result = new HashMap<>(3); + result.put("info", info); + result.put("dbSize", dbSize); + + List> pieList = new ArrayList<>(); + commandStats.stringPropertyNames().forEach(key -> { + Map data = new HashMap<>(2); + String property = commandStats.getProperty(key); + data.put("name", StringUtils.removeStart(key, "cmdstat_")); + data.put("value", StringUtils.substringBetween(property, "calls=", ",usec")); + pieList.add(data); + }); + result.put("commandStats", pieList); + return AjaxResult.success(result); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @GetMapping("/getNames") + public AjaxResult cache() { + return AjaxResult.success(caches); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @GetMapping("/getKeys/{cacheName}") + public AjaxResult getCacheKeys(@PathVariable String cacheName) { + Set cacheKeys = redisTemplate.keys(cacheName + "*"); + return AjaxResult.success(cacheKeys); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @GetMapping("/getValue/{cacheName}/{cacheKey}") + public AjaxResult getCacheValue(@PathVariable String cacheName, @PathVariable String cacheKey) { + String cacheValue = redisTemplate.opsForValue().get(cacheKey); + SysCache sysCache = new SysCache(cacheName, cacheKey, cacheValue); + return AjaxResult.success(sysCache); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @DeleteMapping("/clearCacheName/{cacheName}") + public AjaxResult clearCacheName(@PathVariable String cacheName) { + Collection cacheKeys = redisTemplate.keys(cacheName + "*"); + redisTemplate.delete(cacheKeys); + return AjaxResult.success(); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @DeleteMapping("/clearCacheKey/{cacheKey}") + public AjaxResult clearCacheKey(@PathVariable String cacheKey) { + redisTemplate.delete(cacheKey); + return AjaxResult.success(); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @DeleteMapping("/clearCacheAll") + public AjaxResult clearCacheAll() { + Collection cacheKeys = redisTemplate.keys("*"); + redisTemplate.delete(cacheKeys); + return AjaxResult.success(); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/monitor/ServerController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/monitor/ServerController.java new file mode 100644 index 000000000..8e64d3b76 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/monitor/ServerController.java @@ -0,0 +1,25 @@ +package com.jsowell.web.controller.monitor; + +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.framework.web.domain.Server; + +/** + * 服务器监控 + * + * @author jsowell + */ +@RestController +@RequestMapping("/monitor/server") +public class ServerController { + @PreAuthorize("@ss.hasPermi('monitor:server:list')") + @GetMapping() + public AjaxResult getInfo() throws Exception { + Server server = new Server(); + server.copyTo(); + return AjaxResult.success(server); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/monitor/SysLogininforController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/monitor/SysLogininforController.java new file mode 100644 index 000000000..8c6cc1982 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/monitor/SysLogininforController.java @@ -0,0 +1,72 @@ +package com.jsowell.web.controller.monitor; + +import com.jsowell.common.annotation.Log; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.util.poi.ExcelUtil; +import com.jsowell.framework.web.service.SysPasswordService; +import com.jsowell.system.domain.SysLogininfor; +import com.jsowell.system.service.SysLogininforService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 系统访问记录 + * + * @author jsowell + */ +@RestController +@RequestMapping("/monitor/logininfor") +public class SysLogininforController extends BaseController { + @Autowired + private SysLogininforService logininforService; + + @Autowired + private SysPasswordService passwordService; + + @PreAuthorize("@ss.hasPermi('monitor:logininfor:list')") + @GetMapping("/list") + public TableDataInfo list(SysLogininfor logininfor) { + startPage(); + List list = logininforService.selectLogininforList(logininfor); + return getDataTable(list); + } + + @Log(title = "登录日志", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('monitor:logininfor:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysLogininfor logininfor) { + List list = logininforService.selectLogininforList(logininfor); + ExcelUtil util = new ExcelUtil(SysLogininfor.class); + util.exportExcel(response, list, "登录日志"); + } + + @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')") + @Log(title = "登录日志", businessType = BusinessType.DELETE) + @DeleteMapping("/{infoIds}") + public AjaxResult remove(@PathVariable Long[] infoIds) { + return toAjax(logininforService.deleteLogininforByIds(infoIds)); + } + + @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')") + @Log(title = "登录日志", businessType = BusinessType.CLEAN) + @DeleteMapping("/clean") + public AjaxResult clean() { + logininforService.cleanLogininfor(); + return success(); + } + + @PreAuthorize("@ss.hasPermi('monitor:logininfor:unlock')") + @Log(title = "账户解锁", businessType = BusinessType.OTHER) + @GetMapping("/unlock/{userName}") + public AjaxResult unlock(@PathVariable("userName") String userName) { + passwordService.clearLoginRecordCache(userName); + return success(); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/monitor/SysOperlogController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/monitor/SysOperlogController.java new file mode 100644 index 000000000..21e95c2a8 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/monitor/SysOperlogController.java @@ -0,0 +1,65 @@ +package com.jsowell.web.controller.monitor; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.jsowell.common.annotation.Log; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.util.poi.ExcelUtil; +import com.jsowell.system.domain.SysOperLog; +import com.jsowell.system.service.SysOperLogService; + +/** + * 操作日志记录 + * + * @author jsowell + */ +@RestController +@RequestMapping("/monitor/operlog") +public class SysOperlogController extends BaseController { + @Autowired + private SysOperLogService operLogService; + + @PreAuthorize("@ss.hasPermi('monitor:operlog:list')") + @GetMapping("/list") + public TableDataInfo list(SysOperLog operLog) { + startPage(); + List list = operLogService.selectOperLogList(operLog); + return getDataTable(list); + } + + @Log(title = "操作日志", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('monitor:operlog:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysOperLog operLog) { + List list = operLogService.selectOperLogList(operLog); + ExcelUtil util = new ExcelUtil(SysOperLog.class); + util.exportExcel(response, list, "操作日志"); + } + + @Log(title = "操作日志", businessType = BusinessType.DELETE) + @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')") + @DeleteMapping("/{operIds}") + public AjaxResult remove(@PathVariable Long[] operIds) { + return toAjax(operLogService.deleteOperLogByIds(operIds)); + } + + @Log(title = "操作日志", businessType = BusinessType.CLEAN) + @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')") + @DeleteMapping("/clean") + public AjaxResult clean() { + operLogService.cleanOperLog(); + return AjaxResult.success(); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/monitor/SysUserOnlineController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/monitor/SysUserOnlineController.java new file mode 100644 index 000000000..f7a112583 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/monitor/SysUserOnlineController.java @@ -0,0 +1,75 @@ +package com.jsowell.web.controller.monitor; + +import com.jsowell.common.annotation.Log; +import com.jsowell.common.constant.CacheConstants; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.domain.model.LoginUser; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.core.redis.RedisCache; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.util.StringUtils; +import com.jsowell.system.domain.SysUserOnline; +import com.jsowell.system.service.SysUserOnlineService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * 在线用户监控 + * + * @author jsowell + */ +@RestController +@RequestMapping("/monitor/online") +public class SysUserOnlineController extends BaseController { + @Autowired + private SysUserOnlineService userOnlineService; + + @Autowired + private RedisCache redisCache; + + @PreAuthorize("@ss.hasPermi('monitor:online:list')") + @GetMapping("/list") + public TableDataInfo list(String ipaddr, String userName) { + Collection keys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*"); + List userOnlineList = new ArrayList(); + for (String key : keys) { + LoginUser user = redisCache.getCacheObject(key); + if (StringUtils.isNotEmpty(ipaddr) && StringUtils.isNotEmpty(userName)) { + if (StringUtils.equals(ipaddr, user.getIpaddr()) && StringUtils.equals(userName, user.getUsername())) { + userOnlineList.add(userOnlineService.selectOnlineByInfo(ipaddr, userName, user)); + } + } else if (StringUtils.isNotEmpty(ipaddr)) { + if (StringUtils.equals(ipaddr, user.getIpaddr())) { + userOnlineList.add(userOnlineService.selectOnlineByIpaddr(ipaddr, user)); + } + } else if (StringUtils.isNotEmpty(userName) && StringUtils.isNotNull(user.getUser())) { + if (StringUtils.equals(userName, user.getUsername())) { + userOnlineList.add(userOnlineService.selectOnlineByUserName(userName, user)); + } + } else { + userOnlineList.add(userOnlineService.loginUserToUserOnline(user)); + } + } + Collections.reverse(userOnlineList); + userOnlineList.removeAll(Collections.singleton(null)); + return getDataTable(userOnlineList); + } + + /** + * 强退用户 + */ + @PreAuthorize("@ss.hasPermi('monitor:online:forceLogout')") + @Log(title = "在线用户", businessType = BusinessType.FORCE) + @DeleteMapping("/{tokenId}") + public AjaxResult forceLogout(@PathVariable String tokenId) { + redisCache.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + tokenId); + return AjaxResult.success(); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/MemberBasicInfoController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/MemberBasicInfoController.java new file mode 100644 index 000000000..938958346 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/MemberBasicInfoController.java @@ -0,0 +1,159 @@ +package com.jsowell.web.controller.pile; + +import com.github.pagehelper.PageHelper; +import com.google.common.collect.Lists; +import com.jsowell.common.annotation.Log; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.enums.uniapp.BalanceChangesEnum; +import com.jsowell.common.util.StringUtils; +import com.jsowell.pile.domain.MemberBasicInfo; +import com.jsowell.pile.dto.UniAppQueryMemberBalanceDTO; +import com.jsowell.pile.service.IMemberBasicInfoService; +import com.jsowell.pile.service.IMemberTransactionRecordService; +import com.jsowell.pile.vo.uniapp.MemberVO; +import com.jsowell.pile.vo.uniapp.MemberWalletLogVO; +import com.jsowell.pile.vo.web.MemberTransactionVO; +import com.jsowell.pile.vo.web.UpdateMemberBalanceDTO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 会员基础信息Controller + * + * @author jsowell + * @date 2022-10-12 + */ +@RestController +@RequestMapping("/member/info") +public class MemberBasicInfoController extends BaseController { + @Autowired + private IMemberBasicInfoService memberBasicInfoService; + + @Autowired + private IMemberTransactionRecordService memberTransactionRecordService; + + /** + * 查询会员基础信息列表 + */ + @PreAuthorize("@ss.hasPermi('member:info:list')") + @GetMapping("/list") + public TableDataInfo list(MemberBasicInfo memberBasicInfo) { + startPage(); + List list = memberBasicInfoService.selectMemberList(memberBasicInfo.getMobileNumber(), memberBasicInfo.getNickName()); + return getDataTable(list); + } + + /** + * 导出会员基础信息列表 + */ + /*@PreAuthorize("@ss.hasPermi('member:info:export')") + @Log(title = "会员基础信息", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, MemberBasicInfo memberBasicInfo) { + List list = memberBasicInfoService.selectMemberBasicInfoList(memberBasicInfo); + ExcelUtil util = new ExcelUtil(MemberBasicInfo.class); + util.exportExcel(response, list, "会员基础信息数据"); + }*/ + + /** + * 获取会员基础信息详细信息 + */ + @PreAuthorize("@ss.hasPermi('member:info:query')") + @GetMapping(value = "/{memberId}") + public AjaxResult getInfo(@PathVariable("memberId") String memberId) { + return AjaxResult.success(memberBasicInfoService.queryMemberInfoByMemberId(memberId)); + } + + @PreAuthorize("@ss.hasPermi('member:info:query')") + @GetMapping(value = "/getMemberPersonPileInfo/{memberId}") + public AjaxResult getMemberPersonPileInfo(@PathVariable("memberId") String memberId){ + return AjaxResult.success(memberBasicInfoService.getMemberPersonPileInfo(memberId)); + } + + /** + * 新增会员基础信息 + */ + @PreAuthorize("@ss.hasPermi('member:info:add')") + @Log(title = "会员基础信息", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody MemberBasicInfo memberBasicInfo) { + return toAjax(memberBasicInfoService.insertMemberBasicInfo(memberBasicInfo)); + } + + /** + * 修改会员基础信息 + */ + @PreAuthorize("@ss.hasPermi('member:info:edit')") + @Log(title = "会员基础信息", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody MemberBasicInfo memberBasicInfo) { + return toAjax(memberBasicInfoService.updateMemberBasicInfo(memberBasicInfo)); + } + + /** + * 删除会员基础信息 + */ + @PreAuthorize("@ss.hasPermi('member:info:remove')") + @Log(title = "会员基础信息", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Integer[] ids) { + return toAjax(memberBasicInfoService.deleteMemberBasicInfoByIds(Lists.newArrayList(ids))); + } + + /** + * 充值/扣款余额 + */ + @PreAuthorize("@ss.hasPermi('member:balance:update')") + @Log(title = "会员充值/扣款余额", businessType = BusinessType.UPDATE) + @PutMapping("/updateGiftBalance") + public AjaxResult updateGiftBalance(@RequestBody UpdateMemberBalanceDTO dto) { + logger.info("后管充值/扣款余额 param:{}", dto.toString()); + // 判断入参 + return toAjax(memberBasicInfoService.updateMemberBalance(dto)); + } + + /** + * 查询会员钱包流水 + */ + @PostMapping("/queryMemberBalanceChanges") + public TableDataInfo queryMemberBalanceChanges(@RequestBody UniAppQueryMemberBalanceDTO dto) { + String type = dto.getType(); + if (!StringUtils.equals("1", type) && !StringUtils.equals("2", type)) { + type = ""; + } + PageHelper.startPage(dto.getPageNum(), dto.getPageSize()); + List list = memberBasicInfoService.getMemberBalanceChanges(dto.getMemberId(), type); + for (MemberWalletLogVO walletLogVO : list) { + String subType = walletLogVO.getSubType(); + String subTypeValue = BalanceChangesEnum.getValueByCode(subType); + if (StringUtils.isNotBlank(subTypeValue)) { + walletLogVO.setSubType(subTypeValue); + } + } + return getDataTable(list); + } + + /** + * 查询会员交易流水 + * IMemberTransactionRecordService + */ + @PostMapping("/selectMemberTransactionRecordList") + public TableDataInfo selectMemberTransactionRecordList(@RequestBody UniAppQueryMemberBalanceDTO dto) { + startPage(); + List list = memberTransactionRecordService.selectMemberTransactionRecordList(dto.getMemberId()); + return getDataTable(list); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/OrderAbnormalRecordController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/OrderAbnormalRecordController.java new file mode 100644 index 000000000..7baa776e6 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/OrderAbnormalRecordController.java @@ -0,0 +1,98 @@ +package com.jsowell.web.controller.pile; + +import com.jsowell.common.annotation.Log; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.util.poi.ExcelUtil; +import com.jsowell.pile.domain.OrderAbnormalRecord; +import com.jsowell.pile.service.IOrderAbnormalRecordService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 订单异常记录Controller + * + * @author jsowell + * @date 2023-02-13 + */ +@RestController +@RequestMapping("/order/abnormalRecord") +public class OrderAbnormalRecordController extends BaseController { + @Autowired + private IOrderAbnormalRecordService orderAbnormalRecordService; + + /** + * 查询订单异常记录列表 + */ + @PreAuthorize("@ss.hasPermi('order:abnormalRecord:list')") + @GetMapping("/list") + public TableDataInfo list(OrderAbnormalRecord orderAbnormalRecord) { + startPage(); + List list = orderAbnormalRecordService.selectOrderAbnormalRecordList(orderAbnormalRecord); + return getDataTable(list); + } + + /** + * 导出订单异常记录列表 + */ + @PreAuthorize("@ss.hasPermi('order:abnormalRecord:export')") + @Log(title = "订单异常记录", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, OrderAbnormalRecord orderAbnormalRecord) { + List list = orderAbnormalRecordService.selectOrderAbnormalRecordList(orderAbnormalRecord); + ExcelUtil util = new ExcelUtil(OrderAbnormalRecord.class); + util.exportExcel(response, list, "订单异常记录数据"); + } + + /** + * 获取订单异常记录详细信息 + */ + @PreAuthorize("@ss.hasPermi('order:abnormalRecord:query')") + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") Integer id) { + return AjaxResult.success(orderAbnormalRecordService.selectOrderAbnormalRecordById(id)); + } + + /** + * 新增订单异常记录 + */ + @PreAuthorize("@ss.hasPermi('order:abnormalRecord:add')") + @Log(title = "订单异常记录", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody OrderAbnormalRecord orderAbnormalRecord) { + return toAjax(orderAbnormalRecordService.insertOrderAbnormalRecord(orderAbnormalRecord)); + } + + /** + * 修改订单异常记录 + */ + @PreAuthorize("@ss.hasPermi('order:abnormalRecord:edit')") + @Log(title = "订单异常记录", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody OrderAbnormalRecord orderAbnormalRecord) { + return toAjax(orderAbnormalRecordService.updateOrderAbnormalRecord(orderAbnormalRecord)); + } + + /** + * 删除订单异常记录 + */ + @PreAuthorize("@ss.hasPermi('order:abnormalRecord:remove')") + @Log(title = "订单异常记录", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Integer[] ids) { + return toAjax(orderAbnormalRecordService.deleteOrderAbnormalRecordByIds(ids)); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/OrderBasicInfoController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/OrderBasicInfoController.java new file mode 100644 index 000000000..458a4dbc8 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/OrderBasicInfoController.java @@ -0,0 +1,104 @@ +package com.jsowell.web.controller.pile; + +import com.jsowell.common.annotation.Log; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.util.poi.ExcelUtil; +import com.jsowell.pile.domain.OrderBasicInfo; +import com.jsowell.pile.dto.QueryOrderDTO; +import com.jsowell.pile.service.IOrderBasicInfoService; +import com.jsowell.pile.vo.web.OrderListVO; +import com.jsowell.service.OrderService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 订单Controller + * + * @author jsowell + * @date 2022-09-30 + */ +@RestController +@RequestMapping("/order") +public class OrderBasicInfoController extends BaseController { + @Autowired + private IOrderBasicInfoService orderBasicInfoService; + + @Autowired + private OrderService orderService; + + /** + * 查询订单列表 + */ + @PreAuthorize("@ss.hasPermi('order:order:list')") + @GetMapping("/order/list") + public TableDataInfo list(QueryOrderDTO orderBasicInfo) { + startPage(); + List list = orderBasicInfoService.selectOrderBasicInfoList(orderBasicInfo); + return getDataTable(list); + } + + /** + * 根据参数查询用电度数和消费金额 + */ + @PreAuthorize("@ss.hasPermi('order:order:list')") + @GetMapping("/order/totalData") + public AjaxResult getOrderTotalData(QueryOrderDTO orderBasicInfo) { + return AjaxResult.success(orderBasicInfoService.getOrderTotalData(orderBasicInfo)); + } + + /** + * 导出订单列表 + */ + @PreAuthorize("@ss.hasPermi('order:order:export')") + @Log(title = "订单", businessType = BusinessType.EXPORT) + @PostMapping("/order/export") + public void export(HttpServletResponse response, QueryOrderDTO orderBasicInfo) { + List list = orderBasicInfoService.selectOrderBasicInfoList(orderBasicInfo); + ExcelUtil util = new ExcelUtil(OrderListVO.class); + util.exportExcel(response, list, "订单数据"); + } + + /** + * 获取订单详细信息 + * http://localhost:8080/order/orderDetail/88000000000001012211161342359448 + */ + @PreAuthorize("@ss.hasPermi('order:order:query')") + @GetMapping(value = "/orderDetail/{orderCode}") + public AjaxResult getInfo(@PathVariable("orderCode") String orderCode) { + return AjaxResult.success(orderService.queryOrderDetailInfo(orderCode)); + } + + /** + * 修改订单 后管调用 + */ + @PreAuthorize("@ss.hasPermi('order:order:edit')") + @Log(title = "订单", businessType = BusinessType.UPDATE) + @PutMapping("/order") + public AjaxResult edit(@RequestBody OrderBasicInfo orderBasicInfo) { + return toAjax(orderBasicInfoService.updateOrderBasicInfo(orderBasicInfo)); + } + + /** + * 删除订单 + */ + @PreAuthorize("@ss.hasPermi('order:order:remove')") + @Log(title = "订单", businessType = BusinessType.DELETE) + @DeleteMapping("/order/{ids}") + public AjaxResult remove(@PathVariable Long[] ids) { + return toAjax(orderBasicInfoService.deleteOrderBasicInfoByIds(ids)); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileBasicInfoController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileBasicInfoController.java new file mode 100644 index 000000000..47bd1abef --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileBasicInfoController.java @@ -0,0 +1,163 @@ +package com.jsowell.web.controller.pile; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.annotation.Log; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.poi.ExcelUtil; +import com.jsowell.pile.domain.PileBasicInfo; +import com.jsowell.pile.dto.BatchCreatePileDTO; +import com.jsowell.pile.dto.QueryPileDTO; +import com.jsowell.pile.dto.ReplaceMerchantStationDTO; +import com.jsowell.pile.service.IPileBasicInfoService; +import com.jsowell.pile.service.IPileMsgRecordService; +import com.jsowell.pile.vo.web.PileDetailVO; +import com.jsowell.service.PileService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 设备管理Controller + * + * @author jsowell + * @date 2022-08-26 + */ +@RestController +@RequestMapping("/pile/basic") +public class PileBasicInfoController extends BaseController { + @Autowired + private IPileBasicInfoService pileBasicInfoService; + + @Autowired + private PileService pileService; + + @Autowired + private IPileMsgRecordService pileMsgRecordService; + /** + * 查询设备管理列表 + */ + @PreAuthorize("@ss.hasPermi('pile:basic:list')") + @GetMapping("/list") + public TableDataInfo list(QueryPileDTO queryPileDTO) { + // startPage(); // 在方法中分页 + List list = pileBasicInfoService.queryPileInfos(queryPileDTO); + return getDataTable(list); + } + + /** + * 导出设备管理列表 + */ + @PreAuthorize("@ss.hasPermi('pile:basic:export')") + @Log(title = "设备管理", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, PileBasicInfo pileBasicInfo) { + List list = pileBasicInfoService.selectPileBasicInfoList(pileBasicInfo); + ExcelUtil util = new ExcelUtil(PileBasicInfo.class); + util.exportExcel(response, list, "设备管理数据"); + } + + /** + * 获取设备管理详细信息 + */ + @PreAuthorize("@ss.hasPermi('pile:basic:query')") + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") Long id) { + return AjaxResult.success(pileBasicInfoService.selectPileBasicInfoById(id)); + } + + /** + * 新增设备管理 + */ + @PreAuthorize("@ss.hasPermi('pile:basic:add')") + @Log(title = "设备管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody PileBasicInfo pileBasicInfo) { + return toAjax(pileBasicInfoService.insertPileBasicInfo(pileBasicInfo)); + } + + /** + * 批量生成充电桩 + * @param dto + * @return + */ + @PreAuthorize("@ss.hasPermi('pile:basic:add')") + @Log(title = "设备管理", businessType = BusinessType.INSERT) + @PostMapping("/batchAdd") + public AjaxResult batchAdd(@RequestBody BatchCreatePileDTO dto) { + logger.info("批量生成充电桩 param:{}", JSONObject.toJSONString(dto)); + return toAjax(pileService.batchCreatePile(dto)); + } + + /** + * 修改设备管理 + */ + @PreAuthorize("@ss.hasPermi('pile:basic:edit')") + @Log(title = "设备管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody PileBasicInfo pileBasicInfo) { + return toAjax(pileBasicInfoService.updatePileBasicInfo(pileBasicInfo)); + } + + /** + * 删除设备管理 + */ + @PreAuthorize("@ss.hasPermi('pile:basic:remove')") + @Log(title = "设备管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Long[] ids) { + return toAjax(pileBasicInfoService.deletePileBasicInfoByIds(ids)); + } + + /** + * 批量更新充电桩 + * 运营商 站点 + */ + @PostMapping("/batchUpdatePileList") + public AjaxResult batchUpdatePileList( @RequestBody ReplaceMerchantStationDTO dto) { + logger.info("批量更新充电桩 param:{}", JSONObject.toJSONString(dto)); + return toAjax(pileBasicInfoService.replaceMerchantStationByPileIds(dto)); + } + + /** + * 查询充电桩详情 + * @return + */ + @PostMapping("/getPileDetailById") + public AjaxResult getPileDetailById(@RequestBody QueryPileDTO queryPileDTO) { + logger.info("查询充电桩详情 param:{}", JSONObject.toJSONString(queryPileDTO)); + if (StringUtils.isBlank(queryPileDTO.getPileId())) { + return AjaxResult.error("充电桩id不能为空"); + } + return AjaxResult.success(pileBasicInfoService.selectBasicInfoById(Long.valueOf(queryPileDTO.getPileId()))); + } + + /** + * 查询充电桩通讯日志 + * + * @param dto + * @return + */ + @PostMapping("/getPileFeedList") + public AjaxResult getPileFeedList(@RequestBody QueryPileDTO dto) { + // logger.info("查询充电桩通信日志 param:{}", dto.getPileSn()); + if (StringUtils.isBlank(dto.getPileSn())) { + return AjaxResult.error("充电桩Sn不能为空"); + } + return AjaxResult.success(pileMsgRecordService.getPileFeedList(dto)); + } + +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileBillingTemplateController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileBillingTemplateController.java new file mode 100644 index 000000000..991c473a0 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileBillingTemplateController.java @@ -0,0 +1,167 @@ +package com.jsowell.web.controller.pile; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.annotation.Log; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.util.poi.ExcelUtil; +import com.jsowell.pile.domain.PileBillingTemplate; +import com.jsowell.pile.dto.CreateOrUpdateBillingTemplateDTO; +import com.jsowell.pile.dto.ImportBillingTemplateDTO; +import com.jsowell.pile.dto.PublishBillingTemplateDTO; +import com.jsowell.pile.service.IPileBillingTemplateService; +import com.jsowell.pile.vo.web.BillingTemplateVO; +import com.jsowell.service.PileRemoteService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 计费模板Controller + * + * @author jsowell + * @date 2022-09-20 + */ +@Api(value = "计费模板Controller", tags = { "计费模板" }) +@RestController +@RequestMapping("/billing/template") +public class PileBillingTemplateController extends BaseController { + @Autowired + private IPileBillingTemplateService pileBillingTemplateService; + + @Autowired + private PileRemoteService pileRemoteService; + + /** + * 查询计费模板列表 + */ + @PreAuthorize("@ss.hasPermi('billing:template:list')") + @GetMapping("/list") + public TableDataInfo list(PileBillingTemplate pileBillingTemplate) { + startPage(); + // 只查询公共的 + pileBillingTemplate.setPublicFlag(Constants.ONE); + List list = pileBillingTemplateService.selectPileBillingTemplateList(pileBillingTemplate); + return getDataTable(list); + } + + /** + * 导出计费模板列表 + */ + @PreAuthorize("@ss.hasPermi('billing:template:export')") + @Log(title = "计费模板", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, PileBillingTemplate pileBillingTemplate) { + List list = pileBillingTemplateService.selectPileBillingTemplateList(pileBillingTemplate); + ExcelUtil util = new ExcelUtil(PileBillingTemplate.class); + util.exportExcel(response, list, "计费模板数据"); + } + + /** + * 获取计费模板详细信息 + */ + @PreAuthorize("@ss.hasPermi('billing:template:query')") + @GetMapping(value = "/{id}") + public AjaxResult getInfoById(@PathVariable("id") Long id) { + return AjaxResult.success(pileBillingTemplateService.queryPileBillingTemplateById(id)); + } + + /** + * 修改计费模板 + */ + @PreAuthorize("@ss.hasPermi('billing:template:edit')") + @Log(title = "计费模板", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody PileBillingTemplate pileBillingTemplate) { + return toAjax(pileBillingTemplateService.updatePileBillingTemplate(pileBillingTemplate)); + } + + /** + * 删除计费模板 + */ + @PreAuthorize("@ss.hasPermi('billing:template:remove')") + @Log(title = "计费模板", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Long[] ids) { + return toAjax(pileBillingTemplateService.deletePileBillingTemplateByIds(ids)); + } + + /** + * 新增计费模板 + * http://localhost:8080/billing/template/createBillingTemplate + */ + @ApiOperation("新增计费模板") + @PreAuthorize("@ss.hasPermi('billing:template:add')") + @Log(title = "计费模板", businessType = BusinessType.INSERT) + @PostMapping("/createBillingTemplate") + public AjaxResult createBillingTemplate(@RequestBody CreateOrUpdateBillingTemplateDTO dto) { + logger.info("新增计费模板 param:{}", JSONObject.toJSONString(dto)); + pileBillingTemplateService.createBillingTemplate(dto); + return AjaxResult.success(); + } + + /** + * 更新计费模板 前端 + * @param dto + * @return + */ + @PostMapping("/updateBillingTemplate") + public AjaxResult updateBillingTemplate(@RequestBody CreateOrUpdateBillingTemplateDTO dto) { + logger.info("修改计费模板 param:{}", JSONObject.toJSONString(dto)); + pileBillingTemplateService.updateBillingTemplate(dto); + return AjaxResult.success(); + } + + /** + * 查询公共计费模板 + */ + @GetMapping("/queryPublicBillingTemplateList") + public TableDataInfo queryPublicBillingTemplateList() { + List list = pileBillingTemplateService.queryPublicBillingTemplateList(); + return getDataTable(list); + } + + /** + * 站点导入计费模板接口 + * http://localhost:8080/billing/template/stationImportBillingTemplate + */ + @PostMapping("/stationImportBillingTemplate") + public AjaxResult stationImportBillingTemplate(@RequestBody ImportBillingTemplateDTO dto) { + logger.info("站点导入计费模板 param:{}", JSONObject.toJSONString(dto)); + return toAjax(pileBillingTemplateService.stationImportBillingTemplate(dto)); + } + + /** + * 查询站点计费模板 + */ + @GetMapping("/queryStationBillingTemplateList/{stationId}") + public TableDataInfo queryStationBillingTemplateList(@PathVariable("stationId") String stationId) { + logger.info("查询站点计费模板 param:{}", stationId); + List list = pileBillingTemplateService.queryStationBillingTemplateList(stationId); + logger.info("查询站点计费模板 result:{}", JSONObject.toJSONString(list)); + return getDataTable(list); + } + + /** + * 发布计费模板 + */ + @PostMapping("/publishBillingTemplate") + public AjaxResult publishBillingTemplate(@RequestBody PublishBillingTemplateDTO dto) { + return toAjax(pileRemoteService.publishBillingTemplate(dto)); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileConnectorInfoController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileConnectorInfoController.java new file mode 100644 index 000000000..5e88c03b5 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileConnectorInfoController.java @@ -0,0 +1,113 @@ +package com.jsowell.web.controller.pile; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.annotation.Log; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.util.poi.ExcelUtil; +import com.jsowell.pile.domain.PileConnectorInfo; +import com.jsowell.pile.dto.QueryConnectorDTO; +import com.jsowell.pile.dto.QueryConnectorListDTO; +import com.jsowell.pile.service.IPileConnectorInfoService; +import com.jsowell.pile.vo.web.PileConnectorInfoVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 充电桩枪口信息Controller + * + * @author jsowell + * @date 2022-08-31 + */ +@RestController +@RequestMapping("/pile/connector") +public class PileConnectorInfoController extends BaseController { + @Autowired + private IPileConnectorInfoService pileConnectorInfoService; + + /** + * 查询充电桩枪口信息列表 + */ + @PreAuthorize("@ss.hasPermi('pile:connector:list')") + @GetMapping("/list") + public TableDataInfo list(QueryConnectorDTO queryConnectorDTO) { + startPage(); + List list = pileConnectorInfoService.getConnectorInfoListByParams(queryConnectorDTO); + return getDataTable(list); + } + + /** + * 导出充电桩枪口信息列表 + */ + @PreAuthorize("@ss.hasPermi('pile:connector:export')") + @Log(title = "充电桩枪口信息", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, PileConnectorInfo pileConnectorInfo) { + List list = pileConnectorInfoService.selectPileConnectorInfoList(pileConnectorInfo); + ExcelUtil util = new ExcelUtil(PileConnectorInfo.class); + util.exportExcel(response, list, "充电桩枪口信息数据"); + } + + /** + * 通过入参 查询接口列表 + * http://localhost:8080/pile/connector/getConnectorInfoListByParams?pileIds=1,2 + * 多id使用逗号拼接 + */ + @PostMapping("/getConnectorInfoListByParams") + public TableDataInfo getConnectorInfoListByParams(@RequestBody QueryConnectorListDTO dto) { + logger.info("查询接口列表 param:{}", JSONObject.toJSONString(dto)); + List list = pileConnectorInfoService.getConnectorInfoListByParams(dto); + logger.info("查询接口列表 result:{}", JSONObject.toJSONString(list)); + return getDataTable(list); + } + + /** + * 获取充电桩枪口信息详细信息 + */ + // @PreAuthorize("@ss.hasPermi('pile:connector:query')") + // @GetMapping(value = "/{id}") + // public AjaxResult getInfo(@PathVariable("id") Integer id) { + // return AjaxResult.success(pileConnectorInfoService.selectPileConnectorInfoById(id)); + // } + + /** + * 新增充电桩枪口信息 + */ + // @PreAuthorize("@ss.hasPermi('pile:connector:add')") + // @Log(title = "充电桩枪口信息", businessType = BusinessType.INSERT) + // @PostMapping + // public AjaxResult add(@RequestBody PileConnectorInfo pileConnectorInfo) { + // return toAjax(pileConnectorInfoService.insertPileConnectorInfo(pileConnectorInfo)); + // } + + /** + * 修改充电桩枪口信息 + */ + // @PreAuthorize("@ss.hasPermi('pile:connector:edit')") + // @Log(title = "充电桩枪口信息", businessType = BusinessType.UPDATE) + // @PutMapping + // public AjaxResult edit(@RequestBody PileConnectorInfo pileConnectorInfo) { + // return toAjax(pileConnectorInfoService.updatePileConnectorInfo(pileConnectorInfo)); + // } + + /** + * 删除充电桩枪口信息 + */ + // @PreAuthorize("@ss.hasPermi('pile:connector:remove')") + // @Log(title = "充电桩枪口信息", businessType = BusinessType.DELETE) + // @DeleteMapping("/{ids}") + // public AjaxResult remove(@PathVariable Integer[] ids) { + // return toAjax(pileConnectorInfoService.deletePileConnectorInfoByIds(ids)); + // } + + +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileLicenceInfoController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileLicenceInfoController.java new file mode 100644 index 000000000..714e6da88 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileLicenceInfoController.java @@ -0,0 +1,98 @@ +package com.jsowell.web.controller.pile; + +import com.jsowell.common.annotation.Log; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.util.poi.ExcelUtil; +import com.jsowell.pile.domain.PileLicenceInfo; +import com.jsowell.pile.service.IPileLicenceInfoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 充电桩证书信息Controller + * + * @author jsowell + * @date 2022-08-27 + */ +@RestController +@RequestMapping("/pile/licence") +public class PileLicenceInfoController extends BaseController +{ + @Autowired + private IPileLicenceInfoService pileLicenceInfoService; + + /** + * 查询充电桩证书信息列表 + */ + @PreAuthorize("@ss.hasPermi('pile:licence:list')") + @GetMapping("/list") + public TableDataInfo list(PileLicenceInfo pileLicenceInfo) + { + startPage(); + List list = pileLicenceInfoService.selectPileLicenceInfoList(pileLicenceInfo); + return getDataTable(list); + } + + /** + * 导出充电桩证书信息列表 + */ + @PreAuthorize("@ss.hasPermi('pile:licence:export')") + @Log(title = "充电桩证书信息", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, PileLicenceInfo pileLicenceInfo) + { + List list = pileLicenceInfoService.selectPileLicenceInfoList(pileLicenceInfo); + ExcelUtil util = new ExcelUtil(PileLicenceInfo.class); + util.exportExcel(response, list, "充电桩证书信息数据"); + } + + /** + * 获取充电桩证书信息详细信息 + */ + @PreAuthorize("@ss.hasPermi('pile:licence:query')") + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") Long id) + { + return AjaxResult.success(pileLicenceInfoService.selectPileLicenceInfoById(id)); + } + + /** + * 新增充电桩证书信息 + */ + @PreAuthorize("@ss.hasPermi('pile:licence:add')") + @Log(title = "充电桩证书信息", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody PileLicenceInfo pileLicenceInfo) + { + return toAjax(pileLicenceInfoService.insertPileLicenceInfo(pileLicenceInfo)); + } + + /** + * 修改充电桩证书信息 + */ + @PreAuthorize("@ss.hasPermi('pile:licence:edit')") + @Log(title = "充电桩证书信息", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody PileLicenceInfo pileLicenceInfo) + { + return toAjax(pileLicenceInfoService.updatePileLicenceInfo(pileLicenceInfo)); + } + + /** + * 删除充电桩证书信息 + */ + @PreAuthorize("@ss.hasPermi('pile:licence:remove')") + @Log(title = "充电桩证书信息", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Long[] ids) + { + return toAjax(pileLicenceInfoService.deletePileLicenceInfoByIds(ids)); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileMerchantInfoController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileMerchantInfoController.java new file mode 100644 index 000000000..9ad7e4e44 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileMerchantInfoController.java @@ -0,0 +1,103 @@ +package com.jsowell.web.controller.pile; + +import com.jsowell.common.annotation.Log; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.util.poi.ExcelUtil; +import com.jsowell.pile.domain.PileMerchantInfo; +import com.jsowell.pile.service.IPileMerchantInfoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 充电桩运营商信息Controller + * + * @author jsowell + * @date 2022-08-27 + */ +@RestController +@RequestMapping("/pile/merchant") +public class PileMerchantInfoController extends BaseController { + @Autowired + private IPileMerchantInfoService pileMerchantInfoService; + + /** + * 查询充电桩运营商信息列表 + */ + @PreAuthorize("@ss.hasPermi('pile:merchant:list')") + @GetMapping("/list") + public TableDataInfo list(PileMerchantInfo pileMerchantInfo) { + startPage(); + List list = pileMerchantInfoService.selectPileMerchantInfoList(pileMerchantInfo); + return getDataTable(list); + } + + /** + * 获取运营商列表 不分页 + * @param pileMerchantInfo + * @return + */ + @PreAuthorize("@ss.hasPermi('pile:merchant:list')") + @GetMapping("/getMerchantList") + public TableDataInfo getMerchantList(PileMerchantInfo pileMerchantInfo) { + List list = pileMerchantInfoService.selectPileMerchantInfoList(pileMerchantInfo); + return getDataTable(list); + } + + /** + * 导出充电桩运营商信息列表 + */ + @PreAuthorize("@ss.hasPermi('pile:merchant:export')") + @Log(title = "充电桩运营商信息", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, PileMerchantInfo pileMerchantInfo) { + List list = pileMerchantInfoService.selectPileMerchantInfoList(pileMerchantInfo); + ExcelUtil util = new ExcelUtil(PileMerchantInfo.class); + util.exportExcel(response, list, "充电桩运营商信息数据"); + } + + /** + * 获取充电桩运营商信息详细信息 + */ + @PreAuthorize("@ss.hasPermi('pile:merchant:query')") + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") Long id) { + return AjaxResult.success(pileMerchantInfoService.selectPileMerchantInfoById(id)); + } + + /** + * 新增充电桩运营商信息 + */ + @PreAuthorize("@ss.hasPermi('pile:merchant:add')") + @Log(title = "充电桩运营商信息", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody PileMerchantInfo pileMerchantInfo) { + return toAjax(pileMerchantInfoService.insertPileMerchantInfo(pileMerchantInfo)); + } + + /** + * 修改充电桩运营商信息 + */ + @PreAuthorize("@ss.hasPermi('pile:merchant:edit')") + @Log(title = "充电桩运营商信息", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody PileMerchantInfo pileMerchantInfo) { + return toAjax(pileMerchantInfoService.updatePileMerchantInfo(pileMerchantInfo)); + } + + /** + * 删除充电桩运营商信息 + */ + @PreAuthorize("@ss.hasPermi('pile:merchant:remove')") + @Log(title = "充电桩运营商信息", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Long[] ids) { + return toAjax(pileMerchantInfoService.deletePileMerchantInfoByIds(ids)); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileModelInfoController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileModelInfoController.java new file mode 100644 index 000000000..6ae56c102 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileModelInfoController.java @@ -0,0 +1,91 @@ +package com.jsowell.web.controller.pile; + +import com.jsowell.common.annotation.Log; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.util.poi.ExcelUtil; +import com.jsowell.pile.domain.PileModelInfo; +import com.jsowell.pile.service.IPileModelInfoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 充电桩型号信息Controller + * + * @author jsowell + * @date 2022-08-26 + */ +@RestController +@RequestMapping("/pile/model") +public class PileModelInfoController extends BaseController { + @Autowired + private IPileModelInfoService pileModelInfoService; + + /** + * 查询充电桩型号信息列表 + */ + // @PreAuthorize("@ss.hasPermi('pile:model:list')") + @GetMapping("/list") + public TableDataInfo list(PileModelInfo pileModelInfo) { + startPage(); + List list = pileModelInfoService.selectPileModelInfoList(pileModelInfo); + return getDataTable(list); + } + + /** + * 导出充电桩型号信息列表 + */ + @PreAuthorize("@ss.hasPermi('pile:model:export')") + @Log(title = "充电桩型号信息", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, PileModelInfo pileModelInfo) { + List list = pileModelInfoService.selectPileModelInfoList(pileModelInfo); + ExcelUtil util = new ExcelUtil(PileModelInfo.class); + util.exportExcel(response, list, "充电桩型号信息数据"); + } + + /** + * 获取充电桩型号信息详细信息 + */ + @PreAuthorize("@ss.hasPermi('pile:model:query')") + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") Long id) { + return AjaxResult.success(pileModelInfoService.selectPileModelInfoById(id)); + } + + /** + * 新增充电桩型号信息 + */ + @PreAuthorize("@ss.hasPermi('pile:model:add')") + @Log(title = "充电桩型号信息", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody PileModelInfo pileModelInfo) { + return toAjax(pileModelInfoService.insertPileModelInfo(pileModelInfo)); + } + + /** + * 修改充电桩型号信息 + */ + @PreAuthorize("@ss.hasPermi('pile:model:edit')") + @Log(title = "充电桩型号信息", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody PileModelInfo pileModelInfo) { + return toAjax(pileModelInfoService.updatePileModelInfo(pileModelInfo)); + } + + /** + * 删除充电桩型号信息 + */ + @PreAuthorize("@ss.hasPermi('pile:model:remove')") + @Log(title = "充电桩型号信息", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Long[] ids) { + return toAjax(pileModelInfoService.deletePileModelInfoByIds(ids)); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileRemoteController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileRemoteController.java new file mode 100644 index 000000000..94fef4183 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileRemoteController.java @@ -0,0 +1,109 @@ +package com.jsowell.web.controller.pile; + +import com.google.common.collect.ImmutableMap; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.pile.dto.GenerateOrderDTO; +import com.jsowell.pile.dto.QueryPileDTO; +import com.jsowell.service.OrderService; +import com.jsowell.service.PileRemoteService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 远程控制controller + */ +@RestController +@RequestMapping("/remote") +public class PileRemoteController { + + @Autowired + private PileRemoteService pileRemoteService; + + @Autowired + private OrderService orderService; + + /** + * 获取实时上传数据 + * http://localhost:8080/remote/getRealTimeMonitorData + * @return + */ + @PostMapping("/getRealTimeMonitorData") + public AjaxResult getRealTimeMonitorData(@RequestBody QueryPileDTO queryPileDTO) { + pileRemoteService.getRealTimeMonitorData(queryPileDTO.getPileSn(), queryPileDTO.getConnectorCode()); + return AjaxResult.success(); + } + + /** + * 远程重启 + * http://localhost:8080/remote/reboot + * @return + */ + @PostMapping("/reboot") + public AjaxResult reboot(@RequestBody QueryPileDTO queryPileDTO) { + pileRemoteService.reboot(queryPileDTO.getPileSn()); + return AjaxResult.success(); + } + + /** + * 后管-远程启动充电 + * http://localhost:8080/remote/remoteStartChargingForWeb + */ + @PostMapping("/remoteStartChargingForWeb") + public AjaxResult remoteStartCharging(@RequestBody GenerateOrderDTO dto) { + // pileRemoteService.remoteStartCharging(queryPileDTO.getPileSn(), queryPileDTO.getConnectorCode()); + // 生成订单并远程启动充电 + dto.setStartMode(Constants.ZERO); + String orderCode = orderService.generateOrder(dto); + return AjaxResult.success(ImmutableMap.of("orderCode", orderCode)); + } + + /** + * 远程停止充电 + * http://localhost:8080/remote/remoteStopCharging + * @param dto + * @return + */ + @PostMapping("/remoteStopCharging") + public AjaxResult remoteStopCharging(@RequestBody QueryPileDTO dto) { + pileRemoteService.remoteStopCharging(dto.getPileSn(), dto.getConnectorCode()); + return AjaxResult.success(); + } + + /** + * 远程下发二维码 + * http://localhost:8080/remote/issueQRCode + */ + @PostMapping("/issueQRCode") + public AjaxResult issueQRCode(@RequestBody QueryPileDTO queryPileDTO) { + pileRemoteService.issueQRCode(queryPileDTO.getPileSn()); + return AjaxResult.success(); + } + + /** + * 对时 + * http://localhost:8080/remote/proofreadTime + */ + @PostMapping("/proofreadTime") + public AjaxResult proofreadTime(@RequestBody QueryPileDTO queryPileDTO) { + pileRemoteService.proofreadTime(queryPileDTO.getPileSn()); + return AjaxResult.success(); + } + + /** + * 远程升级 + * http://localhost:8080/remote/updateFile + * @return + */ + @PostMapping("/updateFile") + public AjaxResult updateFile(@RequestBody QueryPileDTO queryPileDTO) { + pileRemoteService.updateFile(queryPileDTO.getPileSns()); + return AjaxResult.success(); + } + +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileSimInfoController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileSimInfoController.java new file mode 100644 index 000000000..3c37a5d21 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileSimInfoController.java @@ -0,0 +1,142 @@ +package com.jsowell.web.controller.pile; + +import com.jsowell.common.annotation.Log; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.exception.BusinessException; +import com.jsowell.common.util.poi.ExcelUtil; +import com.jsowell.pile.domain.PileSimInfo; +import com.jsowell.pile.dto.QuerySimInfoDTO; +import com.jsowell.pile.dto.SimRenewDTO; +import com.jsowell.pile.service.IPileSimInfoService; +import com.jsowell.pile.service.SimCardService; +import com.jsowell.pile.vo.web.SimCardInfoVO; +import com.jsowell.pile.vo.web.SimRenewResultVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 充电桩SIM卡信息Controller + * + * @author jsowell + * @date 2022-08-26 + */ +@RestController +@RequestMapping("/pile/sim") +public class PileSimInfoController extends BaseController { + @Autowired + private IPileSimInfoService pileSimInfoService; + + @Autowired + private SimCardService simCardService; + + /** + * 查询充电桩SIM卡信息列表 + */ + @PreAuthorize("@ss.hasPermi('pile:sim:list')") + @GetMapping("/list") + public TableDataInfo list(PileSimInfo pileSimInfo) { + startPage(); + List list = pileSimInfoService.selectPileSimInfoList(pileSimInfo); + + // List list = pileSimInfoService.getSimInfoList(pileSimInfo); + return getDataTable(list); + } + + @PreAuthorize("@ss.hasPermi('pile:sim:list')") + @PostMapping("/getSimInfo") + public TableDataInfo getSimInfo(QuerySimInfoDTO dto) { + startPage(); + List simInfoList = pileSimInfoService.getSimInfoList(dto); + return getDataTable(simInfoList); + } + /** + * 导出充电桩SIM卡信息列表 + */ + @PreAuthorize("@ss.hasPermi('pile:sim:export')") + @Log(title = "充电桩SIM卡信息", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, PileSimInfo pileSimInfo) { + List list = pileSimInfoService.selectPileSimInfoList(pileSimInfo); + ExcelUtil util = new ExcelUtil(PileSimInfo.class); + util.exportExcel(response, list, "充电桩SIM卡信息数据"); + } + + /** + * 获取充电桩SIM卡信息详细信息 + */ + @PreAuthorize("@ss.hasPermi('pile:sim:query')") + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") Long id) { + return AjaxResult.success(pileSimInfoService.selectPileSimInfoById(id)); + } + + /** + * 新增充电桩SIM卡信息 + */ + @PreAuthorize("@ss.hasPermi('pile:sim:add')") + @Log(title = "充电桩SIM卡信息", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody PileSimInfo pileSimInfo) { + return toAjax(pileSimInfoService.insertPileSimInfo(pileSimInfo)); + } + + /** + * 修改充电桩SIM卡信息 + */ + @PreAuthorize("@ss.hasPermi('pile:sim:edit')") + @Log(title = "充电桩SIM卡信息", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody PileSimInfo pileSimInfo) { + return toAjax(pileSimInfoService.updatePileSimInfo(pileSimInfo)); + } + + /** + * 删除充电桩SIM卡信息 + */ + @PreAuthorize("@ss.hasPermi('pile:sim:remove')") + @Log(title = "充电桩SIM卡信息", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Long[] ids) { + return toAjax(pileSimInfoService.deletePileSimInfoByIds(ids)); + } + + + /** + * 续费sim卡周期 + * @param dto + * @return + */ + @PreAuthorize("@ss.hasPermi('pile:sim:edit')") + @PostMapping("/simRenew") + public AjaxResult simRenew(@RequestBody SimRenewDTO dto) { + List list = simCardService.renewSimByLoop(dto.getIccIds(), dto.getCycleNumber()); + return AjaxResult.success(list); + + + // AjaxResult result; + // try { + // List list = simCardService.renewSimByLoop(dto.getIccIds(), dto.getCycleNumber()); + // result = AjaxResult. + // } catch (BusinessException e) { + // result = AjaxResult.error(Integer.parseInt(e.getCode()), e.getMessage()); + // } catch (Exception e) { + // result = AjaxResult.error(); + // } + // return result; + } + +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileStationInfoController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileStationInfoController.java new file mode 100644 index 000000000..2bba1aefe --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/PileStationInfoController.java @@ -0,0 +1,148 @@ +package com.jsowell.web.controller.pile; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.annotation.Log; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.exception.BusinessException; +import com.jsowell.common.util.poi.ExcelUtil; +import com.jsowell.pile.domain.PileStationInfo; +import com.jsowell.pile.dto.FastCreateStationDTO; +import com.jsowell.pile.dto.QueryStationDTO; +import com.jsowell.pile.service.IPileStationInfoService; +import com.jsowell.pile.vo.web.PileStationVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 充电站信息Controller + * + * @author jsowell + * @date 2022-08-30 + */ +@RestController +@RequestMapping("/pile/station") +public class PileStationInfoController extends BaseController { + @Autowired + private IPileStationInfoService pileStationInfoService; + + + /** + * 查询充电站信息列表NEW + */ + @PreAuthorize("@ss.hasPermi('pile:station:list')") + @GetMapping("/list") + public TableDataInfo list(QueryStationDTO queryStationDTO) { + startPage(); + // List list = pileStationInfoService.selectPileStationInfoList(pileStationInfo); + List list = pileStationInfoService.queryStationInfos(queryStationDTO); + return getDataTable(list); + } + + /** + * 快速建站接口 + */ + // @PreAuthorize("@ss.hasPermi('pile:station:add')") + @PostMapping("/fastCreateStation") + public AjaxResult fastCreateStation(@RequestBody FastCreateStationDTO dto) { + logger.info("快速建站接口 param:{}", JSONObject.toJSONString(dto)); + int i = 0; + try { + i = pileStationInfoService.fastCreateStation(dto); + } catch (BusinessException e) { + logger.warn("快速建站接口 warn", e); + } catch (Exception e) { + logger.error("快速建站接口 error", e); + } + return toAjax(i); + } + + + /** + * 查询充电站信息列表 + */ + /*@PreAuthorize("@ss.hasPermi('pile:station:list')") + @GetMapping("/list") + public TableDataInfo list(PileStationInfo pileStationInfo) { + startPage(); + List list = pileStationInfoService.selectPileStationInfoList(pileStationInfo); + return getDataTable(list); + }*/ + + /** + * 导出充电站信息列表 + */ + @PreAuthorize("@ss.hasPermi('pile:station:export')") + @Log(title = "充电站信息", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, PileStationInfo pileStationInfo) { + List list = pileStationInfoService.selectPileStationInfoList(pileStationInfo); + ExcelUtil util = new ExcelUtil(PileStationInfo.class); + util.exportExcel(response, list, "充电站信息数据"); + } + + /** + * 获取充电站信息详细信息 + */ + @PreAuthorize("@ss.hasPermi('pile:station:query')") + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") Long id) { + return AjaxResult.success(pileStationInfoService.selectPileStationInfoById(id)); + } + + /** + * 后管站点基本资料页面 + * @return + */ + @PreAuthorize("@ss.hasPermi('pile:station:query')") + @GetMapping(value = "/getStationInfo/{stationId}") + public AjaxResult getStationInfo(@PathVariable("stationId") String stationId) { + return AjaxResult.success(pileStationInfoService.getStationInfo(stationId)); + } + + /** + * 新增充电站信息 + */ + @PreAuthorize("@ss.hasPermi('pile:station:add')") + @Log(title = "充电站信息", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody PileStationInfo pileStationInfo) { + return toAjax(pileStationInfoService.insertPileStationInfo(pileStationInfo)); + } + + /** + * 修改充电站信息 + */ + @PreAuthorize("@ss.hasPermi('pile:station:edit')") + @Log(title = "充电站信息", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody PileStationInfo pileStationInfo) { + logger.info("修改充电站信息 param:{}", pileStationInfo.toString()); + return toAjax(pileStationInfoService.updatePileStationInfo(pileStationInfo)); + } + + /** + * 删除充电站信息 + */ + @PreAuthorize("@ss.hasPermi('pile:station:remove')") + @Log(title = "充电站信息", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Long[] ids) { + return toAjax(pileStationInfoService.deletePileStationInfoByIds(ids)); + } + + /** + * 根据运营商id获取充电站列表 + */ + @PreAuthorize("@ss.hasPermi('pile:station:query')") + @PostMapping(value = "/selectStationListByMerchantId") + public AjaxResult selectStationListByMerchantId(@RequestBody QueryStationDTO dto) { + return AjaxResult.success(pileStationInfoService.selectStationListByMerchantId(Long.valueOf(dto.getMerchantId()))); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysConfigController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysConfigController.java new file mode 100644 index 000000000..5200055d5 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysConfigController.java @@ -0,0 +1,124 @@ +package com.jsowell.web.controller.system; + +import com.jsowell.common.annotation.Log; +import com.jsowell.common.constant.UserConstants; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.util.poi.ExcelUtil; +import com.jsowell.system.domain.SysConfig; +import com.jsowell.system.service.SysConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 参数配置 信息操作处理 + * + * @author jsowell + */ +@RestController +@RequestMapping("/system/config") +public class SysConfigController extends BaseController { + @Autowired + private SysConfigService configService; + + /** + * 获取参数配置列表 + */ + @PreAuthorize("@ss.hasPermi('system:config:list')") + @GetMapping("/list") + public TableDataInfo list(SysConfig config) { + startPage(); + List list = configService.selectConfigList(config); + return getDataTable(list); + } + + @Log(title = "参数管理", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:config:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysConfig config) { + List list = configService.selectConfigList(config); + ExcelUtil util = new ExcelUtil(SysConfig.class); + util.exportExcel(response, list, "参数数据"); + } + + /** + * 根据参数编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:config:query')") + @GetMapping(value = "/{configId}") + public AjaxResult getInfo(@PathVariable Long configId) { + return AjaxResult.success(configService.selectConfigById(configId)); + } + + /** + * 根据参数键名查询参数值 + */ + @GetMapping(value = "/configKey/{configKey}") + public AjaxResult getConfigKey(@PathVariable String configKey) { + return AjaxResult.success(configService.selectConfigByKey(configKey)); + } + + /** + * 新增参数配置 + */ + @PreAuthorize("@ss.hasPermi('system:config:add')") + @Log(title = "参数管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysConfig config) { + if (UserConstants.NOT_UNIQUE.equals(configService.checkConfigKeyUnique(config))) { + return AjaxResult.error("新增参数'" + config.getConfigName() + "'失败,参数键名已存在"); + } + config.setCreateBy(getUsername()); + return toAjax(configService.insertConfig(config)); + } + + /** + * 修改参数配置 + */ + @PreAuthorize("@ss.hasPermi('system:config:edit')") + @Log(title = "参数管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysConfig config) { + if (UserConstants.NOT_UNIQUE.equals(configService.checkConfigKeyUnique(config))) { + return AjaxResult.error("修改参数'" + config.getConfigName() + "'失败,参数键名已存在"); + } + config.setUpdateBy(getUsername()); + return toAjax(configService.updateConfig(config)); + } + + /** + * 删除参数配置 + */ + @PreAuthorize("@ss.hasPermi('system:config:remove')") + @Log(title = "参数管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{configIds}") + public AjaxResult remove(@PathVariable Long[] configIds) { + configService.deleteConfigByIds(configIds); + return success(); + } + + /** + * 刷新参数缓存 + */ + @PreAuthorize("@ss.hasPermi('system:config:remove')") + @Log(title = "参数管理", businessType = BusinessType.CLEAN) + @DeleteMapping("/refreshCache") + public AjaxResult refreshCache() { + configService.resetConfigCache(); + return AjaxResult.success(); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysDeptController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysDeptController.java new file mode 100644 index 000000000..1fe2860b0 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysDeptController.java @@ -0,0 +1,140 @@ +package com.jsowell.web.controller.system; + +import com.jsowell.common.annotation.Log; +import com.jsowell.common.constant.UserConstants; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.domain.entity.SysDept; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.util.StringUtils; +import com.jsowell.system.service.SysDeptService; +import org.apache.commons.lang3.ArrayUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.Iterator; +import java.util.List; + +/** + * 部门信息 + * + * @author jsowell + */ +@RestController +@RequestMapping("/system/dept") +public class SysDeptController extends BaseController { + @Autowired + private SysDeptService deptService; + + /** + * 获取部门列表 + */ + @PreAuthorize("@ss.hasPermi('system:dept:list')") + @GetMapping("/list") + public AjaxResult list(SysDept dept) { + List depts = deptService.selectDeptList(dept); + return AjaxResult.success(depts); + } + + /** + * 查询部门列表(排除节点) + */ + @PreAuthorize("@ss.hasPermi('system:dept:list')") + @GetMapping("/list/exclude/{deptId}") + public AjaxResult excludeChild(@PathVariable(value = "deptId", required = false) Long deptId) { + List depts = deptService.selectDeptList(new SysDept()); + Iterator it = depts.iterator(); + while (it.hasNext()) { + SysDept d = (SysDept) it.next(); + if (d.getDeptId().intValue() == deptId + || ArrayUtils.contains(StringUtils.split(d.getAncestors(), ","), deptId + "")) { + it.remove(); + } + } + return AjaxResult.success(depts); + } + + /** + * 根据部门编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:dept:query')") + @GetMapping(value = "/{deptId}") + public AjaxResult getInfo(@PathVariable Long deptId) { + deptService.checkDeptDataScope(deptId); + return AjaxResult.success(deptService.selectDeptById(deptId)); + } + + /** + * 获取部门下拉树列表 + */ + @GetMapping("/treeselect") + public AjaxResult treeselect(SysDept dept) { + List depts = deptService.selectDeptList(dept); + return AjaxResult.success(deptService.buildDeptTreeSelect(depts)); + } + + /** + * 加载对应角色部门列表树 + */ + @GetMapping(value = "/roleDeptTreeselect/{roleId}") + public AjaxResult roleDeptTreeselect(@PathVariable("roleId") Long roleId) { + List depts = deptService.selectDeptList(new SysDept()); + AjaxResult ajax = AjaxResult.success(); + ajax.put("checkedKeys", deptService.selectDeptListByRoleId(roleId)); + ajax.put("depts", deptService.buildDeptTreeSelect(depts)); + return ajax; + } + + /** + * 新增部门 + */ + @PreAuthorize("@ss.hasPermi('system:dept:add')") + @Log(title = "部门管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysDept dept) { + if (UserConstants.NOT_UNIQUE.equals(deptService.checkDeptNameUnique(dept))) { + return AjaxResult.error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在"); + } + dept.setCreateBy(getUsername()); + return toAjax(deptService.insertDept(dept)); + } + + /** + * 修改部门 + */ + @PreAuthorize("@ss.hasPermi('system:dept:edit')") + @Log(title = "部门管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysDept dept) { + Long deptId = dept.getDeptId(); + deptService.checkDeptDataScope(deptId); + if (UserConstants.NOT_UNIQUE.equals(deptService.checkDeptNameUnique(dept))) { + return AjaxResult.error("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在"); + } else if (dept.getParentId().equals(deptId)) { + return AjaxResult.error("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己"); + } else if (StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus()) && deptService.selectNormalChildrenDeptById(deptId) > 0) { + return AjaxResult.error("该部门包含未停用的子部门!"); + } + dept.setUpdateBy(getUsername()); + return toAjax(deptService.updateDept(dept)); + } + + /** + * 删除部门 + */ + @PreAuthorize("@ss.hasPermi('system:dept:remove')") + @Log(title = "部门管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{deptId}") + public AjaxResult remove(@PathVariable Long deptId) { + if (deptService.hasChildByDeptId(deptId)) { + return AjaxResult.error("存在下级部门,不允许删除"); + } + if (deptService.checkDeptExistUser(deptId)) { + return AjaxResult.error("部门存在用户,不允许删除"); + } + deptService.checkDeptDataScope(deptId); + return toAjax(deptService.deleteDeptById(deptId)); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysDictDataController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysDictDataController.java new file mode 100644 index 000000000..92b0ef994 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysDictDataController.java @@ -0,0 +1,113 @@ +package com.jsowell.web.controller.system; + +import java.util.ArrayList; +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.jsowell.common.annotation.Log; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.domain.entity.SysDictData; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.poi.ExcelUtil; +import com.jsowell.system.service.SysDictDataService; +import com.jsowell.system.service.SysDictTypeService; + +/** + * 数据字典信息 + * + * @author jsowell + */ +@RestController +@RequestMapping("/system/dict/data") +public class SysDictDataController extends BaseController { + @Autowired + private SysDictDataService dictDataService; + + @Autowired + private SysDictTypeService dictTypeService; + + @PreAuthorize("@ss.hasPermi('system:dict:list')") + @GetMapping("/list") + public TableDataInfo list(SysDictData dictData) { + startPage(); + List list = dictDataService.selectDictDataList(dictData); + return getDataTable(list); + } + + @Log(title = "字典数据", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:dict:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysDictData dictData) { + List list = dictDataService.selectDictDataList(dictData); + ExcelUtil util = new ExcelUtil(SysDictData.class); + util.exportExcel(response, list, "字典数据"); + } + + /** + * 查询字典数据详细 + */ + @PreAuthorize("@ss.hasPermi('system:dict:query')") + @GetMapping(value = "/{dictCode}") + public AjaxResult getInfo(@PathVariable Long dictCode) { + return AjaxResult.success(dictDataService.selectDictDataById(dictCode)); + } + + /** + * 根据字典类型查询字典数据信息 + */ + @GetMapping(value = "/type/{dictType}") + public AjaxResult dictType(@PathVariable String dictType) { + List data = dictTypeService.selectDictDataByType(dictType); + if (StringUtils.isNull(data)) { + data = new ArrayList(); + } + return AjaxResult.success(data); + } + + /** + * 新增字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:add')") + @Log(title = "字典数据", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysDictData dict) { + dict.setCreateBy(getUsername()); + return toAjax(dictDataService.insertDictData(dict)); + } + + /** + * 修改保存字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:edit')") + @Log(title = "字典数据", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysDictData dict) { + dict.setUpdateBy(getUsername()); + return toAjax(dictDataService.updateDictData(dict)); + } + + /** + * 删除字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:remove')") + @Log(title = "字典类型", businessType = BusinessType.DELETE) + @DeleteMapping("/{dictCodes}") + public AjaxResult remove(@PathVariable Long[] dictCodes) { + dictDataService.deleteDictDataByIds(dictCodes); + return success(); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysDictTypeController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysDictTypeController.java new file mode 100644 index 000000000..b848f6b6b --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysDictTypeController.java @@ -0,0 +1,122 @@ +package com.jsowell.web.controller.system; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.jsowell.common.annotation.Log; +import com.jsowell.common.constant.UserConstants; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.domain.entity.SysDictType; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.util.poi.ExcelUtil; +import com.jsowell.system.service.SysDictTypeService; + +/** + * 数据字典信息 + * + * @author jsowell + */ +@RestController +@RequestMapping("/system/dict/type") +public class SysDictTypeController extends BaseController { + @Autowired + private SysDictTypeService dictTypeService; + + @PreAuthorize("@ss.hasPermi('system:dict:list')") + @GetMapping("/list") + public TableDataInfo list(SysDictType dictType) { + startPage(); + List list = dictTypeService.selectDictTypeList(dictType); + return getDataTable(list); + } + + @Log(title = "字典类型", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:dict:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysDictType dictType) { + List list = dictTypeService.selectDictTypeList(dictType); + ExcelUtil util = new ExcelUtil(SysDictType.class); + util.exportExcel(response, list, "字典类型"); + } + + /** + * 查询字典类型详细 + */ + @PreAuthorize("@ss.hasPermi('system:dict:query')") + @GetMapping(value = "/{dictId}") + public AjaxResult getInfo(@PathVariable Long dictId) { + return AjaxResult.success(dictTypeService.selectDictTypeById(dictId)); + } + + /** + * 新增字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:add')") + @Log(title = "字典类型", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysDictType dict) { + if (UserConstants.NOT_UNIQUE.equals(dictTypeService.checkDictTypeUnique(dict))) { + return AjaxResult.error("新增字典'" + dict.getDictName() + "'失败,字典类型已存在"); + } + dict.setCreateBy(getUsername()); + return toAjax(dictTypeService.insertDictType(dict)); + } + + /** + * 修改字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:edit')") + @Log(title = "字典类型", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysDictType dict) { + if (UserConstants.NOT_UNIQUE.equals(dictTypeService.checkDictTypeUnique(dict))) { + return AjaxResult.error("修改字典'" + dict.getDictName() + "'失败,字典类型已存在"); + } + dict.setUpdateBy(getUsername()); + return toAjax(dictTypeService.updateDictType(dict)); + } + + /** + * 删除字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:remove')") + @Log(title = "字典类型", businessType = BusinessType.DELETE) + @DeleteMapping("/{dictIds}") + public AjaxResult remove(@PathVariable Long[] dictIds) { + dictTypeService.deleteDictTypeByIds(dictIds); + return success(); + } + + /** + * 刷新字典缓存 + */ + @PreAuthorize("@ss.hasPermi('system:dict:remove')") + @Log(title = "字典类型", businessType = BusinessType.CLEAN) + @DeleteMapping("/refreshCache") + public AjaxResult refreshCache() { + dictTypeService.resetDictCache(); + return AjaxResult.success(); + } + + /** + * 获取字典选择框列表 + */ + @GetMapping("/optionselect") + public AjaxResult optionselect() { + List dictTypes = dictTypeService.selectDictTypeAll(); + return AjaxResult.success(dictTypes); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysIndexController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysIndexController.java new file mode 100644 index 000000000..60001e5e5 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysIndexController.java @@ -0,0 +1,29 @@ +package com.jsowell.web.controller.system; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.jsowell.common.config.JsowellConfig; +import com.jsowell.common.util.StringUtils; + +/** + * 首页 + * + * @author jsowell + */ +@RestController +public class SysIndexController { + /** + * 系统基础配置 + */ + @Autowired + private JsowellConfig jsowellConfig; + + /** + * 访问首页,提示语 + */ + @RequestMapping("/") + public String index() { + return StringUtils.format("欢迎使用{}后台管理框架,当前版本:v{},请通过前端地址访问。", jsowellConfig.getName(), jsowellConfig.getVersion()); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysLoginController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysLoginController.java new file mode 100644 index 000000000..f36f9f043 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysLoginController.java @@ -0,0 +1,90 @@ +package com.jsowell.web.controller.system; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.domain.entity.SysMenu; +import com.jsowell.common.core.domain.entity.SysUser; +import com.jsowell.common.core.domain.model.LoginBody; +import com.jsowell.common.util.SecurityUtils; +import com.jsowell.framework.web.service.SysLoginService; +import com.jsowell.framework.web.service.SysPermissionService; +import com.jsowell.system.service.SysMenuService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Set; + +/** + * 登录验证 + * + * @author jsowell + */ +@RestController +public class SysLoginController { + Logger logs = LoggerFactory.getLogger(this.getClass()); + @Autowired + private SysLoginService loginService; + + @Autowired + private SysMenuService menuService; + + @Autowired + private SysPermissionService permissionService; + + /** + * 登录方法 + * + * @param loginBody 登录信息 + * @return 结果 + */ + @PostMapping("/login") + public AjaxResult login(@RequestBody LoginBody loginBody) { + logs.info("登录param:{}", JSONObject.toJSONString(loginBody)); + AjaxResult ajax = AjaxResult.success(); + // 生成令牌 + String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(), + loginBody.getUuid()); + ajax.put(Constants.TOKEN, token); + logs.info("登录 result:{}", ajax.toString()); + return ajax; + } + + /** + * 获取用户信息 + * + * @return 用户信息 + */ + @GetMapping("getInfo") + public AjaxResult getInfo() { + SysUser user = SecurityUtils.getLoginUser().getUser(); + // 角色集合 + Set roles = permissionService.getRolePermission(user); + // 权限集合 + Set permissions = permissionService.getMenuPermission(user); + AjaxResult ajax = AjaxResult.success(); + ajax.put("user" , user); + ajax.put("roles" , roles); + ajax.put("permissions" , permissions); + return ajax; + } + + /** + * 获取路由信息 + * + * @return 路由信息 + */ + @GetMapping("getRouters") + public AjaxResult getRouters() { + Long userId = SecurityUtils.getUserId(); + List menus = menuService.selectMenuTreeByUserId(userId); + logs.info("获取路由信息userId:{}, menus:{}, 菜单数量:{}", userId, menus, menus.size()); + return AjaxResult.success(menuService.buildMenus(menus)); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysMenuController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysMenuController.java new file mode 100644 index 000000000..90e3dcf9f --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysMenuController.java @@ -0,0 +1,125 @@ +package com.jsowell.web.controller.system; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.jsowell.common.annotation.Log; +import com.jsowell.common.constant.UserConstants; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.domain.entity.SysMenu; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.util.StringUtils; +import com.jsowell.system.service.SysMenuService; + +/** + * 菜单信息 + * + * @author jsowell + */ +@RestController +@RequestMapping("/system/menu") +public class SysMenuController extends BaseController { + @Autowired + private SysMenuService menuService; + + /** + * 获取菜单列表 + */ + @PreAuthorize("@ss.hasPermi('system:menu:list')") + @GetMapping("/list") + public AjaxResult list(SysMenu menu) { + List menus = menuService.selectMenuList(menu, getUserId()); + return AjaxResult.success(menus); + } + + /** + * 根据菜单编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:menu:query')") + @GetMapping(value = "/{menuId}") + public AjaxResult getInfo(@PathVariable Long menuId) { + return AjaxResult.success(menuService.selectMenuById(menuId)); + } + + /** + * 获取菜单下拉树列表 + */ + @GetMapping("/treeselect") + public AjaxResult treeselect(SysMenu menu) { + List menus = menuService.selectMenuList(menu, getUserId()); + return AjaxResult.success(menuService.buildMenuTreeSelect(menus)); + } + + /** + * 加载对应角色菜单列表树 + */ + @GetMapping(value = "/roleMenuTreeselect/{roleId}") + public AjaxResult roleMenuTreeselect(@PathVariable("roleId") Long roleId) { + List menus = menuService.selectMenuList(getUserId()); + AjaxResult ajax = AjaxResult.success(); + ajax.put("checkedKeys", menuService.selectMenuListByRoleId(roleId)); + ajax.put("menus", menuService.buildMenuTreeSelect(menus)); + return ajax; + } + + /** + * 新增菜单 + */ + @PreAuthorize("@ss.hasPermi('system:menu:add')") + @Log(title = "菜单管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysMenu menu) { + if (UserConstants.NOT_UNIQUE.equals(menuService.checkMenuNameUnique(menu))) { + return AjaxResult.error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); + } else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.isHttp(menu.getPath())) { + return AjaxResult.error("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); + } + menu.setCreateBy(getUsername()); + return toAjax(menuService.insertMenu(menu)); + } + + /** + * 修改菜单 + */ + @PreAuthorize("@ss.hasPermi('system:menu:edit')") + @Log(title = "菜单管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysMenu menu) { + if (UserConstants.NOT_UNIQUE.equals(menuService.checkMenuNameUnique(menu))) { + return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); + } else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.isHttp(menu.getPath())) { + return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); + } else if (menu.getMenuId().equals(menu.getParentId())) { + return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己"); + } + menu.setUpdateBy(getUsername()); + return toAjax(menuService.updateMenu(menu)); + } + + /** + * 删除菜单 + */ + @PreAuthorize("@ss.hasPermi('system:menu:remove')") + @Log(title = "菜单管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{menuId}") + public AjaxResult remove(@PathVariable("menuId") Long menuId) { + if (menuService.hasChildByMenuId(menuId)) { + return AjaxResult.error("存在子菜单,不允许删除"); + } + if (menuService.checkMenuExistRole(menuId)) { + return AjaxResult.error("菜单已分配,不允许删除"); + } + return toAjax(menuService.deleteMenuById(menuId)); + } +} \ No newline at end of file diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysNoticeController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysNoticeController.java new file mode 100644 index 000000000..e9bdc130d --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysNoticeController.java @@ -0,0 +1,86 @@ +package com.jsowell.web.controller.system; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.jsowell.common.annotation.Log; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.system.domain.SysNotice; +import com.jsowell.system.service.SysNoticeService; + +/** + * 公告 信息操作处理 + * + * @author jsowell + */ +@RestController +@RequestMapping("/system/notice") +public class SysNoticeController extends BaseController { + @Autowired + private SysNoticeService noticeService; + + /** + * 获取通知公告列表 + */ + @PreAuthorize("@ss.hasPermi('system:notice:list')") + @GetMapping("/list") + public TableDataInfo list(SysNotice notice) { + startPage(); + List list = noticeService.selectNoticeList(notice); + return getDataTable(list); + } + + /** + * 根据通知公告编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:notice:query')") + @GetMapping(value = "/{noticeId}") + public AjaxResult getInfo(@PathVariable Long noticeId) { + return AjaxResult.success(noticeService.selectNoticeById(noticeId)); + } + + /** + * 新增通知公告 + */ + @PreAuthorize("@ss.hasPermi('system:notice:add')") + @Log(title = "通知公告", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysNotice notice) { + notice.setCreateBy(getUsername()); + return toAjax(noticeService.insertNotice(notice)); + } + + /** + * 修改通知公告 + */ + @PreAuthorize("@ss.hasPermi('system:notice:edit')") + @Log(title = "通知公告", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysNotice notice) { + notice.setUpdateBy(getUsername()); + return toAjax(noticeService.updateNotice(notice)); + } + + /** + * 删除通知公告 + */ + @PreAuthorize("@ss.hasPermi('system:notice:remove')") + @Log(title = "通知公告", businessType = BusinessType.DELETE) + @DeleteMapping("/{noticeIds}") + public AjaxResult remove(@PathVariable Long[] noticeIds) { + return toAjax(noticeService.deleteNoticeByIds(noticeIds)); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysPostController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysPostController.java new file mode 100644 index 000000000..9bf17a0c4 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysPostController.java @@ -0,0 +1,117 @@ +package com.jsowell.web.controller.system; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.jsowell.common.annotation.Log; +import com.jsowell.common.constant.UserConstants; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.util.poi.ExcelUtil; +import com.jsowell.system.domain.SysPost; +import com.jsowell.system.service.SysPostService; + +/** + * 岗位信息操作处理 + * + * @author jsowell + */ +@RestController +@RequestMapping("/system/post") +public class SysPostController extends BaseController { + @Autowired + private SysPostService postService; + + /** + * 获取岗位列表 + */ + @PreAuthorize("@ss.hasPermi('system:post:list')") + @GetMapping("/list") + public TableDataInfo list(SysPost post) { + startPage(); + List list = postService.selectPostList(post); + return getDataTable(list); + } + + @Log(title = "岗位管理", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:post:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysPost post) { + List list = postService.selectPostList(post); + ExcelUtil util = new ExcelUtil(SysPost.class); + util.exportExcel(response, list, "岗位数据"); + } + + /** + * 根据岗位编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:post:query')") + @GetMapping(value = "/{postId}") + public AjaxResult getInfo(@PathVariable Long postId) { + return AjaxResult.success(postService.selectPostById(postId)); + } + + /** + * 新增岗位 + */ + @PreAuthorize("@ss.hasPermi('system:post:add')") + @Log(title = "岗位管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysPost post) { + if (UserConstants.NOT_UNIQUE.equals(postService.checkPostNameUnique(post))) { + return AjaxResult.error("新增岗位'" + post.getPostName() + "'失败,岗位名称已存在"); + } else if (UserConstants.NOT_UNIQUE.equals(postService.checkPostCodeUnique(post))) { + return AjaxResult.error("新增岗位'" + post.getPostName() + "'失败,岗位编码已存在"); + } + post.setCreateBy(getUsername()); + return toAjax(postService.insertPost(post)); + } + + /** + * 修改岗位 + */ + @PreAuthorize("@ss.hasPermi('system:post:edit')") + @Log(title = "岗位管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysPost post) { + if (UserConstants.NOT_UNIQUE.equals(postService.checkPostNameUnique(post))) { + return AjaxResult.error("修改岗位'" + post.getPostName() + "'失败,岗位名称已存在"); + } else if (UserConstants.NOT_UNIQUE.equals(postService.checkPostCodeUnique(post))) { + return AjaxResult.error("修改岗位'" + post.getPostName() + "'失败,岗位编码已存在"); + } + post.setUpdateBy(getUsername()); + return toAjax(postService.updatePost(post)); + } + + /** + * 删除岗位 + */ + @PreAuthorize("@ss.hasPermi('system:post:remove')") + @Log(title = "岗位管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{postIds}") + public AjaxResult remove(@PathVariable Long[] postIds) { + return toAjax(postService.deletePostByIds(postIds)); + } + + /** + * 获取岗位选择框列表 + */ + @GetMapping("/optionselect") + public AjaxResult optionselect() { + List posts = postService.selectPostAll(); + return AjaxResult.success(posts); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysProfileController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysProfileController.java new file mode 100644 index 000000000..066d598ea --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysProfileController.java @@ -0,0 +1,131 @@ +package com.jsowell.web.controller.system; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import com.jsowell.common.annotation.Log; +import com.jsowell.common.config.JsowellConfig; +import com.jsowell.common.constant.UserConstants; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.domain.entity.SysUser; +import com.jsowell.common.core.domain.model.LoginUser; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.util.SecurityUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.file.FileUploadUtils; +import com.jsowell.common.util.file.MimeTypeUtils; +import com.jsowell.framework.web.service.TokenService; +import com.jsowell.system.service.SysUserService; + +/** + * 个人信息 业务处理 + * + * @author jsowell + */ +@RestController +@RequestMapping("/system/user/profile") +public class SysProfileController extends BaseController { + @Autowired + private SysUserService userService; + + @Autowired + private TokenService tokenService; + + /** + * 个人信息 + */ + @GetMapping + public AjaxResult profile() { + LoginUser loginUser = getLoginUser(); + SysUser user = loginUser.getUser(); + AjaxResult ajax = AjaxResult.success(user); + ajax.put("roleGroup", userService.selectUserRoleGroup(loginUser.getUsername())); + ajax.put("postGroup", userService.selectUserPostGroup(loginUser.getUsername())); + return ajax; + } + + /** + * 修改用户 + */ + @Log(title = "个人信息", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult updateProfile(@RequestBody SysUser user) { + LoginUser loginUser = getLoginUser(); + SysUser sysUser = loginUser.getUser(); + user.setUserName(sysUser.getUserName()); + if (StringUtils.isNotEmpty(user.getPhone()) + && UserConstants.NOT_UNIQUE.equals(userService.checkPhoneUnique(user))) { + return AjaxResult.error("修改用户'" + user.getUserName() + "'失败,手机号码已存在"); + } + if (StringUtils.isNotEmpty(user.getEmail()) + && UserConstants.NOT_UNIQUE.equals(userService.checkEmailUnique(user))) { + return AjaxResult.error("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在"); + } + user.setUserId(sysUser.getUserId()); + user.setPassword(null); + user.setAvatar(null); + user.setDeptId(null); + if (userService.updateUserProfile(user) > 0) { + // 更新缓存用户信息 + sysUser.setNickName(user.getNickName()); + sysUser.setPhone(user.getPhone()); + sysUser.setEmail(user.getEmail()); + sysUser.setSex(user.getSex()); + tokenService.setLoginUser(loginUser); + return AjaxResult.success(); + } + return AjaxResult.error("修改个人信息异常,请联系管理员"); + } + + /** + * 重置密码 + */ + @Log(title = "个人信息", businessType = BusinessType.UPDATE) + @PutMapping("/updatePwd") + public AjaxResult updatePwd(String oldPassword, String newPassword) { + LoginUser loginUser = getLoginUser(); + String userName = loginUser.getUsername(); + String password = loginUser.getPassword(); + if (!SecurityUtils.matchesPassword(oldPassword, password)) { + return AjaxResult.error("修改密码失败,旧密码错误"); + } + if (SecurityUtils.matchesPassword(newPassword, password)) { + return AjaxResult.error("新密码不能与旧密码相同"); + } + if (userService.resetUserPwd(userName, SecurityUtils.encryptPassword(newPassword)) > 0) { + // 更新缓存用户密码 + loginUser.getUser().setPassword(SecurityUtils.encryptPassword(newPassword)); + tokenService.setLoginUser(loginUser); + return AjaxResult.success(); + } + return AjaxResult.error("修改密码异常,请联系管理员"); + } + + /** + * 头像上传 + */ + @Log(title = "用户头像", businessType = BusinessType.UPDATE) + @PostMapping("/avatar") + public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) throws Exception { + if (!file.isEmpty()) { + LoginUser loginUser = getLoginUser(); + String avatar = FileUploadUtils.upload(JsowellConfig.getAvatarPath(), file, MimeTypeUtils.IMAGE_EXTENSION); + if (userService.updateUserAvatar(loginUser.getUsername(), avatar)) { + AjaxResult ajax = AjaxResult.success(); + ajax.put("imgUrl", avatar); + // 更新缓存用户头像 + loginUser.getUser().setAvatar(avatar); + tokenService.setLoginUser(loginUser); + return ajax; + } + } + return AjaxResult.error("上传图片异常,请联系管理员"); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysRegisterController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysRegisterController.java new file mode 100644 index 000000000..92617f352 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysRegisterController.java @@ -0,0 +1,35 @@ +package com.jsowell.web.controller.system; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.domain.model.RegisterBody; +import com.jsowell.common.util.StringUtils; +import com.jsowell.framework.web.service.SysRegisterService; +import com.jsowell.system.service.SysConfigService; + +/** + * 注册验证 + * + * @author jsowell + */ +@RestController +public class SysRegisterController extends BaseController { + @Autowired + private SysRegisterService registerService; + + @Autowired + private SysConfigService configService; + + @PostMapping("/register") + public AjaxResult register(@RequestBody RegisterBody user) { + if (!("true".equals(configService.selectConfigByKey("sys.account.registerUser")))) { + return error("当前系统没有开启注册功能!"); + } + String msg = registerService.register(user); + return StringUtils.isEmpty(msg) ? success() : error(msg); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysRoleController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysRoleController.java new file mode 100644 index 000000000..0bb385491 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysRoleController.java @@ -0,0 +1,223 @@ +package com.jsowell.web.controller.system; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.jsowell.common.annotation.Log; +import com.jsowell.common.constant.UserConstants; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.domain.entity.SysRole; +import com.jsowell.common.core.domain.entity.SysUser; +import com.jsowell.common.core.domain.model.LoginUser; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.poi.ExcelUtil; +import com.jsowell.framework.web.service.SysPermissionService; +import com.jsowell.framework.web.service.TokenService; +import com.jsowell.system.domain.SysUserRole; +import com.jsowell.system.service.SysRoleService; +import com.jsowell.system.service.SysUserService; + +/** + * 角色信息 + * + * @author jsowell + */ +@RestController +@RequestMapping("/system/role") +public class SysRoleController extends BaseController { + @Autowired + private SysRoleService roleService; + + @Autowired + private TokenService tokenService; + + @Autowired + private SysPermissionService permissionService; + + @Autowired + private SysUserService userService; + + @PreAuthorize("@ss.hasPermi('system:role:list')") + @GetMapping("/list") + public TableDataInfo list(SysRole role) { + startPage(); + List list = roleService.selectRoleList(role); + return getDataTable(list); + } + + @Log(title = "角色管理", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:role:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysRole role) { + List list = roleService.selectRoleList(role); + ExcelUtil util = new ExcelUtil(SysRole.class); + util.exportExcel(response, list, "角色数据"); + } + + /** + * 根据角色编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:role:query')") + @GetMapping(value = "/{roleId}") + public AjaxResult getInfo(@PathVariable Long roleId) { + roleService.checkRoleDataScope(roleId); + return AjaxResult.success(roleService.selectRoleById(roleId)); + } + + /** + * 新增角色 + */ + @PreAuthorize("@ss.hasPermi('system:role:add')") + @Log(title = "角色管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysRole role) { + if (UserConstants.NOT_UNIQUE.equals(roleService.checkRoleNameUnique(role))) { + return AjaxResult.error("新增角色'" + role.getRoleName() + "'失败,角色名称已存在"); + } else if (UserConstants.NOT_UNIQUE.equals(roleService.checkRoleKeyUnique(role))) { + return AjaxResult.error("新增角色'" + role.getRoleName() + "'失败,角色权限已存在"); + } + role.setCreateBy(getUsername()); + return toAjax(roleService.insertRole(role)); + + } + + /** + * 修改保存角色 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysRole role) { + roleService.checkRoleAllowed(role); + roleService.checkRoleDataScope(role.getRoleId()); + if (UserConstants.NOT_UNIQUE.equals(roleService.checkRoleNameUnique(role))) { + return AjaxResult.error("修改角色'" + role.getRoleName() + "'失败,角色名称已存在"); + } else if (UserConstants.NOT_UNIQUE.equals(roleService.checkRoleKeyUnique(role))) { + return AjaxResult.error("修改角色'" + role.getRoleName() + "'失败,角色权限已存在"); + } + role.setUpdateBy(getUsername()); + + if (roleService.updateRole(role) > 0) { + // 更新缓存用户权限 + LoginUser loginUser = getLoginUser(); + if (StringUtils.isNotNull(loginUser.getUser()) && !loginUser.getUser().isAdmin()) { + loginUser.setPermissions(permissionService.getMenuPermission(loginUser.getUser())); + loginUser.setUser(userService.selectUserByUserName(loginUser.getUser().getUserName())); + tokenService.setLoginUser(loginUser); + } + return AjaxResult.success(); + } + return AjaxResult.error("修改角色'" + role.getRoleName() + "'失败,请联系管理员"); + } + + /** + * 修改保存数据权限 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.UPDATE) + @PutMapping("/dataScope") + public AjaxResult dataScope(@RequestBody SysRole role) { + roleService.checkRoleAllowed(role); + roleService.checkRoleDataScope(role.getRoleId()); + return toAjax(roleService.authDataScope(role)); + } + + /** + * 状态修改 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.UPDATE) + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysRole role) { + roleService.checkRoleAllowed(role); + roleService.checkRoleDataScope(role.getRoleId()); + role.setUpdateBy(getUsername()); + return toAjax(roleService.updateRoleStatus(role)); + } + + /** + * 删除角色 + */ + @PreAuthorize("@ss.hasPermi('system:role:remove')") + @Log(title = "角色管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{roleIds}") + public AjaxResult remove(@PathVariable Long[] roleIds) { + return toAjax(roleService.deleteRoleByIds(roleIds)); + } + + /** + * 获取角色选择框列表 + */ + @PreAuthorize("@ss.hasPermi('system:role:query')") + @GetMapping("/optionselect") + public AjaxResult optionselect() { + return AjaxResult.success(roleService.selectRoleAll()); + } + + /** + * 查询已分配用户角色列表 + */ + @PreAuthorize("@ss.hasPermi('system:role:list')") + @GetMapping("/authUser/allocatedList") + public TableDataInfo allocatedList(SysUser user) { + startPage(); + List list = userService.selectAllocatedList(user); + return getDataTable(list); + } + + /** + * 查询未分配用户角色列表 + */ + @PreAuthorize("@ss.hasPermi('system:role:list')") + @GetMapping("/authUser/unallocatedList") + public TableDataInfo unallocatedList(SysUser user) { + startPage(); + List list = userService.selectUnallocatedList(user); + return getDataTable(list); + } + + /** + * 取消授权用户 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.GRANT) + @PutMapping("/authUser/cancel") + public AjaxResult cancelAuthUser(@RequestBody SysUserRole userRole) { + return toAjax(roleService.deleteAuthUser(userRole)); + } + + /** + * 批量取消授权用户 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.GRANT) + @PutMapping("/authUser/cancelAll") + public AjaxResult cancelAuthUserAll(Long roleId, Long[] userIds) { + return toAjax(roleService.deleteAuthUsers(roleId, userIds)); + } + + /** + * 批量选择用户授权 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.GRANT) + @PutMapping("/authUser/selectAll") + public AjaxResult selectAuthUserAll(Long roleId, Long[] userIds) { + roleService.checkRoleDataScope(roleId); + return toAjax(roleService.insertAuthUsers(roleId, userIds)); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysUserController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysUserController.java new file mode 100644 index 000000000..085786949 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysUserController.java @@ -0,0 +1,212 @@ +package com.jsowell.web.controller.system; + +import com.jsowell.common.annotation.Log; +import com.jsowell.common.constant.UserConstants; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.domain.entity.SysRole; +import com.jsowell.common.core.domain.entity.SysUser; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.util.SecurityUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.poi.ExcelUtil; +import com.jsowell.system.service.SysPostService; +import com.jsowell.system.service.SysRoleService; +import com.jsowell.system.service.SysUserService; +import org.apache.commons.lang3.ArrayUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 用户信息 + * + * @author jsowell + */ +@RestController +@RequestMapping("/system/user") +public class SysUserController extends BaseController { + @Autowired + private SysUserService userService; + + @Autowired + private SysRoleService roleService; + + @Autowired + private SysPostService postService; + + /** + * 获取用户列表 + */ + @PreAuthorize("@ss.hasPermi('system:user:list')") + @GetMapping("/list") + public TableDataInfo list(SysUser user) { + startPage(); + List list = userService.selectUserList(user); + return getDataTable(list); + } + + @Log(title = "用户管理", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:user:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysUser user) { + List list = userService.selectUserList(user); + ExcelUtil util = new ExcelUtil(SysUser.class); + util.exportExcel(response, list, "用户数据"); + } + + @Log(title = "用户管理", businessType = BusinessType.IMPORT) + @PreAuthorize("@ss.hasPermi('system:user:import')") + @PostMapping("/importData") + public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception { + ExcelUtil util = new ExcelUtil(SysUser.class); + List userList = util.importExcel(file.getInputStream()); + String operName = getUsername(); + String message = userService.importUser(userList, updateSupport, operName); + return AjaxResult.success(message); + } + + @PostMapping("/importTemplate") + public void importTemplate(HttpServletResponse response) { + ExcelUtil util = new ExcelUtil(SysUser.class); + util.importTemplateExcel(response, "用户数据"); + } + + /** + * 根据用户编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:user:query')") + @GetMapping(value = {"/", "/{userId}"}) + public AjaxResult getInfo(@PathVariable(value = "userId", required = false) Long userId) { + userService.checkUserDataScope(userId); + AjaxResult ajax = AjaxResult.success(); + List roles = roleService.selectRoleAll(); + ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList())); + ajax.put("posts", postService.selectPostAll()); + if (StringUtils.isNotNull(userId)) { + SysUser sysUser = userService.selectUserById(userId); + ajax.put(AjaxResult.DATA_TAG, sysUser); + ajax.put("postIds", postService.selectPostListByUserId(userId)); + ajax.put("roleIds", sysUser.getRoles().stream().map(SysRole::getRoleId).collect(Collectors.toList())); + } + return ajax; + } + + /** + * 新增用户 + */ + @PreAuthorize("@ss.hasPermi('system:user:add')") + @Log(title = "用户管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysUser user) { + if (UserConstants.NOT_UNIQUE.equals(userService.checkUserNameUnique(user.getUserName()))) { + return AjaxResult.error("新增用户'" + user.getUserName() + "'失败,登录账号已存在"); + } else if (StringUtils.isNotEmpty(user.getPhone()) + && UserConstants.NOT_UNIQUE.equals(userService.checkPhoneUnique(user))) { + return AjaxResult.error("新增用户'" + user.getUserName() + "'失败,手机号码已存在"); + } else if (StringUtils.isNotEmpty(user.getEmail()) + && UserConstants.NOT_UNIQUE.equals(userService.checkEmailUnique(user))) { + return AjaxResult.error("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在"); + } + user.setCreateBy(getUsername()); + user.setPassword(SecurityUtils.encryptPassword(user.getPassword())); + return toAjax(userService.insertUser(user)); + } + + /** + * 修改用户 + */ + @PreAuthorize("@ss.hasPermi('system:user:edit')") + @Log(title = "用户管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysUser user) { + userService.checkUserAllowed(user); + userService.checkUserDataScope(user.getUserId()); + if (StringUtils.isNotEmpty(user.getPhone()) + && UserConstants.NOT_UNIQUE.equals(userService.checkPhoneUnique(user))) { + return AjaxResult.error("修改用户'" + user.getUserName() + "'失败,手机号码已存在"); + } else if (StringUtils.isNotEmpty(user.getEmail()) + && UserConstants.NOT_UNIQUE.equals(userService.checkEmailUnique(user))) { + return AjaxResult.error("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在"); + } + user.setUpdateBy(getUsername()); + return toAjax(userService.updateUser(user)); + } + + /** + * 删除用户 + */ + @PreAuthorize("@ss.hasPermi('system:user:remove')") + @Log(title = "用户管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{userIds}") + public AjaxResult remove(@PathVariable Long[] userIds) { + if (ArrayUtils.contains(userIds, getUserId())) { + return error("当前用户不能删除"); + } + return toAjax(userService.deleteUserByIds(userIds)); + } + + /** + * 重置密码 + */ + @PreAuthorize("@ss.hasPermi('system:user:resetPwd')") + @Log(title = "用户管理", businessType = BusinessType.UPDATE) + @PutMapping("/resetPwd") + public AjaxResult resetPwd(@RequestBody SysUser user) { + userService.checkUserAllowed(user); + userService.checkUserDataScope(user.getUserId()); + user.setPassword(SecurityUtils.encryptPassword(user.getPassword())); + user.setUpdateBy(getUsername()); + return toAjax(userService.resetPwd(user)); + } + + /** + * 状态修改 + */ + @PreAuthorize("@ss.hasPermi('system:user:edit')") + @Log(title = "用户管理", businessType = BusinessType.UPDATE) + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysUser user) { + userService.checkUserAllowed(user); + userService.checkUserDataScope(user.getUserId()); + user.setUpdateBy(getUsername()); + return toAjax(userService.updateUserStatus(user)); + } + + /** + * 根据用户编号获取授权角色 + */ + @PreAuthorize("@ss.hasPermi('system:user:query')") + @GetMapping("/authRole/{userId}") + public AjaxResult authRole(@PathVariable("userId") Long userId) { + AjaxResult ajax = AjaxResult.success(); + SysUser user = userService.selectUserById(userId); + List roles = roleService.selectRolesByUserId(userId); + ajax.put("user", user); + ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList())); + return ajax; + } + + /** + * 用户授权角色 + */ + @PreAuthorize("@ss.hasPermi('system:user:edit')") + @Log(title = "用户管理", businessType = BusinessType.GRANT) + @PutMapping("/authRole") + public AjaxResult insertAuthRole(Long userId, Long[] roleIds) { + userService.checkUserDataScope(userId); + userService.insertUserAuth(userId, roleIds); + return success(); + } + + public static void main(String[] args) { + System.out.println(SecurityUtils.encryptPassword("admin123")); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/tool/SwaggerController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/tool/SwaggerController.java new file mode 100644 index 000000000..fafddca0a --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/tool/SwaggerController.java @@ -0,0 +1,23 @@ +package com.jsowell.web.controller.tool; + +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import com.jsowell.common.core.controller.BaseController; + +/** + * swagger 接口 + * + * @author jsowell + */ +@Controller +@RequestMapping("/tool/swagger") +public class SwaggerController extends BaseController { + + @PreAuthorize("@ss.hasPermi('tool:swagger:view')") + @GetMapping() + public String index() { + return redirect("/swagger-ui.html"); + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/tool/TestController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/tool/TestController.java new file mode 100644 index 000000000..a23dffe1f --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/tool/TestController.java @@ -0,0 +1,147 @@ +package com.jsowell.web.controller.tool; + +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.R; +import com.jsowell.common.util.StringUtils; +import io.swagger.annotations.*; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * swagger 用户测试方法 + * + * @author jsowell + */ +@Api("用户信息管理123") +@RestController +@RequestMapping("/test/user") +public class TestController extends BaseController { + private final static Map users = new LinkedHashMap(); + + { + users.put(1, new UserEntity(1, "thinkgem" , "admin123" , "15888888888")); + users.put(2, new UserEntity(2, "jsowell-test" , "admin123" , "15666666666")); + } + + @ApiOperation("获取用户列表") + @GetMapping("/list") + public R> userList() { + List userList = new ArrayList(users.values()); + return R.ok(userList); + } + + @ApiOperation("获取用户详细123") + @ApiImplicitParam(name = "userId" , value = "用户ID" , required = true, dataType = "int" , paramType = "path" , dataTypeClass = Integer.class) + @GetMapping("/{userId}") + public R getUser(@PathVariable Integer userId) { + if (!users.isEmpty() && users.containsKey(userId)) { + return R.ok(users.get(userId)); + } else { + return R.fail("用户不存在"); + } + } + + @ApiOperation("新增用户") + @ApiImplicitParams({ + @ApiImplicitParam(name = "userId" , value = "用户id" , dataType = "Integer" , dataTypeClass = Integer.class), + @ApiImplicitParam(name = "username" , value = "用户名称" , dataType = "String" , dataTypeClass = String.class), + @ApiImplicitParam(name = "password" , value = "用户密码" , dataType = "String" , dataTypeClass = String.class), + @ApiImplicitParam(name = "mobile" , value = "用户手机" , dataType = "String" , dataTypeClass = String.class) + }) + @PostMapping("/save") + public R save(UserEntity user) { + if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId())) { + return R.fail("用户ID不能为空"); + } + users.put(user.getUserId(), user); + return R.ok(); + } + + @ApiOperation("更新用户") + @PutMapping("/update") + public R update(@RequestBody UserEntity user) { + if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId())) { + return R.fail("用户ID不能为空"); + } + if (users.isEmpty() || !users.containsKey(user.getUserId())) { + return R.fail("用户不存在"); + } + users.remove(user.getUserId()); + users.put(user.getUserId(), user); + return R.ok(); + } + + @ApiOperation("删除用户信息") + @ApiImplicitParam(name = "userId" , value = "用户ID" , required = true, dataType = "int" , paramType = "path" , dataTypeClass = Integer.class) + @DeleteMapping("/{userId}") + public R delete(@PathVariable Integer userId) { + if (!users.isEmpty() && users.containsKey(userId)) { + users.remove(userId); + return R.ok(); + } else { + return R.fail("用户不存在"); + } + } +} + +@ApiModel(value = "UserEntity" , description = "用户实体") +class UserEntity { + @ApiModelProperty("用户ID") + private Integer userId; + + @ApiModelProperty("用户名称") + private String username; + + @ApiModelProperty("用户密码") + private String password; + + @ApiModelProperty("用户手机") + private String mobile; + + public UserEntity() { + + } + + public UserEntity(Integer userId, String username, String password, String mobile) { + this.userId = userId; + this.username = username; + this.password = password; + this.mobile = mobile; + } + + public Integer getUserId() { + return userId; + } + + public void setUserId(Integer userId) { + this.userId = userId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getMobile() { + return mobile; + } + + public void setMobile(String mobile) { + this.mobile = mobile; + } +} diff --git a/jsowell-admin/src/main/java/com/jsowell/web/core/config/SwaggerConfig.java b/jsowell-admin/src/main/java/com/jsowell/web/core/config/SwaggerConfig.java new file mode 100644 index 000000000..31a8df346 --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/core/config/SwaggerConfig.java @@ -0,0 +1,122 @@ +package com.jsowell.web.core.config; + +import com.jsowell.common.config.JsowellConfig; +import io.swagger.annotations.ApiOperation; +import io.swagger.models.auth.In; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.*; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.service.contexts.SecurityContext; +import springfox.documentation.spring.web.plugins.Docket; + +import java.util.ArrayList; +import java.util.List; + +/** + * Swagger2的接口配置 + * + * @author jsowell + */ +@Configuration +public class SwaggerConfig { + /** + * 系统基础配置 + */ + @Autowired + private JsowellConfig jsowellConfig; + + /** + * 是否开启swagger + */ + @Value("${swagger.enabled}") + private boolean enabled; + + /** + * 设置请求的统一前缀 + */ + @Value("${swagger.pathMapping}") + private String pathMapping; + + /** + * 创建API + */ + @Bean + public Docket createRestApi() { + return new Docket(DocumentationType.OAS_30) + // 是否启用Swagger + .enable(enabled) + // 用来创建该API的基本信息,展示在文档的页面中(自定义展示的信息) + .apiInfo(apiInfo()) + // 设置哪些接口暴露给Swagger展示 + .select() + // 扫描所有有注解的api,用这种方式更灵活 + .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) + // 扫描指定包中的swagger注解 + // .apis(RequestHandlerSelectors.basePackage("com.jsowell.project.tool.swagger")) + // 扫描所有 + .apis(RequestHandlerSelectors.any()) + .paths(PathSelectors.any()) + .build() + /* 设置安全模式,swagger可以设置访问token */ + .securitySchemes(securitySchemes()) + .securityContexts(securityContexts()) + .pathMapping(pathMapping); + } + + /** + * 安全模式,这里指定token通过Authorization头请求头传递 + */ + private List securitySchemes() { + List apiKeyList = new ArrayList(); + apiKeyList.add(new ApiKey("Authorization" , "Authorization" , In.HEADER.toValue())); + return apiKeyList; + } + + /** + * 安全上下文 + */ + private List securityContexts() { + List securityContexts = new ArrayList<>(); + securityContexts.add( + SecurityContext.builder() + .securityReferences(defaultAuth()) + .operationSelector(o -> o.requestMappingPattern().matches("/.*")) + .build()); + return securityContexts; + } + + /** + * 默认的安全上引用 + */ + private List defaultAuth() { + AuthorizationScope authorizationScope = new AuthorizationScope("global" , "accessEverything"); + AuthorizationScope[] authorizationScopes = new AuthorizationScope[1]; + authorizationScopes[0] = authorizationScope; + List securityReferences = new ArrayList<>(); + securityReferences.add(new SecurityReference("Authorization" , authorizationScopes)); + return securityReferences; + } + + /** + * 添加摘要信息 + */ + private ApiInfo apiInfo() { + // 用ApiInfoBuilder进行定制 + return new ApiInfoBuilder() + // 设置标题 + .title("标题:举视后台管理系统_接口文档") + // 描述 + // .description("描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...") + // 作者信息 + .contact(new Contact(jsowellConfig.getName(), null, null)) + // 版本 + .version("版本号:" + jsowellConfig.getVersion()) + .build(); + } +} diff --git a/jsowell-admin/src/main/resources/META-INF/spring-devtools.properties b/jsowell-admin/src/main/resources/META-INF/spring-devtools.properties new file mode 100644 index 000000000..2b23f85a3 --- /dev/null +++ b/jsowell-admin/src/main/resources/META-INF/spring-devtools.properties @@ -0,0 +1 @@ +restart.include.json=/com.alibaba.fastjson.*.jar \ No newline at end of file diff --git a/jsowell-admin/src/main/resources/application-dev.yml b/jsowell-admin/src/main/resources/application-dev.yml new file mode 100644 index 000000000..4d22c90c8 --- /dev/null +++ b/jsowell-admin/src/main/resources/application-dev.yml @@ -0,0 +1,132 @@ +# 数据源配置 +spring: + # redis 配置 + redis: + # 地址 + host: 192.168.2.2 + # 端口,默认为6379 + port: 6379 + # 数据库索引 + database: 0 + # 密码 + password: + # 连接超时时间 + timeout: 10s + lettuce: + pool: + # 连接池中的最小空闲连接 + min-idle: 0 + # 连接池中的最大空闲连接 + max-idle: 8 + # 连接池的最大数据库连接数 + max-active: 8 + # #连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + + # 数据源配置 + datasource: + type: com.alibaba.druid.pool.DruidDataSource + driverClassName: com.mysql.cj.jdbc.Driver + druid: + # 主库数据源 + master: + url: jdbc:mysql://192.168.2.2:3306/jsowell_dev?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 + username: root + password: 123456 + # 从库数据源 + slave: + # 从数据源开关/默认关闭 + enabled: false + url: + username: + password: + # 初始连接数 + initialSize: 10 + # 最小连接池数量 + minIdle: 10 + # 最大连接池数量 + maxActive: 20 + # 配置获取连接等待超时的时间 + maxWait: 60000 + # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 + timeBetweenEvictionRunsMillis: 60000 + # 配置一个连接在池中最小生存的时间,单位是毫秒 + minEvictableIdleTimeMillis: 300000 + # 配置一个连接在池中最大生存的时间,单位是毫秒 + maxEvictableIdleTimeMillis: 900000 + # 配置检测连接是否有效 + validationQuery: SELECT 1 FROM DUAL + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + webStatFilter: + enabled: true + statViewServlet: + enabled: true + # 设置白名单,不填则允许所有访问 + allow: + url-pattern: /druid/* + # 控制台管理用户名和密码 + login-username: jsowell + login-password: 123456 + filter: + stat: + enabled: true + # 慢SQL记录 + log-slow-sql: true + slow-sql-millis: 1000 + merge-sql: true + wall: + config: + multi-statement-allow: true + +# Swagger配置 +swagger: + # 是否开启swagger + enabled: true + # 请求前缀 + pathMapping: /dev-api + +# 日志配置 +logging: + level: + com.jsowell: debug + org.springframework: warn + +# 二维码前缀 +qrcodeurl: + prefix: https://api.jsowellcloud.com + + +########################微信支付参数####################################### +#微信商户号 +wechat: + mchId: 1632405339 + #商家API证书序列号 + mchSerialNo: 7596EF543159D21D25F199F82B9045FB9A82D7E0 + #商户在微信公众平台申请服务号对应的APPID + appId: wxbb3e0d474569481d + #回调报文解密V3密钥key + v3Key: bbac689f4654b209de4d6944808ec80b + #微信获取平台证书列表地址 + certificates: + url: https://api.mch.weixin.qq.com/v3/certificates + #微信统一下单Navtive的API地址,用于二维码支付 + unifiedOrder: + url: https://api.mch.weixin.qq.com/v3/pay/transactions/native + #微信统一下单JSAPI的API地址,用于微信小程序 + jsurl: https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi + # 申请退款API + refund: + jsurl: https://api.mch.weixin.qq.com/v3/refund/domestic/refunds + #异步接收微信支付结果通知的回调地址 + callback: https://api.jsowellcloud.com/uniapp/pay/wechatPayCallback + #异步接收微信退款结果通知的回调地址 + refundCallback: https://api.jsowellcloud.com/uniapp/pay/wechatPayRefundCallback + #商户证书私钥路径 + key: + path: D:/WechatCert/apiclient_key.pem +########################微信支付参数####################################### +# C:/Users/autum/Desktop/work/1632405339_20221125_cert/apiclient_key.pem +# E://key/1632405339_20221125_cert/apiclient_key.pem + diff --git a/jsowell-admin/src/main/resources/application-prd.yml b/jsowell-admin/src/main/resources/application-prd.yml new file mode 100644 index 000000000..25e5b52f5 --- /dev/null +++ b/jsowell-admin/src/main/resources/application-prd.yml @@ -0,0 +1,130 @@ +# 数据源配置 +spring: + # redis 配置 + redis: + # 地址 + host: r-uf6k0uet7mihr5z78f.redis.rds.aliyuncs.com + # 端口,默认为6379 + port: 6379 + # 数据库索引 + database: 0 + # 账号 + username: jsowell + # 密码 + password: js@160829 + # 连接超时时间 + timeout: 10s + lettuce: + pool: + # 连接池中的最小空闲连接 + min-idle: 0 + # 连接池中的最大空闲连接 + max-idle: 8 + # 连接池的最大数据库连接数 + max-active: 8 + # #连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + + datasource: + type: com.alibaba.druid.pool.DruidDataSource + driverClassName: com.mysql.cj.jdbc.Driver + druid: + # 主库数据源 + master: + url: jdbc:mysql://rm-uf6ra51u33dc3798l.mysql.rds.aliyuncs.com:3306/jsowell_prd?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 + username: jsowell + password: js@160829 + # 从库数据源 + slave: + # 从数据源开关/默认关闭 + enabled: false + url: + username: + password: + # 初始连接数 + initialSize: 5 + # 最小连接池数量 + minIdle: 10 + # 最大连接池数量 + maxActive: 20 + # 配置获取连接等待超时的时间 + maxWait: 60000 + # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 + timeBetweenEvictionRunsMillis: 60000 + # 配置一个连接在池中最小生存的时间,单位是毫秒 + minEvictableIdleTimeMillis: 300000 + # 配置一个连接在池中最大生存的时间,单位是毫秒 + maxEvictableIdleTimeMillis: 900000 + # 配置检测连接是否有效 + validationQuery: SELECT 1 FROM DUAL + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + webStatFilter: + enabled: true + statViewServlet: + enabled: true + # 设置白名单,不填则允许所有访问 + allow: + url-pattern: /druid/* + # 控制台管理用户名和密码 + login-username: jsowell + login-password: 123456 + filter: + stat: + enabled: true + # 慢SQL记录 + log-slow-sql: true + slow-sql-millis: 1000 + merge-sql: true + wall: + config: + multi-statement-allow: true + +# Swagger配置 +swagger: + # 是否开启swagger + enabled: false + # 请求前缀 + pathMapping: /dev-api + +# 日志配置 +logging: + level: + com.jsowell: info + org.springframework: warn + + +# 二维码前缀 +qrcodeurl: + prefix: https://api.jsowellcloud.com + +########################微信支付参数####################################### +#微信商户号 +wechat: + mchId: 1632405339 + #商家API证书序列号 + mchSerialNo: 7596EF543159D21D25F199F82B9045FB9A82D7E0 + #商户在微信公众平台申请服务号对应的APPID + appId: wxbb3e0d474569481d + #回调报文解密V3密钥key + v3Key: bbac689f4654b209de4d6944808ec80b + #微信获取平台证书列表地址 + certificates: + url: https://api.mch.weixin.qq.com/v3/certificates + #微信统一下单Navtive的API地址,用于二维码支付 + unifiedOrder: + url: https://api.mch.weixin.qq.com/v3/pay/transactions/native + #微信统一下单JSAPI的API地址,用于微信小程序 + jsurl: https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi + # 申请退款API + refund: + jsurl: https://api.mch.weixin.qq.com/v3/refund/domestic/refunds + #异步接收微信支付结果通知的回调地址 + callback: https://api.jsowellcloud.com/uniapp/pay/wechatPayCallback + #异步接收微信退款结果通知的回调地址 + refundCallback: https://api.jsowellcloud.com/uniapp/pay/wechatPayRefundCallback + #商户证书私钥路径 + key: + path: /usr/local/1632405339_20221125_cert/apiclient_key.pem +########################微信支付参数####################################### diff --git a/jsowell-admin/src/main/resources/application.yml b/jsowell-admin/src/main/resources/application.yml new file mode 100644 index 000000000..209b2d56e --- /dev/null +++ b/jsowell-admin/src/main/resources/application.yml @@ -0,0 +1,128 @@ +# 项目相关配置 +jsowell: + # 名称 + name: jsowell-service + # 版本 + version: 1.0.0 + # 版权年份 + copyrightYear: 2022 + # 实例演示开关 + demoEnabled: true + # 文件路径 示例( Windows配置D:/jsowell/uploadPath,Linux配置 /home/jsowell/uploadPath) + profile: D:/jsowell/uploadPath + # 获取ip地址开关 + addressEnabled: false + # 验证码类型 math 数组计算 char 字符验证 + captchaType: char + +# 开发环境配置 +server: + # 服务器的HTTP端口,默认为8080 + port: 8080 + servlet: + # 应用的访问路径 + context-path: / + tomcat: + # tomcat的URI编码 + uri-encoding: UTF-8 + # 连接数满后的排队数,默认为100 + accept-count: 1000 + threads: + # tomcat最大线程数,默认为200 + max: 800 + # Tomcat启动初始化的线程数,默认值10 + min-spare: 100 +# token配置 +token: + # 令牌自定义标识 + header: Authorization + # 令牌密钥 + secret: cdnflglzjoxjovuusklsqjtuup + # 令牌有效期(默认300分钟) + expireTime: 300 + # 接口令牌有效期 一个月 + serviceExpireTime: 43200 + +# 用户配置 +user: + password: + # 密码最大错误次数 + maxRetryCount: 5 + # 密码锁定时间(默认10分钟) + lockTime: 10 + +# Spring配置 +spring: + # 资源信息 + messages: + # 国际化资源文件路径 + basename: i18n/messages + profiles: + active: { profile } + # 文件上传 + servlet: + multipart: + # 单个文件大小 + max-file-size: 10MB + # 设置总上传的文件大小 + max-request-size: 20MB + + # 服务模块 + devtools: + restart: + # 热部署开关 + enabled: true + +# MyBatis配置 +mybatis: + # 搜索指定包别名 + typeAliasesPackage: com.jsowell.**.domain + # 配置mapper的扫描,找到所有的mapper.xml映射文件 + mapperLocations: classpath*:mapper/**/*Mapper.xml + # 加载全局的配置文件 + configLocation: classpath:mybatis/mybatis-config.xml + +# PageHelper分页插件 +pagehelper: + helperDialect: mysql + supportMethodsArguments: true + params: count=countSql + +# 防止XSS攻击 +xss: + # 过滤开关 + enabled: true + # 排除链接(多个用逗号分隔) + excludes: /system/notice + # 匹配链接 + urlPatterns: /system/*,/monitor/*,/tool/* + +# 微信登录相关 +weixin: + login: + # gateway: https://api.weixin.qq.com/sns/oauth2/access_token + gateway: https://api.weixin.qq.com/sns/jscode2session + appid: wxbb3e0d474569481d + appsecret: bbac689f4654b209de4d6944808ec80b + redirectUrl: http://www.kuangstudy.com/login/api/wx/callback + sendMsg: + startChargingTmpId: BGgZe98QHr0I1S1GrtGps5_rLX6n9cW1AsXhL4YkHHc + stopChargingTmpId: UyBPbADlZfsCj89rh_xvfZGlxTW5J5KURpZtt9CNFrY + +#Sim卡信息 +xunzhong: + apiId: 1126135385835987 + apiSecret: 9dT9iz0bfYx6UljzKj3pJEodjTvjd1lF + sim: + getSimCardDetailURL: https://iot.commchina.net/api/customer/v1/sim_cards/get_sim_card_detail + renewURL: https://iot.commchina.net/api/customer/v1/sim_cards/renew + trafficPool: + poolListURL: https://iot.commchina.net/api/customer/v1/traffic_pools/pool_list + +wulian: + appId: 23178072739 + appSecret: 9cf4e28037ca5ea1a8ea8e63959d454b + getWay: https://api.wl1688.net/iotc/getway + name: + getSimInfo: api.v2.card.info + WuLianSimRenew: api.v2.order.renew \ No newline at end of file diff --git a/jsowell-admin/src/main/resources/banner.txt b/jsowell-admin/src/main/resources/banner.txt new file mode 100644 index 000000000..3c39b64a3 --- /dev/null +++ b/jsowell-admin/src/main/resources/banner.txt @@ -0,0 +1,9 @@ + _ _____ ______ ________ _ _ + | |/ ____|/ __ \ \ / / ____| | | | + | | (___ | | | \ \ /\ / /| |__ | | | | + _ | |\___ \| | | |\ \/ \/ / | __| | | | | + | |__| |____) | |__| | \ /\ / | |____| |____| |____ + \____/|_____/ \____/ \/ \/ |______|______|______| + +Application Version: ${jsowell.version} +Spring Boot Version: ${spring-boot.version} \ No newline at end of file diff --git a/jsowell-admin/src/main/resources/i18n/messages.properties b/jsowell-admin/src/main/resources/i18n/messages.properties new file mode 100644 index 000000000..4098fc92c --- /dev/null +++ b/jsowell-admin/src/main/resources/i18n/messages.properties @@ -0,0 +1,37 @@ +#错误消息 +not.null=* 必须填写 +user.jcaptcha.error=验证码错误 +user.jcaptcha.expire=验证码已失效 +user.not.exists=用户不存在/密码错误 +user.password.not.match=用户不存在/密码错误 +user.password.retry.limit.count=密码输入错误{0}次 +user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟 +user.password.delete=对不起,您的账号已被删除 +user.blocked=用户已封禁,请联系管理员 +role.blocked=角色已封禁,请联系管理员 +user.logout.success=退出成功 + +length.not.valid=长度必须在{min}到{max}个字符之间 + +user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头 +user.password.not.valid=* 5-50个字符 + +user.email.not.valid=邮箱格式错误 +user.mobile.phone.number.not.valid=手机号格式错误 +user.login.success=登录成功 +user.register.success=注册成功 +user.notfound=请重新登录 +user.forcelogout=管理员强制退出,请重新登录 +user.unknown.error=未知错误,请重新登录 + +##文件上传消息 +upload.exceed.maxSize=上传的文件大小超出限制的文件大小!
允许的文件最大大小是:{0}MB! +upload.filename.exceed.length=上传的文件名最长{0}个字符 + +##权限 +no.permission=您没有数据的权限,请联系管理员添加权限 [{0}] +no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}] +no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}] +no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}] +no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}] +no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}] diff --git a/jsowell-admin/src/main/resources/logback.xml b/jsowell-admin/src/main/resources/logback.xml new file mode 100644 index 000000000..3abfdc498 --- /dev/null +++ b/jsowell-admin/src/main/resources/logback.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + ${log.pattern} + + + + + + ${log.path}/sys-info.log + + + + ${log.path}/sys-info.%d{yyyy-MM-dd}.log + + ${maxHistory} + + + ${log.pattern} + + + + INFO + + ACCEPT + + DENY + + + + + + ${log.path}/netty9011.log + + + + ${log.path}/netty9011.%d{yyyy-MM-dd}.log + + ${maxHistory} + + + ${log.pattern} + + + + INFO + + ACCEPT + + DENY + + + + + + ${log.path}/sys-error.log + + + + ${log.path}/sys-error.%d{yyyy-MM-dd}.log + + ${maxHistory} + + + ${log.pattern} + + + + ERROR + + ACCEPT + + DENY + + + + + + ${log.path}/sys-user.log + + + ${log.path}/sys-user.%d{yyyy-MM-dd}.log + + ${maxHistory} + + + ${log.pattern} + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jsowell-admin/src/main/resources/mybatis/mybatis-config.xml b/jsowell-admin/src/main/resources/mybatis/mybatis-config.xml new file mode 100644 index 000000000..9e14908f2 --- /dev/null +++ b/jsowell-admin/src/main/resources/mybatis/mybatis-config.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + diff --git a/jsowell-admin/src/test/java/SpringBootTestController.java b/jsowell-admin/src/test/java/SpringBootTestController.java new file mode 100644 index 000000000..f12eda1a2 --- /dev/null +++ b/jsowell-admin/src/test/java/SpringBootTestController.java @@ -0,0 +1,688 @@ +import com.alibaba.fastjson2.JSONObject; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.collect.ImmutableMap; +import com.jsowell.JsowellApplication; +import com.jsowell.common.constant.CacheConstants; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.domain.ykc.LoginRequestData; +import com.jsowell.common.core.domain.ykc.TransactionRecordsData; +import com.jsowell.common.core.redis.RedisCache; +import com.jsowell.common.enums.ykc.OrderStatusEnum; +import com.jsowell.common.exception.BusinessException; +import com.jsowell.common.util.*; +import com.jsowell.common.util.id.SnUtils; +import com.jsowell.common.util.id.SnowflakeIdWorker; +import com.jsowell.common.util.ip.AddressUtils; +import com.jsowell.netty.command.ykc.IssueQRCodeCommand; +import com.jsowell.netty.command.ykc.ProofreadTimeCommand; +import com.jsowell.netty.handler.HeartbeatRequestHandler; +import com.jsowell.netty.service.yunkuaichong.YKCBusinessService; +import com.jsowell.netty.service.yunkuaichong.YKCPushCommandService; +import com.jsowell.pile.domain.OrderBasicInfo; +import com.jsowell.pile.domain.PileBillingDetail; +import com.jsowell.pile.domain.PileBillingTemplate; +import com.jsowell.pile.domain.PileStationInfo; +import com.jsowell.pile.domain.WxpayCallbackRecord; +import com.jsowell.pile.dto.BasicPileDTO; +import com.jsowell.pile.dto.BatchCreatePileDTO; +import com.jsowell.pile.dto.ImportBillingTemplateDTO; +import com.jsowell.pile.dto.QueryOrderDTO; +import com.jsowell.pile.dto.QueryPileDTO; +import com.jsowell.pile.dto.QueryStationDTO; +import com.jsowell.pile.dto.WeixinPayDTO; +import com.jsowell.pile.mapper.MemberBasicInfoMapper; +import com.jsowell.pile.mapper.PileBillingTemplateMapper; +import com.jsowell.pile.service.IOrderBasicInfoService; +import com.jsowell.pile.service.IPileBasicInfoService; +import com.jsowell.pile.service.IPileBillingTemplateService; +import com.jsowell.pile.service.IPileMsgRecordService; +import com.jsowell.pile.service.IPileStationInfoService; +import com.jsowell.pile.service.SimCardService; +import com.jsowell.pile.service.WechatPayService; +import com.jsowell.pile.service.WxpayCallbackRecordService; +import com.jsowell.pile.vo.web.*; +import com.jsowell.service.MemberService; +import com.jsowell.service.OrderService; +import com.jsowell.service.PileRemoteService; +import com.jsowell.service.PileService; +import com.jsowell.wxpay.common.WeChatPayParameter; +import com.jsowell.wxpay.dto.AppletTemplateMessageSendDTO; +import com.jsowell.wxpay.dto.WeChatRefundDTO; +import com.jsowell.wxpay.response.WechatPayRefundRequest; +import com.jsowell.wxpay.service.WxAppletRemoteService; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.compress.utils.Lists; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.util.StopWatch; + +import java.math.BigDecimal; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@ActiveProfiles("dev") +@SpringBootTest(classes = JsowellApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@RunWith(SpringRunner.class) +public class SpringBootTestController { + + private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + @Autowired + private SnUtils snUtils; + + @Autowired + private PileService pileService; + + @Autowired + private YKCPushCommandService ykcPushCommandService; + + @Autowired + private IPileMsgRecordService pileMsgRecordService; + + @Autowired + private IPileStationInfoService pileStationInfoService; + + @Autowired + private YKCPushCommandService ykcPushBusinessService; + + @Autowired + private HeartbeatRequestHandler heartbeatRequestHandler; + + @Autowired + private YKCBusinessService ykcBusinessService; + + @Autowired + private PileBillingTemplateMapper pileBillingTemplateMapper; + + @Autowired + private PileRemoteService pileRemoteService; + + @Autowired + private MemberService memberService; + + @Autowired + private OrderService orderService; + + @Autowired + private IPileBillingTemplateService pileBillingTemplateService; + + @Autowired + private MemberBasicInfoMapper memberBasicInfoMapper; + + @Autowired + private SimCardService simCardService; + + @Autowired + private IPileBasicInfoService pileBasicInfoService; + + @Autowired + private WechatPayService wechatPayService; + + @Autowired + private IOrderBasicInfoService orderBasicInfoService; + + @Autowired + private WxpayCallbackRecordService wxpayCallbackRecordService; + + @Autowired + private WxAppletRemoteService wxAppletRemoteService; + + @Autowired + private RedisCache redisCache; + + @Test + public void testCloseStartFailedOrder() { + String startTime = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, DateUtils.addDays(new Date(), -2)); + String endTime = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, new Date()); + orderBasicInfoService.closeStartFailedOrder(startTime, endTime); + } + + @Test + public void testRedisSaveRealTimeData() { + String pileSn = "88000000000001"; + String connectorCode = "01"; + String orderCode = "88000000000001012211161342359448"; + + String pileConnectorCode = pileSn + connectorCode; + String redisKey = CacheConstants.PILE_REAL_TIME_MONITOR_DATA + pileConnectorCode + "_" + orderCode; + + // for (int i = 0; i < 10; i++) { + // try { + // Thread.sleep(10000); + // } catch (InterruptedException e) { + // e.printStackTrace(); + // } + // Date now = new Date(); + // redisCache.hset(redisKey, DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:00", now), i + ":" + DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", now)); + // } + + orderBasicInfoService.getChargingRealTimeData(orderCode); + } + + @Test + public void testuniformMessageSend() { + AppletTemplateMessageSendDTO appletTemplateMessageSendDTO = new AppletTemplateMessageSendDTO(); + // String openId = wxAppletRemoteService.getOpenIdByCode("0537u2100jTXsP1Y0Y300j426t47u210"); + // System.out.println("openId:" + openId); + + appletTemplateMessageSendDTO.setTouser("o4REX5MprZfTaLnVNxfdOY-wnwGI"); // openid + + String templateId = "UyBPbADlZfsCj89rh_xvfZGlxTW5J5KURpZtt9CNFrY"; + appletTemplateMessageSendDTO.setTemplate_id(templateId); + // appletTemplateMessageSendDTO.setPage("跳转的页面"); + Map map = new HashMap<>(); + map.put("amount17", ImmutableMap.of("value", "¥100")); // 结束时间 + map.put("time3", ImmutableMap.of("value", "2022-12-30")); // 结束时间 + map.put("thing7", ImmutableMap.of("value", "thing7")); // 结束原因 + + // map.put("thing5", ImmutableMap.of("value", "thing5")); // 结束原因 + // map.put("time2", ImmutableMap.of("value", "time2")); // 结束原因 + appletTemplateMessageSendDTO.setData(map); + + wxAppletRemoteService.uniformMessageSend(appletTemplateMessageSendDTO); + } + + @Test + public void testWeChatRefund() { + WeChatRefundDTO dto = new WeChatRefundDTO(); + dto.setRefundType("2"); + dto.setMemberId("82100864"); + dto.setRefundAmount(new BigDecimal("1.23")); + orderBasicInfoService.weChatRefund(dto); + } + + @Test + public void testSelectBalanceRechargeRecord() { + List list = wxpayCallbackRecordService.queryBalanceRechargeRecordOfTheLatestYear("82100864"); + System.out.println(list); + } + + @Test + public void testSelectOrderBasicInfoList() { + QueryOrderDTO orderBasicInfo = new QueryOrderDTO(); + orderBasicInfo.setPileSn("88000000000001"); + orderBasicInfo.setOrderStatus(OrderStatusEnum.IN_THE_CHARGING.getValue()); + List orderListVOS = orderBasicInfoService.selectOrderBasicInfoList(orderBasicInfo); + System.out.println(orderListVOS); + for (OrderListVO orderListVO : orderListVOS) { + if (StringUtils.equals(orderListVO.getOrderStatus(), OrderStatusEnum.IN_THE_CHARGING.getValue())) { + // 修改数据库订单状态 + OrderBasicInfo info = OrderBasicInfo.builder() + .id(Long.parseLong(orderListVO.getId())) + .orderStatus(OrderStatusEnum.ABNORMAL.getValue()) + .build(); + orderBasicInfoService.updateOrderBasicInfo(info); + } + } + } + + @Test + public void testWechatRefund() throws JsonProcessingException { + WechatPayRefundRequest request = new WechatPayRefundRequest(); + request.setTransaction_id("4200001656202212302746036536"); // 微信支付单号 + request.setOut_trade_no("768677222373363712"); // 商户订单号 + // 生成退款单号 + request.setOut_refund_no(SnowflakeIdWorker.getSnowflakeId()); // 商户退款单号 + request.setNotify_url(WeChatPayParameter.refundNotifyUrl); // 回调接口 + WechatPayRefundRequest.Amount amount = new WechatPayRefundRequest.Amount(); + amount.setRefund(10 * 100); // 退款金额 + amount.setTotal(10 * 100); // 原订单金额 + request.setAmount(amount); + request.setReason("结算退款"); + request.setFunds_account("AVAILABLE"); + + wechatPayService.ApplyForWechatPayRefundV3(request); + + // 退款方法 + + } + + @Test + public void testUpdatePileSimInfo() { + String pileSn = "88000000000001"; + String iccid = "898604940121C1385725"; + pileBasicInfoService.updatePileSimInfo(pileSn, iccid); + } + + @Test + public void testPay() { + String code = "081zIoGa11GamE0iVVIa1aaJ4G0zIoGE"; + String openId = memberService.getOpenIdByCode(code); + Map pay = null; + try { + WeixinPayDTO dto = new WeixinPayDTO(); + dto.setOpenId(openId); + dto.setAmount("0.01"); + pay = orderService.weixinPayV3(dto); + } catch (Exception e) { + e.printStackTrace(); + } + System.out.println(JSONObject.toJSONString(pay)); + } + + @Test + public void testMemberRegisterAndLogin() { + // String phone = "18512341234"; + // String merchantId = "18512341234"; + // String token = memberService.memberRegisterAndLogin(phone, merchantId); + // System.out.println(token); + // String memberId = JWTUtils.getMemberId(token); + // System.out.println(memberId); + } + + @Test + public void testMemberBasicInfoMapper() { + // String memberId = "21772870"; + // BigDecimal principalBalance = new BigDecimal("-10"); + // BigDecimal giftBalance = new BigDecimal("-110"); + // Integer version = 2; + // int i = memberBasicInfoMapper.updateMemberBalance(memberId, principalBalance, giftBalance, version); + // if (i == 1) { + // System.out.println("更新余额成功"); + // } else { + // System.out.println("更新余额失败"); + // } + } + + @Test + public void testGenerateBillingTemplateMsgBody() { + String pileSn = "88000000000001"; + // 根据桩号查询计费模板 + BillingTemplateVO billingTemplateVO = pileBillingTemplateService.selectBillingTemplateDetailByPileSn(pileSn); + + byte[] messageBody = pileBillingTemplateService.generateBillingTemplateMsgBody(pileSn, billingTemplateVO); + System.out.println(BytesUtil.binary(messageBody, 16)); + } + + @Test + public void testAnalysisPileParameter() { + BasicPileDTO dto = new BasicPileDTO(); + // 3个都不传的情况 + try { + System.out.println("3个都不传的情况"); + dto.setPileSn(""); + dto.setConnectorCode(""); + dto.setPileConnectorCode(""); + orderService.analysisPileParameter(dto); + System.out.println("数据正确"); + } catch (BusinessException e) { + System.out.println(e.getMessage()); + } + System.out.println(); + + // 只传sn的情况 + try { + System.out.println("只传sn的情况"); + dto.setConnectorCode(""); + dto.setPileConnectorCode(""); + dto.setPileSn("88000000000001"); + orderService.analysisPileParameter(dto); + System.out.println("数据正确"); + } catch (BusinessException e) { + System.out.println(e.getMessage()); + } + System.out.println(); + + // + try { + System.out.println("只穿枪口号的情况"); + dto.setConnectorCode("01"); + dto.setPileConnectorCode(""); + dto.setPileSn(""); + orderService.analysisPileParameter(dto); + System.out.println("数据正确"); + } catch (BusinessException e) { + System.out.println(e.getMessage()); + } + System.out.println(); + + // 只传充电桩枪口编号的情况 + try { + System.out.println("只传充电桩枪口编号的情况"); + dto.setPileConnectorCode("8800000000000101"); + dto.setConnectorCode(""); + dto.setPileSn(""); + orderService.analysisPileParameter(dto); + System.out.println("数据正确"); + } catch (BusinessException e) { + System.out.println(e.getMessage()); + } + + try { + System.out.println(); + System.out.println("传充电桩枪sn+枪口号的情况"); + dto.setPileConnectorCode(""); + dto.setConnectorCode("01"); + dto.setPileSn("88000000000001"); + orderService.analysisPileParameter(dto); + System.out.println("数据正确"); + } catch (BusinessException e) { + System.out.println(e.getMessage()); + } + } + + @Test + public void testImportBillingTemplate() { + ImportBillingTemplateDTO dto = new ImportBillingTemplateDTO(); + dto.setBillingTemplateId("1"); + // 查询公共计费模板是否存在 + PileBillingTemplate pileBillingTemplate = pileBillingTemplateMapper.selectPileBillingTemplateById(Long.valueOf(dto.getBillingTemplateId())); + if (pileBillingTemplate == null) { + + } + List billingDetailList = pileBillingTemplate.getPileBillingDetailList(); + } + + @Test + public void testProcess() { + // 62 68 + String msgString = "680da300000388000000000001010020d06840a40000130000000000000000000000000000000088000000000001010202000000000000000000000000000000000000000000000000000000000000000000001516"; + byte[] msg = BytesUtil.str2Bcd(msgString); + boolean b = YKCUtils.checkMsg(msg); + // ykcBusinessService.process(msg, null); + } + + @Test + public void testHeartbeat() { + // heartbeatRequestHandler.updateStatus("88000000000001", "01", "0"); + + // heartbeatRequestHandler.updateStatus("88000000000001", "02", "0"); + } + + @Test + public void testPush() { + byte[] msg = new byte[]{}; + String pileSn = "88000000000001"; + // ykcPushBusinessService.push(msg, pileSn, YKCFrameTypeCode.READ_REAL_TIME_MONITOR_DATA_CODE); + } + + @Test + public void TestMapUtils() { + String address = "淀山湖镇黄浦江南路278号"; + String areaCode = "320000,320500,320583"; + Map longitudeAndLatitude = AddressUtils.getLongitudeAndLatitude(areaCode, address); + System.out.println(longitudeAndLatitude); + } + + @Test + public void testCreatePile() { + BatchCreatePileDTO dto = BatchCreatePileDTO.builder() + .merchantId("1") + .stationId("1") + .softwareProtocol("1") + // .connectorNum(1) + // .num(10) + // .productionDate(new Date()) + .build(); + pileService.batchCreatePile(dto); + } + + @Test + public void testGetIncre() { + StopWatch stopWatch = new StopWatch(); + + // 生成100个 + stopWatch.start("生成100个sn号"); + List list2 = snUtils.generateSN(1); + stopWatch.stop(); + System.out.println(list2); + + System.out.println(stopWatch.getLastTaskTimeMillis()); + } + + @Test + public void testDict() { + String dictValue = DictUtils.getDictValue("query_pile_info", "url"); + String station_type = DictUtils.getDictLabel("station_type", "1"); + System.out.println(station_type); + System.out.println("123"); + + } + + @Test + public void testSelectByMerchantId() { + List list = pileStationInfoService.selectStationListByMerchantId(Long.valueOf(Constants.ONE)); + System.out.println(list); + } + + @Test + public void testStr2Bcd() { + String logicCardNum = "00000000"; + byte[] logicCardNumByteArr = BytesUtil.str2Bcd(logicCardNum); + System.out.println(Arrays.toString(logicCardNumByteArr)); + } + + @Test + public void testBigDecimalMultiply() { + BigDecimal a = new BigDecimal("216.5"); + BigDecimal b = new BigDecimal("11.5"); + + BigDecimal result = a.multiply(b).setScale(2, BigDecimal.ROUND_HALF_UP); + System.out.println(result); + } + + @Test + public void testRemoteUpdate() { + ArrayList list = new ArrayList<>(); + list.add("88000000000001"); + pileRemoteService.updateFile(list); + } + + @Test + public void testGetUserPhoneNum() { + String code = "0e5394cfa4eb41c6181ed257f2368a86dfe4ebdac0a4fac85df63657637e6cc3"; + wxAppletRemoteService.getMobileNumberByCode(code); + } + + @Test + public void testDistance() { + QueryStationDTO dto = new QueryStationDTO(); + dto.setStationLat("123.2222"); + dto.setStationLng("55.6232"); + + // pileStationInfoService.uniAppQueryStationInfos(dto); + } + + /** + * 生成英文字母随机数 RandomStringUtils.randomAlphabetic(10); + * 生成数字随机数 RandomStringUtils.randomNumeric(10); + * 字母+数字结合 RandomStringUtils.randomAlphanumeric(10); + */ + @Test + public void Test() throws ParseException { + /*String s = RandomStringUtils.randomAlphanumeric(32); + System.out.println(s); // PuLe4Tyyg1jSFNPhF5d2Ts9ejRn6E8KQ + String str = "JS160829"; + System.out.println(Md5Utils.hash(str).toUpperCase(Locale.ROOT)); +*/ + + Date startTimeDate = sdf.parse("2022-11-26 10:44:11"); + Date endTimeDate = sdf.parse("2022-11-27 12:45:11"); + + System.out.println(DateUtils.getDatePoor(endTimeDate, startTimeDate)); // 1天2小时1分钟 + + + + + /*String stra = "sp_mchid=1632405339&sub_mchid=1632405339&out_trade_no=1217752501201407033233368318&sp_appid=wxbb3e0d474569481d&sub_appid=wxbb3e0d474569481d" + + "bbac689f4654b209de4d6944808ec80b"; + System.out.println(Md5Utils.hash(stra).toUpperCase(Locale.ROOT));*/ + } + + @Test + public void testSimCard() throws ParseException { + ArrayList list = Lists.newArrayList(); + Collections.addAll(list, "898607B9102090253556", "898607B9102090253560"); + // String s = list.toString().replaceAll("(?:\\[|null|\\]| +)", ""); + // System.out.println(s); + // List simCardVOList = simCardService.selectSimCardInfoByIccId(list); + // System.out.println(simCardVOList.toString()); + + // simCardService.XunZhongSimRenewal(list, 12); + // System.out.println(s); + + // SimCardVO simCardVO = simCardService.searchByLoop("898607B9102090253556"); + // + // System.out.println(simCardVO.toString()); + + simCardService.WuLianSimRenew(list, 1); + + + } + + @Test + public void testRefund() { + OrderBasicInfo orderInfo = orderBasicInfoService.getOrderInfoByOrderCode("88000000000001012212171045412218"); + orderInfo.setReason("充电异常中止,急停开入"); + + TransactionRecordsData data = TransactionRecordsData.builder() + .orderCode("88000000000001012212171045412218") + .consumptionAmount(String.valueOf(0.00)) + .stopReasonMsg(orderInfo.getReason()) + .totalElectricity("0") + .sharpUsedElectricity("0") + .peakUsedElectricity("0") + .flatUsedElectricity("0") + .valleyUsedElectricity("0") + .build(); + + orderBasicInfoService.settleOrder(data, orderInfo); + } + + + @Test + public void testLoginHandler() { + String msg = "8800000000001001010f63362d333000000000898604b319227036282200"; + byte[] msgBody = BytesUtil.str2Bcd(msg); + + int startIndex = 0; + int length = 7; + + // 桩编码 + byte[] pileSnByte = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.binary(pileSnByte, 16); + // log.info("桩号:{}", pileSn); + + + // 桩类型 0 表示直流桩, 1 表示交流桩 + startIndex += length; + length = 1; + byte[] pileTypeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileType = BytesUtil.bcd2Str(pileTypeByteArr); + + // 充电枪数量 + startIndex += length; + byte[] connectorNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String connectorNum = BytesUtil.bcd2Str(connectorNumByteArr); + + // 通信协议版本 版本号乘 10,v1.0 表示 0x0A + startIndex += length; + byte[] communicationVersionByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + // int i = Integer.parseInt(BytesUtil.bcd2Str(communicationVersionByteArr)); // 0F --> 15 + BigDecimal bigDecimal = new BigDecimal(BytesUtil.bcd2Str(communicationVersionByteArr)); + BigDecimal communicationVersionTemp = bigDecimal.divide(new BigDecimal(10)); + String communicationVersion = "v" + communicationVersionTemp; + + // 程序版本 + startIndex += length; + length = 8; + byte[] programVersionByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String programVersion = BytesUtil.bcd2Str(programVersionByteArr); + + // 网络连接类型 0x00 SIM 卡 0x01 LAN 0x02 WAN 0x03 其他 + startIndex += length; + length = 1; + byte[] internetConnectionTypeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String internetConnection = BytesUtil.bcd2Str(internetConnectionTypeByteArr); + + // sim卡 + startIndex += length; + length = 10; + byte[] simCardNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String iccid = BytesUtil.bin2HexStr(simCardNumByteArr); + + // 运营商 0x00 移动 0x02 电信 0x03 联通 0x04 其他 + startIndex += length; + length = 1; + byte[] businessTypeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String business = BytesUtil.bcd2Str(businessTypeByteArr); + + LoginRequestData loginRequestData = LoginRequestData.builder() + .pileSn(pileSn) + .pileType(pileType) + .connectorNum(connectorNum) + .communicationVersion(communicationVersion) + .programVersion(programVersion) + .internetConnection(internetConnection) + .iccid(iccid) + .business(business) + .build(); + + // 结果(默认 0x01:登录失败) + byte[] flag = Constants.oneByteArray; + + // 通过桩编码SN查询数据库,如果有数据,则登录成功,否则登录失败 + QueryPileDTO dto = new QueryPileDTO(); + dto.setPileSn(pileSn); + List list = pileBasicInfoService.queryPileInfos(dto); + if (CollectionUtils.isNotEmpty(list)) { + flag = Constants.zeroByteArray; + // 登录成功,保存桩号和channel的关系 + + // PileChannelEntity.put(pileSn, channel); + // 更改桩和该桩下的枪口状态分别为 在线、空闲 + // pileBasicInfoService.updatePileStatus(pileSn, PileStatusEnum.ON_LINE.getValue()); + // pileConnectorInfoService.updateConnectorStatusByPileSn(pileSn, PileConnectorDataBaseStatusEnum.FREE.getValue()); + + // 对时 + ProofreadTimeCommand command = ProofreadTimeCommand.builder().pileSn(pileSn).build(); + ykcPushCommandService.pushProofreadTimeCommand(command); + + // 公共方法修改状态 + pileBasicInfoService.updateStatus(BytesUtil.bcd2Str(new byte[]{0x01}), pileSn, null, null, null); + + + // 下发二维码 + IssueQRCodeCommand issueQRCodeCommand = IssueQRCodeCommand.builder().pileSn(pileSn).build(); + ykcPushCommandService.pushIssueQRCodeCommand(issueQRCodeCommand); + + } + + + // 充电桩使用的sim卡,把信息存库 + if (StringUtils.equals("00", internetConnection)) { + try { + pileBasicInfoService.updatePileSimInfo(pileSn, iccid); + } catch (Exception e) { + // log.error("更新充电桩sim卡信息失败", e); + System.out.println(e.getMessage()); + } + } + + // 保存报文 + String jsonMsg = JSONObject.toJSONString(loginRequestData); + // pileMsgRecordService.save(pileSn, pileSn, type, jsonMsg, ykcDataProtocol.getHEXString()); + + // // 消息体 + // byte[] messageBody = Bytes.concat(pileSnByte, flag); + // return getResult(ykcDataProtocol, messageBody); + } + + @Test + public void testGetMemberToken(){ + String memberId = JWTUtils.getMemberId("eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2NzY1MTY5MzgsImV4cCI6MTY3OTEwODkzOH0.4MwhZIOpnCfQloR7zEm2hwPOh2yyI2qxbBbTcv_SnZ4"); + System.out.println(memberId); + } + +} diff --git a/jsowell-common/pom.xml b/jsowell-common/pom.xml new file mode 100644 index 000000000..03605d687 --- /dev/null +++ b/jsowell-common/pom.xml @@ -0,0 +1,181 @@ + + + 4.0.0 + + com.jsowell + jsowell-charger-web + 1.0.0 + + + jsowell-common + + + common通用工具 + + + + + + + org.springframework + spring-context-support + + + + + org.springframework + spring-web + + + + + org.springframework.boot + spring-boot-starter-security + + + + + com.github.pagehelper + pagehelper-spring-boot-starter + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.apache.commons + commons-lang3 + 3.12.0 + + + + + com.fasterxml.jackson.core + jackson-databind + + + + + com.alibaba.fastjson2 + fastjson2 + + + + + commons-io + commons-io + + + + + commons-fileupload + commons-fileupload + + + + + org.apache.poi + poi-ooxml + + + + + org.yaml + snakeyaml + + + + + io.jsonwebtoken + jjwt + + + + + javax.xml.bind + jaxb-api + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.apache.commons + commons-pool2 + + + + + eu.bitwalker + UserAgentUtils + + + + + javax.servlet + javax.servlet-api + + + + + org.projectlombok + lombok + + + + + com.google.guava + guava + + + + com.tencentcloudapi + tencentcloud-sdk-java + + + com.github.qcloudsms + qcloudsms + + + + com.thoughtworks.xstream + xstream + + + + cn.hutool + hutool-all + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.8 + 1.8 + + /src/test/** + + utf-8 + + + + + + + \ No newline at end of file diff --git a/jsowell-common/src/main/java/com/jsowell/common/annotation/Anonymous.java b/jsowell-common/src/main/java/com/jsowell/common/annotation/Anonymous.java new file mode 100644 index 000000000..a3d3cd235 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/annotation/Anonymous.java @@ -0,0 +1,18 @@ +package com.jsowell.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 匿名访问不鉴权注解 + * + * @author jsowell + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Anonymous { +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/annotation/DataScope.java b/jsowell-common/src/main/java/com/jsowell/common/annotation/DataScope.java new file mode 100644 index 000000000..beb2c9259 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/annotation/DataScope.java @@ -0,0 +1,27 @@ +package com.jsowell.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 数据权限过滤注解 + * + * @author jsowell + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DataScope { + /** + * 部门表的别名 + */ + public String deptAlias() default ""; + + /** + * 用户表的别名 + */ + public String userAlias() default ""; +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/annotation/DataSource.java b/jsowell-common/src/main/java/com/jsowell/common/annotation/DataSource.java new file mode 100644 index 000000000..5444f4135 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/annotation/DataSource.java @@ -0,0 +1,28 @@ +package com.jsowell.common.annotation; + +import com.jsowell.common.enums.DataSourceType; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 自定义多数据源切换注解 + *

+ * 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准 + * + * @author jsowell + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface DataSource { + /** + * 切换数据源名称 + */ + public DataSourceType value() default DataSourceType.MASTER; +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/annotation/Excel.java b/jsowell-common/src/main/java/com/jsowell/common/annotation/Excel.java new file mode 100644 index 000000000..04ca36e64 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/annotation/Excel.java @@ -0,0 +1,181 @@ +package com.jsowell.common.annotation; + +import com.jsowell.common.util.poi.ExcelHandlerAdapter; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.IndexedColors; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.math.BigDecimal; + +/** + * 自定义导出Excel数据注解 + * + * @author jsowell + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Excel { + /** + * 导出时在excel中排序 + */ + public int sort() default Integer.MAX_VALUE; + + /** + * 导出到Excel中的名字. + */ + public String name() default ""; + + /** + * 日期格式, 如: yyyy-MM-dd + */ + public String dateFormat() default ""; + + /** + * 如果是字典类型,请设置字典的type值 (如: sys_user_sex) + */ + public String dictType() default ""; + + /** + * 读取内容转表达式 (如: 0=男,1=女,2=未知) + */ + public String readConverterExp() default ""; + + /** + * 分隔符,读取字符串组内容 + */ + public String separator() default ","; + + /** + * BigDecimal 精度 默认:-1(默认不开启BigDecimal格式化) + */ + public int scale() default -1; + + /** + * BigDecimal 舍入规则 默认:BigDecimal.ROUND_HALF_EVEN + */ + public int roundingMode() default BigDecimal.ROUND_HALF_EVEN; + + /** + * 导出时在excel中每个列的高度 单位为字符 + */ + public double height() default 14; + + /** + * 导出时在excel中每个列的宽 单位为字符 + */ + public double width() default 16; + + /** + * 文字后缀,如% 90 变成90% + */ + public String suffix() default ""; + + /** + * 当值为空时,字段的默认值 + */ + public String defaultValue() default ""; + + /** + * 提示信息 + */ + public String prompt() default ""; + + /** + * 设置只能选择不能输入的列内容. + */ + public String[] combo() default {}; + + /** + * 是否需要纵向合并单元格,应对需求:含有list集合单元格) + */ + public boolean needMerge() default false; + + /** + * 是否导出数据,应对需求:有时我们需要导出一份模板,这是标题需要但内容需要用户手工填写. + */ + public boolean isExport() default true; + + /** + * 另一个类中的属性名称,支持多级获取,以小数点隔开 + */ + public String targetAttr() default ""; + + /** + * 是否自动统计数据,在最后追加一行统计数据总和 + */ + public boolean isStatistics() default false; + + /** + * 导出类型(0数字 1字符串 2图片) + */ + public ColumnType cellType() default ColumnType.STRING; + + /** + * 导出列头背景色 + */ + public IndexedColors headerBackgroundColor() default IndexedColors.GREY_50_PERCENT; + + /** + * 导出列头字体颜色 + */ + public IndexedColors headerColor() default IndexedColors.WHITE; + + /** + * 导出单元格背景色 + */ + public IndexedColors backgroundColor() default IndexedColors.WHITE; + + /** + * 导出单元格字体颜色 + */ + public IndexedColors color() default IndexedColors.BLACK; + + /** + * 导出字段对齐方式 + */ + public HorizontalAlignment align() default HorizontalAlignment.CENTER; + + /** + * 自定义数据处理器 + */ + public Class handler() default ExcelHandlerAdapter.class; + + /** + * 自定义数据处理器参数 + */ + public String[] args() default {}; + + /** + * 字段类型(0:导出导入;1:仅导出;2:仅导入) + */ + Type type() default Type.ALL; + + public enum Type { + ALL(0), EXPORT(1), IMPORT(2); + private final int value; + + Type(int value) { + this.value = value; + } + + public int value() { + return this.value; + } + } + + public enum ColumnType { + NUMERIC(0), STRING(1), IMAGE(2); + private final int value; + + ColumnType(int value) { + this.value = value; + } + + public int value() { + return this.value; + } + } +} \ No newline at end of file diff --git a/jsowell-common/src/main/java/com/jsowell/common/annotation/Excels.java b/jsowell-common/src/main/java/com/jsowell/common/annotation/Excels.java new file mode 100644 index 000000000..a8a8bd048 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/annotation/Excels.java @@ -0,0 +1,17 @@ +package com.jsowell.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Excel注解集 + * + * @author jsowell + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Excels { + public Excel[] value(); +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/annotation/Log.java b/jsowell-common/src/main/java/com/jsowell/common/annotation/Log.java new file mode 100644 index 000000000..cf872cc8e --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/annotation/Log.java @@ -0,0 +1,41 @@ +package com.jsowell.common.annotation; + +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.enums.OperatorType; + +import java.lang.annotation.*; + +/** + * 自定义操作日志记录注解 + * + * @author jsowell + */ +@Target({ElementType.PARAMETER, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Log { + /** + * 模块 + */ + public String title() default ""; + + /** + * 功能 + */ + public BusinessType businessType() default BusinessType.OTHER; + + /** + * 操作人类别 + */ + public OperatorType operatorType() default OperatorType.MANAGE; + + /** + * 是否保存请求的参数 + */ + public boolean isSaveRequestData() default true; + + /** + * 是否保存响应的参数 + */ + public boolean isSaveResponseData() default true; +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/annotation/RateLimiter.java b/jsowell-common/src/main/java/com/jsowell/common/annotation/RateLimiter.java new file mode 100644 index 000000000..8310351d0 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/annotation/RateLimiter.java @@ -0,0 +1,40 @@ +package com.jsowell.common.annotation; + +import com.jsowell.common.constant.CacheConstants; +import com.jsowell.common.enums.LimitType; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 限流注解 + * + * @author jsowell + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RateLimiter { + /** + * 限流key + */ + public String key() default CacheConstants.RATE_LIMIT_KEY; + + /** + * 限流时间,单位秒 + */ + public int time() default 60; + + /** + * 限流次数 + */ + public int count() default 100; + + /** + * 限流类型 + */ + public LimitType limitType() default LimitType.DEFAULT; +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/annotation/RepeatSubmit.java b/jsowell-common/src/main/java/com/jsowell/common/annotation/RepeatSubmit.java new file mode 100644 index 000000000..f7a879efa --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/annotation/RepeatSubmit.java @@ -0,0 +1,29 @@ +package com.jsowell.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 自定义注解防止表单重复提交 + * + * @author jsowell + */ +@Inherited +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RepeatSubmit { + /** + * 间隔时间(ms),小于此时间视为重复提交 + */ + public int interval() default 5000; + + /** + * 提示消息 + */ + public String message() default "不允许重复提交,请稍候再试"; +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/config/JsowellConfig.java b/jsowell-common/src/main/java/com/jsowell/common/config/JsowellConfig.java new file mode 100644 index 000000000..eeafd2d84 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/config/JsowellConfig.java @@ -0,0 +1,132 @@ +package com.jsowell.common.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 读取项目相关配置 + * + * @author jsowell + */ +@Component +@ConfigurationProperties(prefix = "jsowell") +public class JsowellConfig { + /** + * 项目名称 + */ + private String name; + + /** + * 版本 + */ + private String version; + + /** + * 版权年份 + */ + private String copyrightYear; + + /** + * 实例演示开关 + */ + private boolean demoEnabled; + + /** + * 上传路径 + */ + private static String profile; + + /** + * 获取地址开关 + */ + private static boolean addressEnabled; + + /** + * 验证码类型 + */ + private static String captchaType; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getCopyrightYear() { + return copyrightYear; + } + + public void setCopyrightYear(String copyrightYear) { + this.copyrightYear = copyrightYear; + } + + public boolean isDemoEnabled() { + return demoEnabled; + } + + public void setDemoEnabled(boolean demoEnabled) { + this.demoEnabled = demoEnabled; + } + + public static String getProfile() { + return profile; + } + + public void setProfile(String profile) { + JsowellConfig.profile = profile; + } + + public static boolean isAddressEnabled() { + return addressEnabled; + } + + public void setAddressEnabled(boolean addressEnabled) { + JsowellConfig.addressEnabled = addressEnabled; + } + + public static String getCaptchaType() { + return captchaType; + } + + public void setCaptchaType(String captchaType) { + JsowellConfig.captchaType = captchaType; + } + + /** + * 获取导入上传路径 + */ + public static String getImportPath() { + return getProfile() + "/import"; + } + + /** + * 获取头像上传路径 + */ + public static String getAvatarPath() { + return getProfile() + "/avatar"; + } + + /** + * 获取下载路径 + */ + public static String getDownloadPath() { + return getProfile() + "/download/"; + } + + /** + * 获取上传路径 + */ + public static String getUploadPath() { + return getProfile() + "/upload"; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/constant/CacheConstants.java b/jsowell-common/src/main/java/com/jsowell/common/constant/CacheConstants.java new file mode 100644 index 000000000..992e73874 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/constant/CacheConstants.java @@ -0,0 +1,102 @@ +package com.jsowell.common.constant; + +/** + * 缓存的key 常量 + * + * @author jsowell + */ +public class CacheConstants { + /** + * + */ + + + public static final int cache_expire_time_1m = 60; + + public static final int cache_expire_time_5m = 60 * 5; + + public static final int cache_expire_time_10m = 60 * 10; + + public static final int cache_expire_time_1h = 60 * 60; + + public static final int cache_expire_time_12h = 60 * 60 * 12; + + public static final int cache_expire_time_1d = 60 * 60 * 24; + + public static final String PILE_PROGRAM_VERSION = "pile_program_version"; + + /** + * 通过订单号查询订单信息Key + */ + public static final String GET_ORDER_INFO_BY_ORDER_CODE = "get_order_info_by_order_code:"; + + /** + * 充电枪口实时监控数据 + */ + public static final String PILE_REAL_TIME_MONITOR_DATA = "pile_real_time_monitor_data:"; + + /** + * 充电桩最后连接时间 + */ + public static final String PILE_LAST_CONNECTION = "pile_last_connection:"; + + /** + * 查询枪口信息列表前缀 + */ + public static final String SELECT_PILE_CONNECTOR_INFO_LIST = "select_pile_connector_info_list:"; + + /** + * 充电桩枪口状态前缀 + */ + public static final String PILE_CONNECTOR_STATUS_KEY = "pile_connector_status:"; + + /** + * 充电桩sn生成 key + */ + public static final String PILE_SN_GENERATE_KEY = "pile_sn_generate_"; + + /** + * 充电桩详情key + */ + public static final String PILE_DETAIL_KEY = "pile_detail:"; + + /** + * 登录用户 redis key + */ + public static final String LOGIN_TOKEN_KEY = "login_tokens:"; + + /** + * 验证码 redis key + */ + public static final String CAPTCHA_CODE_KEY = "captcha_codes:"; + + /** + * 参数管理 cache key + */ + public static final String SYS_CONFIG_KEY = "sys_config:"; + + /** + * 字典管理 cache key + */ + public static final String SYS_DICT_KEY = "sys_dict:"; + + /** + * 防重提交 redis key + */ + public static final String REPEAT_SUBMIT_KEY = "repeat_submit:"; + + /** + * 限流 redis key + */ + public static final String RATE_LIMIT_KEY = "rate_limit:"; + + /** + * 登录账户密码错误次数 redis key + */ + public static final String PWD_ERR_CNT_KEY = "pwd_err_cnt:"; + + /** + * 验证码有效期时长 redis key + */ + public static final String SMS_VERIFICATION_CODE_KEY = "sms_verification_code:"; +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/constant/Constants.java b/jsowell-common/src/main/java/com/jsowell/common/constant/Constants.java new file mode 100644 index 000000000..9943ac46e --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/constant/Constants.java @@ -0,0 +1,210 @@ +package com.jsowell.common.constant; + +import io.jsonwebtoken.Claims; + +/** + * 通用常量信息 + * + * @author jsowell + */ +public class Constants { + // 充电桩sn号长度 + public static final int PILE_SN_LENGTH = 14; + + // 充电桩枪口号长度 + public static final int CONNECTOR_CODE_LENGTH = 2; + + // 充电桩枪口编号长度 + public static final int PILE_CONNECTOR_CODE_LENGTH = PILE_SN_LENGTH + CONNECTOR_CODE_LENGTH; + + public static final String SOCKET_IP = "127.0.0.1"; + public static final Integer SOCKET_PORT = 9011; + + public static final String updateServerIP = "192.168.2.5"; + public static final int port = 0x15; + + public static final byte[] updateServerPort = new byte[]{port}; + + public static final String updateServerUserName = "ftpuser"; + + public static final String updateServerPassword = "ftp123456"; + + public static final String filePath = "/pile/test.bin"; + + public static final String partnerId = "1632405339"; // 商户号Id + + // public static final String APP_ID = "wxbb3e0d474569481d"; // 举视充电网 wxbb3e0d474569481d + // + // public static final String APP_SECRET = "bbac689f4654b209de4d6944808ec80b"; // 举视充电网 bbac689f4654b209de4d6944808ec80b + + public static final String ZERO = "0"; + + public static final String ONE = "1"; + + public static final String TWO = "2"; + + public static final String THREE = "3"; + + public static final int zero = 0; + + public static final int one = 1; + + public static final byte zeroByte = 0x00; + + public static final byte[] zeroByteArray = new byte[]{zeroByte}; + + public static final byte oneByte = 0x01; + + public static final byte[] oneByteArray = new byte[]{oneByte}; + + public static final byte twoByte = 0x02; + + public static final byte[] twoByteArray = new byte[]{twoByte}; + + public static final String FAULT_CODE = "255"; + + public static final String DOUBLE_ZERO = "00"; + + public static final String ZERO_ONE = "01"; + + public static final String ZERO_THREE = "03"; + /** + * 删除标识 0标识正常 + */ + public static final String DEL_FLAG_NORMAL = "0"; + + /** + * 删除标识 1标识被删除 + */ + public static final String DEL_FLAG_DELETE = "1"; + /** + * UTF-8 字符集 + */ + public static final String UTF8 = "UTF-8"; + + /** + * GBK 字符集 + */ + public static final String GBK = "GBK"; + + /** + * http请求 + */ + public static final String HTTP = "http://"; + + /** + * https请求 + */ + public static final String HTTPS = "https://"; + + /** + * 通用成功标识 + */ + public static final String SUCCESS = "0"; + + /** + * 通用失败标识 + */ + public static final String FAIL = "1"; + + /** + * 登录成功 + */ + public static final String LOGIN_SUCCESS = "Success"; + + /** + * 注销 + */ + public static final String LOGOUT = "Logout"; + + /** + * 注册 + */ + public static final String REGISTER = "Register"; + + /** + * 登录失败 + */ + public static final String LOGIN_FAIL = "Error"; + + /** + * 登录验证码有效期 + */ + public static final Integer VERIFICATION_CODE_EXPIRATION_TIME = 10; + + /** + * 验证码有效期(分钟) + */ + public static final Integer CAPTCHA_EXPIRATION = 2; + + /** + * 令牌 + */ + public static final String TOKEN = "token"; + + /** + * 令牌前缀 + */ + public static final String TOKEN_PREFIX = "Bearer"; + + /** + * 令牌前缀 + */ + public static final String LOGIN_USER_KEY = "login_user_key"; + + /** + * 用户ID + */ + public static final String JWT_USERID = "userid"; + + /** + * 用户名称 + */ + public static final String JWT_USERNAME = Claims.SUBJECT; + + /** + * 用户头像 + */ + public static final String JWT_AVATAR = "avatar"; + + /** + * 创建时间 + */ + public static final String JWT_CREATED = "created"; + + /** + * 用户权限 + */ + public static final String JWT_AUTHORITIES = "authorities"; + + /** + * 资源映射路径 前缀 + */ + public static final String RESOURCE_PREFIX = "/profile"; + + /** + * RMI 远程方法调用 + */ + public static final String LOOKUP_RMI = "rmi:"; + + /** + * LDAP 远程方法调用 + */ + public static final String LOOKUP_LDAP = "ldap:"; + + /** + * LDAPS 远程方法调用 + */ + public static final String LOOKUP_LDAPS = "ldaps:"; + + /** + * 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加) + */ + public static final String[] JOB_WHITELIST_STR = {"com.jsowell"}; + + /** + * 定时任务违规的字符 + */ + public static final String[] JOB_ERROR_STR = {"java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml", + "org.springframework", "org.apache", "com.jsowell.common.util.file"}; +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/constant/GenConstants.java b/jsowell-common/src/main/java/com/jsowell/common/constant/GenConstants.java new file mode 100644 index 000000000..378d9555d --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/constant/GenConstants.java @@ -0,0 +1,186 @@ +package com.jsowell.common.constant; + +/** + * 代码生成通用常量 + * + * @author jsowell + */ +public class GenConstants { + /** + * 单表(增删改查) + */ + public static final String TPL_CRUD = "crud"; + + /** + * 树表(增删改查) + */ + public static final String TPL_TREE = "tree"; + + /** + * 主子表(增删改查) + */ + public static final String TPL_SUB = "sub"; + + /** + * 树编码字段 + */ + public static final String TREE_CODE = "treeCode"; + + /** + * 树父编码字段 + */ + public static final String TREE_PARENT_CODE = "treeParentCode"; + + /** + * 树名称字段 + */ + public static final String TREE_NAME = "treeName"; + + /** + * 上级菜单ID字段 + */ + public static final String PARENT_MENU_ID = "parentMenuId"; + + /** + * 上级菜单名称字段 + */ + public static final String PARENT_MENU_NAME = "parentMenuName"; + + /** + * 数据库字符串类型 + */ + public static final String[] COLUMNTYPE_STR = {"char", "varchar", "nvarchar", "varchar2"}; + + /** + * 数据库文本类型 + */ + public static final String[] COLUMNTYPE_TEXT = {"tinytext", "text", "mediumtext", "longtext"}; + + /** + * 数据库时间类型 + */ + public static final String[] COLUMNTYPE_TIME = {"datetime", "time", "date", "timestamp"}; + + /** + * 数据库数字类型 + */ + public static final String[] COLUMNTYPE_NUMBER = {"tinyint", "smallint", "mediumint", "int", "number", "integer", + "bit", "bigint", "float", "double", "decimal"}; + + /** + * 页面不需要编辑字段 + */ + public static final String[] COLUMNNAME_NOT_EDIT = {"id", "create_by", "create_time", "del_flag"}; + + /** + * 页面不需要显示的列表字段 + */ + public static final String[] COLUMNNAME_NOT_LIST = {"id", "create_by", "create_time", "del_flag", "update_by", + "update_time"}; + + /** + * 页面不需要查询字段 + */ + public static final String[] COLUMNNAME_NOT_QUERY = {"id", "create_by", "create_time", "del_flag", "update_by", + "update_time", "remark"}; + + /** + * Entity基类字段 + */ + public static final String[] BASE_ENTITY = {"createBy", "createTime", "updateBy", "updateTime", "remark"}; + + /** + * Tree基类字段 + */ + public static final String[] TREE_ENTITY = {"parentName", "parentId", "orderNum", "ancestors", "children"}; + + /** + * 文本框 + */ + public static final String HTML_INPUT = "input"; + + /** + * 文本域 + */ + public static final String HTML_TEXTAREA = "textarea"; + + /** + * 下拉框 + */ + public static final String HTML_SELECT = "select"; + + /** + * 单选框 + */ + public static final String HTML_RADIO = "radio"; + + /** + * 复选框 + */ + public static final String HTML_CHECKBOX = "checkbox"; + + /** + * 日期控件 + */ + public static final String HTML_DATETIME = "datetime"; + + /** + * 图片上传控件 + */ + public static final String HTML_IMAGE_UPLOAD = "imageUpload"; + + /** + * 文件上传控件 + */ + public static final String HTML_FILE_UPLOAD = "fileUpload"; + + /** + * 富文本控件 + */ + public static final String HTML_EDITOR = "editor"; + + /** + * 字符串类型 + */ + public static final String TYPE_STRING = "String"; + + /** + * 整型 + */ + public static final String TYPE_INTEGER = "Integer"; + + /** + * 长整型 + */ + public static final String TYPE_LONG = "Long"; + + /** + * 浮点型 + */ + public static final String TYPE_DOUBLE = "Double"; + + /** + * 高精度计算类型 + */ + public static final String TYPE_BIGDECIMAL = "BigDecimal"; + + /** + * 时间类型 + */ + public static final String TYPE_DATE = "Date"; + + /** + * 模糊查询 + */ + public static final String QUERY_LIKE = "LIKE"; + + /** + * 相等查询 + */ + public static final String QUERY_EQ = "EQ"; + + /** + * 需要 + */ + public static final String REQUIRE = "1"; +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/constant/HttpStatus.java b/jsowell-common/src/main/java/com/jsowell/common/constant/HttpStatus.java new file mode 100644 index 000000000..4e6b8a78b --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/constant/HttpStatus.java @@ -0,0 +1,88 @@ +package com.jsowell.common.constant; + +/** + * 返回状态码 + * + * @author jsowell + */ +public class HttpStatus { + /** + * 操作成功 + */ + public static final int SUCCESS = 200; + + /** + * 对象创建成功 + */ + public static final int CREATED = 201; + + /** + * 请求已经被接受 + */ + public static final int ACCEPTED = 202; + + /** + * 操作已经执行成功,但是没有返回数据 + */ + public static final int NO_CONTENT = 204; + + /** + * 资源已被移除 + */ + public static final int MOVED_PERM = 301; + + /** + * 重定向 + */ + public static final int SEE_OTHER = 303; + + /** + * 资源没有被修改 + */ + public static final int NOT_MODIFIED = 304; + + /** + * 参数列表错误(缺少,格式不匹配) + */ + public static final int BAD_REQUEST = 400; + + /** + * 未授权 + */ + public static final int UNAUTHORIZED = 401; + + /** + * 访问受限,授权过期 + */ + public static final int FORBIDDEN = 403; + + /** + * 资源,服务未找到 + */ + public static final int NOT_FOUND = 404; + + /** + * 不允许的http方法 + */ + public static final int BAD_METHOD = 405; + + /** + * 资源冲突,或者资源被锁 + */ + public static final int CONFLICT = 409; + + /** + * 不支持的数据,媒体类型 + */ + public static final int UNSUPPORTED_TYPE = 415; + + /** + * 系统内部错误 + */ + public static final int ERROR = 500; + + /** + * 接口未实现 + */ + public static final int NOT_IMPLEMENTED = 501; +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/constant/ScheduleConstants.java b/jsowell-common/src/main/java/com/jsowell/common/constant/ScheduleConstants.java new file mode 100644 index 000000000..d0f53afcd --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/constant/ScheduleConstants.java @@ -0,0 +1,56 @@ +package com.jsowell.common.constant; + +/** + * 任务调度通用常量 + * + * @author jsowell + */ +public class ScheduleConstants { + public static final String TASK_CLASS_NAME = "TASK_CLASS_NAME"; + + /** + * 执行目标key + */ + public static final String TASK_PROPERTIES = "TASK_PROPERTIES"; + + /** + * 默认 + */ + public static final String MISFIRE_DEFAULT = "0"; + + /** + * 立即触发执行 + */ + public static final String MISFIRE_IGNORE_MISFIRES = "1"; + + /** + * 触发一次执行 + */ + public static final String MISFIRE_FIRE_AND_PROCEED = "2"; + + /** + * 不触发立即执行 + */ + public static final String MISFIRE_DO_NOTHING = "3"; + + public enum Status { + /** + * 正常 + */ + NORMAL("0"), + /** + * 暂停 + */ + PAUSE("1"); + + private String value; + + private Status(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/constant/UserConstants.java b/jsowell-common/src/main/java/com/jsowell/common/constant/UserConstants.java new file mode 100644 index 000000000..d7c2bc32b --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/constant/UserConstants.java @@ -0,0 +1,111 @@ +package com.jsowell.common.constant; + +/** + * 用户常量信息 + * + * @author jsowell + */ +public class UserConstants { + /** + * 平台内系统用户的唯一标志 + */ + public static final String SYS_USER = "SYS_USER"; + + /** + * 正常状态 + */ + public static final String NORMAL = "0"; + + /** + * 异常状态 + */ + public static final String EXCEPTION = "1"; + + /** + * 用户封禁状态 + */ + public static final String USER_DISABLE = "1"; + + /** + * 角色封禁状态 + */ + public static final String ROLE_DISABLE = "1"; + + /** + * 部门正常状态 + */ + public static final String DEPT_NORMAL = "0"; + + /** + * 部门停用状态 + */ + public static final String DEPT_DISABLE = "1"; + + /** + * 字典正常状态 + */ + public static final String DICT_NORMAL = "0"; + + /** + * 是否为系统默认(是) + */ + public static final String YES = "Y"; + + /** + * 是否菜单外链(是) + */ + public static final String YES_FRAME = "0"; + + /** + * 是否菜单外链(否) + */ + public static final String NO_FRAME = "1"; + + /** + * 菜单类型(目录) + */ + public static final String TYPE_DIR = "M"; + + /** + * 菜单类型(菜单) + */ + public static final String TYPE_MENU = "C"; + + /** + * 菜单类型(按钮) + */ + public static final String TYPE_BUTTON = "F"; + + /** + * Layout组件标识 + */ + public final static String LAYOUT = "Layout"; + + /** + * ParentView组件标识 + */ + public final static String PARENT_VIEW = "ParentView"; + + /** + * InnerLink组件标识 + */ + public final static String INNER_LINK = "InnerLink"; + + /** + * 校验返回结果码 + */ + public final static String UNIQUE = "0"; + public final static String NOT_UNIQUE = "1"; + + /** + * 用户名长度限制 + */ + public static final int USERNAME_MIN_LENGTH = 2; + public static final int USERNAME_MAX_LENGTH = 20; + + /** + * 密码长度限制 + */ + public static final int PASSWORD_MIN_LENGTH = 5; + public static final int PASSWORD_MAX_LENGTH = 20; +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/constant/WeiXinConstants.java b/jsowell-common/src/main/java/com/jsowell/common/constant/WeiXinConstants.java new file mode 100644 index 000000000..083375ae0 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/constant/WeiXinConstants.java @@ -0,0 +1,64 @@ +package com.jsowell.common.constant; + +/** + * 微信支付常量 + * WeiXinConstants
+ * 创建人:小威
+ * 时间:2015年10月19日-下午7:14:15
+ * + * @version 1.0.0 + */ +public class WeiXinConstants { + + + public static final String URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";//统一下单url + + public static final String MCH_URL = "https://api.mch.weixin.qq.com/secapi/pay/profitsharing";//统一分账url + + public static final String MCH_URL_MULTIPRO = "https://api.mch.weixin.qq.com/secapi/pay/multiprofitsharing";//统一分账url(多次分账) + + public static final String PROFIT_SHARING_QUERY = "https://api.mch.weixin.qq.com/pay/profitsharingquery"; //统一分账结果查询 + + public static final String MCH_URL_FINISH = "https://api.mch.weixin.qq.com/secapi/pay/profitsharingfinish";//统一分账url + + public static final String MCH_STATUS_URL = "https://api.mch.weixin.qq.com/pay/profitsharingquery";//统一分账查询url + + public static final String MCH_ADD_RELATION_URL = "https://api.mch.weixin.qq.com/pay/profitsharingaddreceiver";//分账关系绑定地址 + + public static final String REFUND = "https://api.mch.weixin.qq.com/secapi/pay/refund";//退款 + + + public static final String RETURN_CODE = "return_code"; // SUCCESS/FAIL 此字段是通信标识,非交易标识 + + public static final String SUCCESS = "SUCCESS"; // SUCCESS/FAIL 此字段是通信标识,非交易标识 + + public static final int WIDTH = 200;//二维码宽度 + + public static final int HEIGHT = 200;//二维码高度 + + public static final String RESULT = "result_code";//返回结果 + + public static final String ORDER_PAID = "orderPaid";//已经支付 + + public static final String WEIXINPAY = "weixinPay";//可以支付 + + public static final String CODE_URL = "code_url";//微信支付url + + public static final String ERROR_CODE = "err_code";//错误码 + + public static final String ATTACH = "attach";//商家数据包 + + public static final String BODY = "body";//商品名称 + + public static final String NOTIFY_URL = "notify_url";//异步回调地址 + + public static final String OUT_TRADE_NO = "out_trade_no";//商家订单号 + + public static final String TRANSACTION_ID = "transaction_id";//微信订单号 + + public static final String PRODUCT_ID = "product_id";//商品ID + + public static final String SPBILL_CREATE_IP = "spbill_create_ip";//支付用户IP地址 + + public static final String TOTAL_FEE = "total_fee";//支付金额 +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/controller/BaseController.java b/jsowell-common/src/main/java/com/jsowell/common/core/controller/BaseController.java new file mode 100644 index 000000000..427a84b23 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/controller/BaseController.java @@ -0,0 +1,187 @@ +package com.jsowell.common.core.controller; + +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.jsowell.common.constant.HttpStatus; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.domain.model.LoginUser; +import com.jsowell.common.core.page.PageDomain; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.core.page.TableSupport; +import com.jsowell.common.enums.ykc.ReturnCodeEnum; +import com.jsowell.common.exception.BusinessException; +import com.jsowell.common.util.DateUtils; +import com.jsowell.common.util.JWTUtils; +import com.jsowell.common.util.PageUtils; +import com.jsowell.common.util.SecurityUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.sql.SqlUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; + +import javax.servlet.http.HttpServletRequest; +import java.beans.PropertyEditorSupport; +import java.util.Date; +import java.util.List; + +/** + * web层通用数据处理 + * + * @author jsowell + */ +public class BaseController { + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + + /** + * 将前台传递过来的日期格式的字符串,自动转化为Date类型 + */ + @InitBinder + public void initBinder(WebDataBinder binder) { + // Date 类型转换 + binder.registerCustomEditor(Date.class, new PropertyEditorSupport() { + @Override + public void setAsText(String text) { + setValue(DateUtils.parseDate(text)); + } + }); + } + + /** + * 设置请求分页数据 + */ + protected void startPage() { + PageUtils.startPage(); + } + + /** + * 设置请求排序数据 + */ + protected void startOrderBy() { + PageDomain pageDomain = TableSupport.buildPageRequest(); + if (StringUtils.isNotEmpty(pageDomain.getOrderBy())) { + String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy()); + PageHelper.orderBy(orderBy); + } + } + + /** + * 清理分页的线程变量 + */ + protected void clearPage() { + PageUtils.clearPage(); + } + + /** + * 响应请求分页数据 + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + protected TableDataInfo getDataTable(List list) { + TableDataInfo rspData = new TableDataInfo(); + rspData.setCode(HttpStatus.SUCCESS); + rspData.setMsg("查询成功"); + rspData.setRows(list); + rspData.setTotal(new PageInfo(list).getTotal()); + return rspData; + } + + /** + * 返回成功 + */ + public AjaxResult success() { + return AjaxResult.success(); + } + + /** + * 返回失败消息 + */ + public AjaxResult error() { + return AjaxResult.error(); + } + + /** + * 返回成功消息 + */ + public AjaxResult success(String message) { + return AjaxResult.success(message); + } + + /** + * 返回失败消息 + */ + public AjaxResult error(String message) { + return AjaxResult.error(message); + } + + /** + * 响应返回结果 + * + * @param rows 影响行数 + * @return 操作结果 + */ + protected AjaxResult toAjax(int rows) { + return rows > 0 ? AjaxResult.success() : AjaxResult.error(); + } + + /** + * 响应返回结果 + * + * @param result 结果 + * @return 操作结果 + */ + protected AjaxResult toAjax(boolean result) { + return result ? success() : error(); + } + + /** + * 页面跳转 + */ + public String redirect(String url) { + return StringUtils.format("redirect:{}", url); + } + + /** + * 获取用户缓存信息 + */ + public LoginUser getLoginUser() { + return SecurityUtils.getLoginUser(); + } + + /** + * 获取登录用户id + */ + public Long getUserId() { + return getLoginUser().getUserId(); + } + + /** + * 获取登录部门id + */ + public Long getDeptId() { + return getLoginUser().getDeptId(); + } + + /** + * 获取登录用户名 + */ + public String getUsername() { + return getLoginUser().getUsername(); + } + + public String getMemberIdByAuthorization(String authorization) { + if (StringUtils.isBlank(authorization)) { + throw new BusinessException(ReturnCodeEnum.CODE_TOKEN_ERROR); + } + String memberId = JWTUtils.getMemberId(authorization); + if (StringUtils.isBlank(memberId)) { + throw new BusinessException(ReturnCodeEnum.CODE_TOKEN_ERROR); + } + // logger.info("authorization:{}, memberId:{}", authorization, memberId); + return memberId; + } + + public String getMemberIdByAuthorization(HttpServletRequest request) { + return getMemberIdByAuthorization(request.getHeader("Authorization")); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/domain/AjaxResult.java b/jsowell-common/src/main/java/com/jsowell/common/core/domain/AjaxResult.java new file mode 100644 index 000000000..5265a9988 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/domain/AjaxResult.java @@ -0,0 +1,155 @@ +package com.jsowell.common.core.domain; + +import com.jsowell.common.constant.HttpStatus; +import com.jsowell.common.util.StringUtils; + +import java.util.HashMap; + +/** + * 操作消息提醒 + * + * @author jsowell + */ +public class AjaxResult extends HashMap { + private static final long serialVersionUID = 1L; + + /** + * 状态码 + */ + public static final String CODE_TAG = "code"; + + /** + * 返回内容 + */ + public static final String MSG_TAG = "msg"; + + /** + * 数据对象 + */ + public static final String DATA_TAG = "data"; + + /** + * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。 + */ + public AjaxResult() { + } + + /** + * 初始化一个新创建的 AjaxResult 对象 + * + * @param code 状态码 + * @param msg 返回内容 + */ + public AjaxResult(int code, String msg) { + super.put(CODE_TAG, code); + super.put(MSG_TAG, msg); + } + + /** + * 初始化一个新创建的 AjaxResult 对象 + * + * @param code 状态码 + * @param msg 返回内容 + * @param data 数据对象 + */ + public AjaxResult(int code, String msg, Object data) { + super.put(CODE_TAG, code); + super.put(MSG_TAG, msg); + if (StringUtils.isNotNull(data)) { + super.put(DATA_TAG, data); + } + } + + /** + * 返回成功消息 + * + * @return 成功消息 + */ + public static AjaxResult success() { + return AjaxResult.success("操作成功"); + } + + /** + * 返回成功数据 + * + * @return 成功消息 + */ + public static AjaxResult success(Object data) { + return AjaxResult.success("操作成功", data); + } + + /** + * 返回成功消息 + * + * @param msg 返回内容 + * @return 成功消息 + */ + public static AjaxResult success(String msg) { + return AjaxResult.success(msg, null); + } + + /** + * 返回成功消息 + * + * @param msg 返回内容 + * @param data 数据对象 + * @return 成功消息 + */ + public static AjaxResult success(String msg, Object data) { + return new AjaxResult(HttpStatus.SUCCESS, msg, data); + } + + /** + * 返回错误消息 + * + * @return + */ + public static AjaxResult error() { + return AjaxResult.error("操作失败"); + } + + /** + * 返回错误消息 + * + * @param msg 返回内容 + * @return 警告消息 + */ + public static AjaxResult error(String msg) { + return AjaxResult.error(msg, null); + } + + /** + * 返回错误消息 + * + * @param msg 返回内容 + * @param data 数据对象 + * @return 警告消息 + */ + public static AjaxResult error(String msg, Object data) { + return new AjaxResult(HttpStatus.ERROR, msg, data); + } + + /** + * 返回错误消息 + * + * @param code 状态码 + * @param msg 返回内容 + * @return 警告消息 + */ + public static AjaxResult error(int code, String msg) { + return new AjaxResult(code, msg, null); + } + + /** + * 方便链式调用 + * + * @param key 键 + * @param value 值 + * @return 数据对象 + */ + @Override + public AjaxResult put(String key, Object value) { + super.put(key, value); + return this; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/domain/BaseEntity.java b/jsowell-common/src/main/java/com/jsowell/common/core/domain/BaseEntity.java new file mode 100644 index 000000000..96702d497 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/domain/BaseEntity.java @@ -0,0 +1,114 @@ +package com.jsowell.common.core.domain; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * Entity基类 + * + * @author jsowell + */ + +public class BaseEntity implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 搜索值 + */ + private String searchValue; + + /** + * 创建者 + */ + private String createBy; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 更新者 + */ + private String updateBy; + + /** + * 更新时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** + * 备注 + */ + private String remark; + + /** + * 请求参数 + */ + private Map params; + + public String getSearchValue() { + return searchValue; + } + + public void setSearchValue(String searchValue) { + this.searchValue = searchValue; + } + + public String getCreateBy() { + return createBy; + } + + public void setCreateBy(String createBy) { + this.createBy = createBy; + } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } + + public String getUpdateBy() { + return updateBy; + } + + public void setUpdateBy(String updateBy) { + this.updateBy = updateBy; + } + + public Date getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Date updateTime) { + this.updateTime = updateTime; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public Map getParams() { + if (params == null) { + params = new HashMap<>(); + } + return params; + } + + public void setParams(Map params) { + this.params = params; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/domain/R.java b/jsowell-common/src/main/java/com/jsowell/common/core/domain/R.java new file mode 100644 index 000000000..9b858d8d5 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/domain/R.java @@ -0,0 +1,94 @@ +package com.jsowell.common.core.domain; + +import com.jsowell.common.constant.HttpStatus; + +import java.io.Serializable; + +/** + * 响应信息主体 + * + * @author jsowell + */ +public class R implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 成功 + */ + public static final int SUCCESS = HttpStatus.SUCCESS; + + /** + * 失败 + */ + public static final int FAIL = HttpStatus.ERROR; + + private int code; + + private String msg; + + private T data; + + public static R ok() { + return restResult(null, SUCCESS, "操作成功"); + } + + public static R ok(T data) { + return restResult(data, SUCCESS, "操作成功"); + } + + public static R ok(T data, String msg) { + return restResult(data, SUCCESS, msg); + } + + public static R fail() { + return restResult(null, FAIL, "操作失败"); + } + + public static R fail(String msg) { + return restResult(null, FAIL, msg); + } + + public static R fail(T data) { + return restResult(data, FAIL, "操作失败"); + } + + public static R fail(T data, String msg) { + return restResult(data, FAIL, msg); + } + + public static R fail(int code, String msg) { + return restResult(null, code, msg); + } + + private static R restResult(T data, int code, String msg) { + R apiResult = new R<>(); + apiResult.setCode(code); + apiResult.setData(data); + apiResult.setMsg(msg); + return apiResult; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/domain/TreeEntity.java b/jsowell-common/src/main/java/com/jsowell/common/core/domain/TreeEntity.java new file mode 100644 index 000000000..3a38ec8cb --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/domain/TreeEntity.java @@ -0,0 +1,78 @@ +package com.jsowell.common.core.domain; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tree基类 + * + * @author jsowell + */ +public class TreeEntity extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * 父菜单名称 + */ + private String parentName; + + /** + * 父菜单ID + */ + private Long parentId; + + /** + * 显示顺序 + */ + private Integer orderNum; + + /** + * 祖级列表 + */ + private String ancestors; + + /** + * 子部门 + */ + private List children = new ArrayList<>(); + + public String getParentName() { + return parentName; + } + + public void setParentName(String parentName) { + this.parentName = parentName; + } + + public Long getParentId() { + return parentId; + } + + public void setParentId(Long parentId) { + this.parentId = parentId; + } + + public Integer getOrderNum() { + return orderNum; + } + + public void setOrderNum(Integer orderNum) { + this.orderNum = orderNum; + } + + public String getAncestors() { + return ancestors; + } + + public void setAncestors(String ancestors) { + this.ancestors = ancestors; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/domain/TreeSelect.java b/jsowell-common/src/main/java/com/jsowell/common/core/domain/TreeSelect.java new file mode 100644 index 000000000..6a32b5622 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/domain/TreeSelect.java @@ -0,0 +1,74 @@ +package com.jsowell.common.core.domain; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.jsowell.common.core.domain.entity.SysDept; +import com.jsowell.common.core.domain.entity.SysMenu; + +import java.io.Serializable; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Treeselect树结构实体类 + * + * @author jsowell + */ +public class TreeSelect implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 节点ID + */ + private Long id; + + /** + * 节点名称 + */ + private String label; + + /** + * 子节点 + */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private List children; + + public TreeSelect() { + + } + + public TreeSelect(SysDept dept) { + this.id = dept.getDeptId(); + this.label = dept.getDeptName(); + this.children = dept.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + public TreeSelect(SysMenu menu) { + this.id = menu.getMenuId(); + this.label = menu.getMenuName(); + this.children = menu.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/domain/entity/SysDept.java b/jsowell-common/src/main/java/com/jsowell/common/core/domain/entity/SysDept.java new file mode 100644 index 000000000..4ed76ea6b --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/domain/entity/SysDept.java @@ -0,0 +1,203 @@ +package com.jsowell.common.core.domain.entity; + +import com.jsowell.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.ArrayList; +import java.util.List; + +/** + * 部门表 sys_dept + * + * @author jsowell + */ +public class SysDept extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 父部门ID + */ + private Long parentId; + + /** + * 祖级列表 + */ + private String ancestors; + + /** + * 部门名称 + */ + private String deptName; + + /** + * 显示顺序 + */ + private Integer orderNum; + + /** + * 负责人 + */ + private String leader; + + /** + * 联系电话 + */ + private String phone; + + /** + * 邮箱 + */ + private String email; + + /** + * 部门状态:0正常,1停用 + */ + private String status; + + /** + * 删除标志(0代表存在 2代表删除) + */ + private String delFlag; + + /** + * 父部门名称 + */ + private String parentName; + + /** + * 子部门 + */ + private List children = new ArrayList(); + + public Long getDeptId() { + return deptId; + } + + public void setDeptId(Long deptId) { + this.deptId = deptId; + } + + public Long getParentId() { + return parentId; + } + + public void setParentId(Long parentId) { + this.parentId = parentId; + } + + public String getAncestors() { + return ancestors; + } + + public void setAncestors(String ancestors) { + this.ancestors = ancestors; + } + + @NotBlank(message = "部门名称不能为空") + @Size(min = 0, max = 30, message = "部门名称长度不能超过30个字符") + public String getDeptName() { + return deptName; + } + + public void setDeptName(String deptName) { + this.deptName = deptName; + } + + @NotNull(message = "显示顺序不能为空") + public Integer getOrderNum() { + return orderNum; + } + + public void setOrderNum(Integer orderNum) { + this.orderNum = orderNum; + } + + public String getLeader() { + return leader; + } + + public void setLeader(String leader) { + this.leader = leader; + } + + @Size(min = 0, max = 11, message = "联系电话长度不能超过11个字符") + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + @Email(message = "邮箱格式不正确") + @Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符") + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getDelFlag() { + return delFlag; + } + + public void setDelFlag(String delFlag) { + this.delFlag = delFlag; + } + + public String getParentName() { + return parentName; + } + + public void setParentName(String parentName) { + this.parentName = parentName; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("deptId", getDeptId()) + .append("parentId", getParentId()) + .append("ancestors", getAncestors()) + .append("deptName", getDeptName()) + .append("orderNum", getOrderNum()) + .append("leader", getLeader()) + .append("phone", getPhone()) + .append("email", getEmail()) + .append("status", getStatus()) + .append("delFlag", getDelFlag()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .toString(); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/domain/entity/SysDictData.java b/jsowell-common/src/main/java/com/jsowell/common/core/domain/entity/SysDictData.java new file mode 100644 index 000000000..f7d1e7ed2 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/domain/entity/SysDictData.java @@ -0,0 +1,175 @@ +package com.jsowell.common.core.domain.entity; + +import com.jsowell.common.annotation.Excel; +import com.jsowell.common.annotation.Excel.ColumnType; +import com.jsowell.common.constant.UserConstants; +import com.jsowell.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +/** + * 字典数据表 sys_dict_data + * + * @author jsowell + */ +public class SysDictData extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * 字典编码 + */ + @Excel(name = "字典编码", cellType = ColumnType.NUMERIC) + private Long dictCode; + + /** + * 字典排序 + */ + @Excel(name = "字典排序", cellType = ColumnType.NUMERIC) + private Long dictSort; + + /** + * 字典标签 + */ + @Excel(name = "字典标签") + private String dictLabel; + + /** + * 字典键值 + */ + @Excel(name = "字典键值") + private String dictValue; + + /** + * 字典类型 + */ + @Excel(name = "字典类型") + private String dictType; + + /** + * 样式属性(其他样式扩展) + */ + private String cssClass; + + /** + * 表格字典样式 + */ + private String listClass; + + /** + * 是否默认(Y是 N否) + */ + @Excel(name = "是否默认", readConverterExp = "Y=是,N=否") + private String isDefault; + + /** + * 状态(0正常 1停用) + */ + @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + private String status; + + public Long getDictCode() { + return dictCode; + } + + public void setDictCode(Long dictCode) { + this.dictCode = dictCode; + } + + public Long getDictSort() { + return dictSort; + } + + public void setDictSort(Long dictSort) { + this.dictSort = dictSort; + } + + @NotBlank(message = "字典标签不能为空") + @Size(min = 0, max = 100, message = "字典标签长度不能超过100个字符") + public String getDictLabel() { + return dictLabel; + } + + public void setDictLabel(String dictLabel) { + this.dictLabel = dictLabel; + } + + @NotBlank(message = "字典键值不能为空") + @Size(min = 0, max = 100, message = "字典键值长度不能超过100个字符") + public String getDictValue() { + return dictValue; + } + + public void setDictValue(String dictValue) { + this.dictValue = dictValue; + } + + @NotBlank(message = "字典类型不能为空") + @Size(min = 0, max = 100, message = "字典类型长度不能超过100个字符") + public String getDictType() { + return dictType; + } + + public void setDictType(String dictType) { + this.dictType = dictType; + } + + @Size(min = 0, max = 100, message = "样式属性长度不能超过100个字符") + public String getCssClass() { + return cssClass; + } + + public void setCssClass(String cssClass) { + this.cssClass = cssClass; + } + + public String getListClass() { + return listClass; + } + + public void setListClass(String listClass) { + this.listClass = listClass; + } + + public boolean getDefault() { + return UserConstants.YES.equals(this.isDefault) ? true : false; + } + + public String getIsDefault() { + return isDefault; + } + + public void setIsDefault(String isDefault) { + this.isDefault = isDefault; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("dictCode", getDictCode()) + .append("dictSort", getDictSort()) + .append("dictLabel", getDictLabel()) + .append("dictValue", getDictValue()) + .append("dictType", getDictType()) + .append("cssClass", getCssClass()) + .append("listClass", getListClass()) + .append("isDefault", getIsDefault()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/domain/entity/SysDictType.java b/jsowell-common/src/main/java/com/jsowell/common/core/domain/entity/SysDictType.java new file mode 100644 index 000000000..bd758809e --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/domain/entity/SysDictType.java @@ -0,0 +1,96 @@ +package com.jsowell.common.core.domain.entity; + +import com.jsowell.common.annotation.Excel; +import com.jsowell.common.annotation.Excel.ColumnType; +import com.jsowell.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; + +/** + * 字典类型表 sys_dict_type + * + * @author jsowell + */ +public class SysDictType extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * 字典主键 + */ + @Excel(name = "字典主键", cellType = ColumnType.NUMERIC) + private Long dictId; + + /** + * 字典名称 + */ + @Excel(name = "字典名称") + private String dictName; + + /** + * 字典类型 + */ + @Excel(name = "字典类型") + private String dictType; + + /** + * 状态(0正常 1停用) + */ + @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + private String status; + + public Long getDictId() { + return dictId; + } + + public void setDictId(Long dictId) { + this.dictId = dictId; + } + + @NotBlank(message = "字典名称不能为空") + @Size(min = 0, max = 100, message = "字典类型名称长度不能超过100个字符") + public String getDictName() { + return dictName; + } + + public void setDictName(String dictName) { + this.dictName = dictName; + } + + @NotBlank(message = "字典类型不能为空") + @Size(min = 0, max = 100, message = "字典类型类型长度不能超过100个字符") + @Pattern(regexp = "^[a-z][a-z0-9_]*$", message = "字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)") + public String getDictType() { + return dictType; + } + + public void setDictType(String dictType) { + this.dictType = dictType; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("dictId", getDictId()) + .append("dictName", getDictName()) + .append("dictType", getDictType()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/domain/entity/SysMenu.java b/jsowell-common/src/main/java/com/jsowell/common/core/domain/entity/SysMenu.java new file mode 100644 index 000000000..9645c4a53 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/domain/entity/SysMenu.java @@ -0,0 +1,259 @@ +package com.jsowell.common.core.domain.entity; + +import com.jsowell.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.ArrayList; +import java.util.List; + +/** + * 菜单权限表 sys_menu + * + * @author jsowell + */ +public class SysMenu extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * 菜单ID + */ + private Long menuId; + + /** + * 菜单名称 + */ + private String menuName; + + /** + * 父菜单名称 + */ + private String parentName; + + /** + * 父菜单ID + */ + private Long parentId; + + /** + * 显示顺序 + */ + private Integer orderNum; + + /** + * 路由地址 + */ + private String path; + + /** + * 组件路径 + */ + private String component; + + /** + * 路由参数 + */ + private String query; + + /** + * 是否为外链(0是 1否) + */ + private String isFrame; + + /** + * 是否缓存(0缓存 1不缓存) + */ + private String isCache; + + /** + * 类型(M目录 C菜单 F按钮) + */ + private String menuType; + + /** + * 显示状态(0显示 1隐藏) + */ + private String visible; + + /** + * 菜单状态(0显示 1隐藏) + */ + private String status; + + /** + * 权限字符串 + */ + private String perms; + + /** + * 菜单图标 + */ + private String icon; + + /** + * 子菜单 + */ + private List children = new ArrayList(); + + public Long getMenuId() { + return menuId; + } + + public void setMenuId(Long menuId) { + this.menuId = menuId; + } + + @NotBlank(message = "菜单名称不能为空") + @Size(min = 0, max = 50, message = "菜单名称长度不能超过50个字符") + public String getMenuName() { + return menuName; + } + + public void setMenuName(String menuName) { + this.menuName = menuName; + } + + public String getParentName() { + return parentName; + } + + public void setParentName(String parentName) { + this.parentName = parentName; + } + + public Long getParentId() { + return parentId; + } + + public void setParentId(Long parentId) { + this.parentId = parentId; + } + + @NotNull(message = "显示顺序不能为空") + public Integer getOrderNum() { + return orderNum; + } + + public void setOrderNum(Integer orderNum) { + this.orderNum = orderNum; + } + + @Size(min = 0, max = 200, message = "路由地址不能超过200个字符") + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + @Size(min = 0, max = 200, message = "组件路径不能超过255个字符") + public String getComponent() { + return component; + } + + public void setComponent(String component) { + this.component = component; + } + + public String getQuery() { + return query; + } + + public void setQuery(String query) { + this.query = query; + } + + public String getIsFrame() { + return isFrame; + } + + public void setIsFrame(String isFrame) { + this.isFrame = isFrame; + } + + public String getIsCache() { + return isCache; + } + + public void setIsCache(String isCache) { + this.isCache = isCache; + } + + @NotBlank(message = "菜单类型不能为空") + public String getMenuType() { + return menuType; + } + + public void setMenuType(String menuType) { + this.menuType = menuType; + } + + public String getVisible() { + return visible; + } + + public void setVisible(String visible) { + this.visible = visible; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + @Size(min = 0, max = 100, message = "权限标识长度不能超过100个字符") + public String getPerms() { + return perms; + } + + public void setPerms(String perms) { + this.perms = perms; + } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("menuId", getMenuId()) + .append("menuName", getMenuName()) + .append("parentId", getParentId()) + .append("orderNum", getOrderNum()) + .append("path", getPath()) + .append("component", getComponent()) + .append("isFrame", getIsFrame()) + .append("IsCache", getIsCache()) + .append("menuType", getMenuType()) + .append("visible", getVisible()) + .append("status ", getStatus()) + .append("perms", getPerms()) + .append("icon", getIcon()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/domain/entity/SysRole.java b/jsowell-common/src/main/java/com/jsowell/common/core/domain/entity/SysRole.java new file mode 100644 index 000000000..56606008e --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/domain/entity/SysRole.java @@ -0,0 +1,222 @@ +package com.jsowell.common.core.domain.entity; + +import com.jsowell.common.annotation.Excel; +import com.jsowell.common.annotation.Excel.ColumnType; +import com.jsowell.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +/** + * 角色表 sys_role + * + * @author jsowell + */ +public class SysRole extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * 角色ID + */ + @Excel(name = "角色序号", cellType = ColumnType.NUMERIC) + private Long roleId; + + /** + * 角色名称 + */ + @Excel(name = "角色名称") + private String roleName; + + /** + * 角色权限 + */ + @Excel(name = "角色权限") + private String roleKey; + + /** + * 角色排序 + */ + @Excel(name = "角色排序") + private String roleSort; + + /** + * 数据范围(1:所有数据权限;2:自定义数据权限;3:本部门数据权限;4:本部门及以下数据权限;5:仅本人数据权限) + */ + @Excel(name = "数据范围", readConverterExp = "1=所有数据权限,2=自定义数据权限,3=本部门数据权限,4=本部门及以下数据权限,5=仅本人数据权限") + private String dataScope; + + /** + * 菜单树选择项是否关联显示( 0:父子不互相关联显示 1:父子互相关联显示) + */ + private boolean menuCheckStrictly; + + /** + * 部门树选择项是否关联显示(0:父子不互相关联显示 1:父子互相关联显示 ) + */ + private boolean deptCheckStrictly; + + /** + * 角色状态(0正常 1停用) + */ + @Excel(name = "角色状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** + * 删除标志(0代表存在 2代表删除) + */ + private String delFlag; + + /** + * 用户是否存在此角色标识 默认不存在 + */ + private boolean flag = false; + + /** + * 菜单组 + */ + private Long[] menuIds; + + /** + * 部门组(数据权限) + */ + private Long[] deptIds; + + public SysRole() { + + } + + public SysRole(Long roleId) { + this.roleId = roleId; + } + + public Long getRoleId() { + return roleId; + } + + public void setRoleId(Long roleId) { + this.roleId = roleId; + } + + public boolean isAdmin() { + return isAdmin(this.roleId); + } + + public static boolean isAdmin(Long roleId) { + return roleId != null && 1L == roleId; + } + + @NotBlank(message = "角色名称不能为空") + @Size(min = 0, max = 30, message = "角色名称长度不能超过30个字符") + public String getRoleName() { + return roleName; + } + + public void setRoleName(String roleName) { + this.roleName = roleName; + } + + @NotBlank(message = "权限字符不能为空") + @Size(min = 0, max = 100, message = "权限字符长度不能超过100个字符") + public String getRoleKey() { + return roleKey; + } + + public void setRoleKey(String roleKey) { + this.roleKey = roleKey; + } + + @NotBlank(message = "显示顺序不能为空") + public String getRoleSort() { + return roleSort; + } + + public void setRoleSort(String roleSort) { + this.roleSort = roleSort; + } + + public String getDataScope() { + return dataScope; + } + + public void setDataScope(String dataScope) { + this.dataScope = dataScope; + } + + public boolean isMenuCheckStrictly() { + return menuCheckStrictly; + } + + public void setMenuCheckStrictly(boolean menuCheckStrictly) { + this.menuCheckStrictly = menuCheckStrictly; + } + + public boolean isDeptCheckStrictly() { + return deptCheckStrictly; + } + + public void setDeptCheckStrictly(boolean deptCheckStrictly) { + this.deptCheckStrictly = deptCheckStrictly; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getDelFlag() { + return delFlag; + } + + public void setDelFlag(String delFlag) { + this.delFlag = delFlag; + } + + public boolean isFlag() { + return flag; + } + + public void setFlag(boolean flag) { + this.flag = flag; + } + + public Long[] getMenuIds() { + return menuIds; + } + + public void setMenuIds(Long[] menuIds) { + this.menuIds = menuIds; + } + + public Long[] getDeptIds() { + return deptIds; + } + + public void setDeptIds(Long[] deptIds) { + this.deptIds = deptIds; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("roleId", getRoleId()) + .append("roleName", getRoleName()) + .append("roleKey", getRoleKey()) + .append("roleSort", getRoleSort()) + .append("dataScope", getDataScope()) + .append("menuCheckStrictly", isMenuCheckStrictly()) + .append("deptCheckStrictly", isDeptCheckStrictly()) + .append("status", getStatus()) + .append("delFlag", getDelFlag()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/domain/entity/SysUser.java b/jsowell-common/src/main/java/com/jsowell/common/core/domain/entity/SysUser.java new file mode 100644 index 000000000..f1a163685 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/domain/entity/SysUser.java @@ -0,0 +1,322 @@ +package com.jsowell.common.core.domain.entity; + +import com.jsowell.common.annotation.Excel; +import com.jsowell.common.annotation.Excel.ColumnType; +import com.jsowell.common.annotation.Excel.Type; +import com.jsowell.common.annotation.Excels; +import com.jsowell.common.core.domain.BaseEntity; +import com.jsowell.common.xss.Xss; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.util.Date; +import java.util.List; + +/** + * 用户对象 sys_user + * + * @author jsowell + */ +public class SysUser extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * 用户ID + */ + @Excel(name = "用户序号", cellType = ColumnType.NUMERIC, prompt = "用户编号") + private Long userId; + + /** + * 部门ID + */ + @Excel(name = "部门编号", type = Type.IMPORT) + private Long deptId; + + /** + * 用户账号 + */ + @Excel(name = "登录名称") + private String userName; + + /** + * 用户昵称 + */ + @Excel(name = "用户名称") + private String nickName; + + /** + * 用户邮箱 + */ + @Excel(name = "用户邮箱") + private String email; + + /** + * 手机号码 + */ + @Excel(name = "手机号码") + private String phone; + + /** + * 用户性别 + */ + @Excel(name = "用户性别", readConverterExp = "0=男,1=女,2=未知") + private String sex; + + /** + * 用户头像 + */ + private String avatar; + + /** + * 密码 + */ + private String password; + + /** + * 帐号状态(0正常 1停用) + */ + @Excel(name = "帐号状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** + * 删除标志(0代表存在 2代表删除) + */ + private String delFlag; + + /** + * 最后登录IP + */ + @Excel(name = "最后登录IP", type = Type.EXPORT) + private String loginIp; + + /** + * 最后登录时间 + */ + @Excel(name = "最后登录时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss", type = Type.EXPORT) + private Date loginDate; + + /** + * 部门对象 + */ + @Excels({ + @Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT), + @Excel(name = "部门负责人", targetAttr = "leader", type = Type.EXPORT) + }) + private SysDept dept; + + /** + * 角色对象 + */ + private List roles; + + /** + * 角色组 + */ + private Long[] roleIds; + + /** + * 岗位组 + */ + private Long[] postIds; + + /** + * 角色ID + */ + private Long roleId; + + public SysUser() { + + } + + public SysUser(Long userId) { + this.userId = userId; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public boolean isAdmin() { + return isAdmin(this.userId); + } + + public static boolean isAdmin(Long userId) { + return userId != null && 1L == userId; + } + + public Long getDeptId() { + return deptId; + } + + public void setDeptId(Long deptId) { + this.deptId = deptId; + } + + @Xss(message = "用户昵称不能包含脚本字符") + @Size(min = 0, max = 30, message = "用户昵称长度不能超过30个字符") + public String getNickName() { + return nickName; + } + + public void setNickName(String nickName) { + this.nickName = nickName; + } + + @Xss(message = "用户账号不能包含脚本字符") + @NotBlank(message = "用户账号不能为空") + @Size(min = 0, max = 30, message = "用户账号长度不能超过30个字符") + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + @Email(message = "邮箱格式不正确") + @Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符") + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + @Size(min = 0, max = 11, message = "手机号码长度不能超过11个字符") + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getSex() { + return sex; + } + + public void setSex(String sex) { + this.sex = sex; + } + + public String getAvatar() { + return avatar; + } + + public void setAvatar(String avatar) { + this.avatar = avatar; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getDelFlag() { + return delFlag; + } + + public void setDelFlag(String delFlag) { + this.delFlag = delFlag; + } + + public String getLoginIp() { + return loginIp; + } + + public void setLoginIp(String loginIp) { + this.loginIp = loginIp; + } + + public Date getLoginDate() { + return loginDate; + } + + public void setLoginDate(Date loginDate) { + this.loginDate = loginDate; + } + + public SysDept getDept() { + return dept; + } + + public void setDept(SysDept dept) { + this.dept = dept; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } + + public Long[] getRoleIds() { + return roleIds; + } + + public void setRoleIds(Long[] roleIds) { + this.roleIds = roleIds; + } + + public Long[] getPostIds() { + return postIds; + } + + public void setPostIds(Long[] postIds) { + this.postIds = postIds; + } + + public Long getRoleId() { + return roleId; + } + + public void setRoleId(Long roleId) { + this.roleId = roleId; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("userId", getUserId()) + .append("deptId", getDeptId()) + .append("userName", getUserName()) + .append("nickName", getNickName()) + .append("email", getEmail()) + .append("phone", getPhone()) + .append("sex", getSex()) + .append("avatar", getAvatar()) + .append("password", getPassword()) + .append("status", getStatus()) + .append("delFlag", getDelFlag()) + .append("loginIp", getLoginIp()) + .append("loginDate", getLoginDate()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .append("dept", getDept()) + .toString(); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/domain/model/LoginBody.java b/jsowell-common/src/main/java/com/jsowell/common/core/domain/model/LoginBody.java new file mode 100644 index 000000000..694786212 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/domain/model/LoginBody.java @@ -0,0 +1,32 @@ +package com.jsowell.common.core.domain.model; + +import lombok.Data; + +/** + * 用户登录对象 + * + * @author jsowell + */ +@Data +public class LoginBody { + /** + * 用户名 + */ + private String username; + + /** + * 用户密码 + */ + private String password; + + /** + * 验证码 + */ + private String code; + + /** + * 唯一标识 + */ + private String uuid; + +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/domain/model/LoginUser.java b/jsowell-common/src/main/java/com/jsowell/common/core/domain/model/LoginUser.java new file mode 100644 index 000000000..017688080 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/domain/model/LoginUser.java @@ -0,0 +1,234 @@ +package com.jsowell.common.core.domain.model; + +import com.alibaba.fastjson2.annotation.JSONField; +import com.jsowell.common.core.domain.entity.SysUser; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Set; + +/** + * 登录用户身份权限 + * + * @author jsowell + */ +public class LoginUser implements UserDetails { + private static final long serialVersionUID = 1L; + + /** + * 用户ID + */ + private Long userId; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 用户唯一标识 + */ + private String token; + + /** + * 登录时间 + */ + private Long loginTime; + + /** + * 过期时间 + */ + private Long expireTime; + + /** + * 登录IP地址 + */ + private String ipaddr; + + /** + * 登录地点 + */ + private String loginLocation; + + /** + * 浏览器类型 + */ + private String browser; + + /** + * 操作系统 + */ + private String os; + + /** + * 权限列表 + */ + private Set permissions; + + /** + * 用户信息 + */ + private SysUser user; + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public Long getDeptId() { + return deptId; + } + + public void setDeptId(Long deptId) { + this.deptId = deptId; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public LoginUser() { + } + + public LoginUser(SysUser user, Set permissions) { + this.user = user; + this.permissions = permissions; + } + + public LoginUser(Long userId, Long deptId, SysUser user, Set permissions) { + this.userId = userId; + this.deptId = deptId; + this.user = user; + this.permissions = permissions; + } + + @JSONField(serialize = false) + @Override + public String getPassword() { + return user.getPassword(); + } + + @Override + public String getUsername() { + return user.getUserName(); + } + + /** + * 账户是否未过期,过期无法验证 + */ + @JSONField(serialize = false) + @Override + public boolean isAccountNonExpired() { + return true; + } + + /** + * 指定用户是否解锁,锁定的用户无法进行身份验证 + * + * @return + */ + @JSONField(serialize = false) + @Override + public boolean isAccountNonLocked() { + return true; + } + + /** + * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证 + * + * @return + */ + @JSONField(serialize = false) + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + /** + * 是否可用 ,禁用的用户不能身份验证 + * + * @return + */ + @JSONField(serialize = false) + @Override + public boolean isEnabled() { + return true; + } + + public Long getLoginTime() { + return loginTime; + } + + public void setLoginTime(Long loginTime) { + this.loginTime = loginTime; + } + + public String getIpaddr() { + return ipaddr; + } + + public void setIpaddr(String ipaddr) { + this.ipaddr = ipaddr; + } + + public String getLoginLocation() { + return loginLocation; + } + + public void setLoginLocation(String loginLocation) { + this.loginLocation = loginLocation; + } + + public String getBrowser() { + return browser; + } + + public void setBrowser(String browser) { + this.browser = browser; + } + + public String getOs() { + return os; + } + + public void setOs(String os) { + this.os = os; + } + + public Long getExpireTime() { + return expireTime; + } + + public void setExpireTime(Long expireTime) { + this.expireTime = expireTime; + } + + public Set getPermissions() { + return permissions; + } + + public void setPermissions(Set permissions) { + this.permissions = permissions; + } + + public SysUser getUser() { + return user; + } + + public void setUser(SysUser user) { + this.user = user; + } + + @Override + public Collection getAuthorities() { + return null; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/domain/model/RegisterBody.java b/jsowell-common/src/main/java/com/jsowell/common/core/domain/model/RegisterBody.java new file mode 100644 index 000000000..77d57be26 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/domain/model/RegisterBody.java @@ -0,0 +1,10 @@ +package com.jsowell.common.core.domain.model; + +/** + * 用户注册对象 + * + * @author jsowell + */ +public class RegisterBody extends LoginBody { + +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/domain/ykc/LoginRequestData.java b/jsowell-common/src/main/java/com/jsowell/common/core/domain/ykc/LoginRequestData.java new file mode 100644 index 000000000..f3dd51468 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/domain/ykc/LoginRequestData.java @@ -0,0 +1,58 @@ +package com.jsowell.common.core.domain.ykc; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class LoginRequestData { + /** + * 桩编码 BCD 码 7 不足 7 位补 0 + */ + private String pileSn; + + /** + * 桩类型 BIN 码 1 0 表示直流桩, 1 表示交流桩 + */ + private String pileType; + + /** + * 充电枪数量 BIN 码 1 + */ + private String connectorNum; + + /** + * 通信协议版本 BIN 码 1 版本号乘 10,v1.0 表示 0x0A + */ + private String communicationVersion; + + /** + * 程序版本 ASCII 码 8 不足 8 位补零 + */ + private String programVersion; + + /** + * 网络链接类型 BIN 码 1 0x00 SIM 卡 + * 0x01 LAN + * 0x02 WAN + * 0x03 其他 + */ + private String internetConnection; + + /** + * Sim 卡 BCD 码 10 不足 10 位补零,取不到置零 + */ + private String iccid; + + /** + * 运营商 BIN 码 1 0x00 移动 + * 0x02 电信 + * 0x03 联通 + * 0x04 其他 + */ + private String business; +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/domain/ykc/RealTimeMonitorData.java b/jsowell-common/src/main/java/com/jsowell/common/core/domain/ykc/RealTimeMonitorData.java new file mode 100644 index 000000000..6788ff5dd --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/domain/ykc/RealTimeMonitorData.java @@ -0,0 +1,124 @@ +package com.jsowell.common.core.domain.ykc; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 实时监测数据 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class RealTimeMonitorData { + /** + * 交易流水号 + */ + private String orderCode; + + /** + * 桩编号 + */ + private String pileSn; + + /** + * 枪口编号 + */ + private String connectorCode; + + /** + * 状态 + */ + private String connectorStatus; + + /** + * 枪是否归位 + */ + private String homingFlag; + + /** + * 充电枪编号 + */ + private String pileConnectorCode; + + /** + * 是否插枪 0x00:否 0x01:是 + */ + private String putGunType; + + /** + * 输出电压 + */ + private String outputVoltage; + + /** + * 输出电流 + */ + private String outputCurrent; + + /** + * 输出功率(由输出电压,输出电流计算得出) + */ + private String outputPower; + + /** + * 枪线温度 + */ + private String gunLineTemperature; + + /** + * 枪线编码 没有置零 + */ + private String gunLineCode; + + /** + * SOC 待机置零;交流桩置零 + */ + private String SOC; + + /** + * 电池组最高温度 + */ + private String batteryMaxTemperature; + + /** + * 累计充电时间 单位: min;待机置零 + */ + private String sumChargingTime; + + /** + * 剩余时间 单位: min;待机置零、交流桩置零 + */ + private String timeRemaining; + + /** + * 充电度数 精确到小数点后四位;待机置零 + */ + private String chargingDegree; + + /** + * 计损充电度数 精确到小数点后四位;待机置零 未设置计损比例时等于充电度数 + */ + private String lossDegree; + + /** + * 已充金额 精确到小数点后四位;待机置零 (电费+服务费) *计损充电度数 + */ + private String chargingAmount; + + /** + * 硬件故障 + */ + private String hardwareFault; + + /** + * 时间 + */ + private String dateTime; + + public String getPileConnectorCode() { + return this.pileSn + this.getConnectorCode(); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/domain/ykc/TransactionRecordsData.java b/jsowell-common/src/main/java/com/jsowell/common/core/domain/ykc/TransactionRecordsData.java new file mode 100644 index 000000000..4557b4090 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/domain/ykc/TransactionRecordsData.java @@ -0,0 +1,157 @@ +package com.jsowell.common.core.domain.ykc; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 交易记录数据对象 + * + * @author JS-ZZA + * @date 2022/11/16 15:01 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TransactionRecordsData { + + // 交易流水号 + private String orderCode; + + // 桩编码 + private String pileSn; + + // 枪号 + private String connectorCode; + + // 开始时间 CP56Time2a 格式 + private String startTime; + + // 结束时间 + private String endTime; + + // 尖单价 精确到小数点后五位(尖电费+尖服务费,见费率帧) + private String sharpPrice; + + // 尖电量 精确到小数点后四位 + private String sharpUsedElectricity; + + // 计损尖电量 + private String sharpPlanLossElectricity; + + // 尖金额 + private String sharpAmount; + + // 峰单价 精确到小数点后五位(峰电费+峰服务费) + private String peakPrice; + + // 峰电量 + private String peakUsedElectricity; + + // 计损峰电量 + private String peakPlanLossElectricity; + + // 峰金额 + private String peakAmount; + + // 平单价 精确到小数点后五位(平电费+平服务费) + private String flatPrice; + + // 平电量 + private String flatUsedElectricity; + + // 计损平电量 + private String flatPlanLossElectricity; + + // 平金额 + private String flatAmount; + + // 谷单价 精确到小数点后五位(谷电费+谷 服务费) + private String valleyPrice; + + // 谷电量 + private String valleyUsedElectricity; + + // 计损谷电量 + private String valleyPlanLossElectricity; + + // 谷金额 + private String valleyAmount; + + // 电表总起值 + private String ammeterTotalStart; + + // 电表总止值 + private String ammeterTotalEnd; + + // 总电量 + private String totalElectricity; + + // 计损总电量 + private String planLossTotalElectricity; + + // 消费金额 精确到小数点后四位,包含电费、 服务费 + private String consumptionAmount; + + // VIN 码 VIN 码,此处 VIN 码和充电时 VIN 码不同, 正序直接上传, 无需补 0 和反序 + private String vinCode; + + /** order_anomaly_record + * 交易标识 + * 0x01: app 启动 + * 0x02:卡启动 + * 0x04:离线卡启动 + * 0x05: vin 码启动充电 + */ + private String transactionIdentifier; + + // 交易时间 CP56Time2a 格式 + private String transactionTime; + + // 停止原因 + private String stopReasonMsg; + + // 物理卡号 不足 8 位补 0 + private String logicCard; + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("orderCode", orderCode) + .append("pileSn", pileSn) + .append("connectorCode", connectorCode) + .append("startTime", startTime) + .append("endTime", endTime) + .append("sharpPrice", sharpPrice) + .append("sharpUsedElectricity", sharpUsedElectricity) + .append("sharpPlanLossElectricity", sharpPlanLossElectricity) + .append("sharpAmount", sharpAmount) + .append("peakPrice", peakPrice) + .append("peakUsedElectricity", peakUsedElectricity) + .append("peakPlanLossElectricity", peakPlanLossElectricity) + .append("peakAmount", peakAmount) + .append("flatPrice", flatPrice) + .append("flatUsedElectricity", flatUsedElectricity) + .append("flatPlanLossElectricity", flatPlanLossElectricity) + .append("flatAmount", flatAmount) + .append("valleyPrice", valleyPrice) + .append("valleyUsedElectricity", valleyUsedElectricity) + .append("valleyPlanLossElectricity", valleyPlanLossElectricity) + .append("valleyAmount", valleyAmount) + .append("ammeterTotalStart", ammeterTotalStart) + .append("ammeterTotalEnd", ammeterTotalEnd) + .append("totalElectricity", totalElectricity) + .append("planLossTotalElectricity", planLossTotalElectricity) + .append("consumptionAmount", consumptionAmount) + .append("vinCode", vinCode) + .append("transactionIdentifier", transactionIdentifier) + .append("transactionTime", transactionTime) + .append("stopReasonMsg", stopReasonMsg) + .append("logicCard", logicCard) + .toString(); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/domain/ykc/YKCDataProtocol.java b/jsowell-common/src/main/java/com/jsowell/common/core/domain/ykc/YKCDataProtocol.java new file mode 100644 index 000000000..67de4b837 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/domain/ykc/YKCDataProtocol.java @@ -0,0 +1,73 @@ +package com.jsowell.common.core.domain.ykc; + +import com.google.common.primitives.Bytes; +import com.jsowell.common.util.BytesUtil; +import lombok.Data; + +/** + * 云快充数据模板 + */ +@Data +public class YKCDataProtocol { + /** + * 起始标志 1字节 + */ + private byte[] head; + + /** + * 数据长度 1字节 + */ + private byte[] length; + + /** + * 序列号域 2字节 + */ + private byte[] serialNumber; + + /** + * 加密标志 1 字节 + */ + private byte[] encryptFlag; + + /** + * 帧类型标志 1 字节 + */ + private byte[] frameType; + + /** + * 消息体 N字节 + */ + private byte[] msgBody; + + /** + * 帧校验域 2字节 + */ + private byte[] crcByte; + + public YKCDataProtocol(byte[] msg) { + // 起始标志 + this.head = BytesUtil.copyBytes(msg, 0, 1); + // 数据长度 + this.length = BytesUtil.copyBytes(msg, 1, 1); + // 序列号域 + this.serialNumber = BytesUtil.copyBytes(msg, 2, 2); + // 加密标志 + this.encryptFlag = BytesUtil.copyBytes(msg, 4, 1); + // 帧类型标志 + this.frameType = BytesUtil.copyBytes(msg, 5, 1); + // 消息体 + this.msgBody = BytesUtil.copyBytes(msg, 6, msg.length - 8); + // 帧校验域 + this.crcByte = new byte[]{msg[msg.length - 2], msg[msg.length - 1]}; + } + + /** + * 转换为十六进制字符串 + * + * @return 报文 + */ + public String getHEXString() { + byte[] bytes = Bytes.concat(this.head, this.length, this.serialNumber, this.encryptFlag, this.frameType, this.msgBody, this.crcByte); + return BytesUtil.binary(bytes, 16); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/domain/ykc/YKCFrameTypeCode.java b/jsowell-common/src/main/java/com/jsowell/common/core/domain/ykc/YKCFrameTypeCode.java new file mode 100644 index 000000000..4e8c5a4e2 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/domain/ykc/YKCFrameTypeCode.java @@ -0,0 +1,203 @@ +package com.jsowell.common.core.domain.ykc; + +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.YKCUtils; + +/** + * 云快充 帧类型码 + * FrameTypeCode + * frame + */ +public enum YKCFrameTypeCode { + + LOGIN_CODE(0x01, "充电桩登录认证"), + LOGIN_ANSWER_CODE(0x02, "登录认证应答"), + + HEART_BEAT_CODE(0x03, "充电桩心跳包"), + HEART_BEAT_ANSWER_CODE(0x04, "心跳包应答"), + + BILLING_TEMPLATE_VALIDATE_CODE(0x05, "计费模型验证请求"), + BILLING_TEMPLATE_VALIDATE_ANSWER_CODE(0x06, "计费模型验证请求应答"), + + BILLING_TEMPLATE_CODE(0x09, "充电桩计费模型请求"), + BILLING_TEMPLATE_ANSWER_CODE(0x0A, "计费模型请求应答"), + + UPLOAD_REAL_TIME_MONITOR_DATA_OLD_VERSION_CODE(0x11, "上传实时监测数据V1.3"), + READ_REAL_TIME_MONITOR_DATA_CODE(0x12, "读取实时监测数据"), + UPLOAD_REAL_TIME_MONITOR_DATA_CODE(0x13, "上传实时监测数据"), + + CHARGING_HANDSHAKE_CODE(0x15, "充电握手"), + PARAMETER_CONFIGURATION_CODE(0x17, "参数配置"), + CHARGE_END_CODE(0X19, "充电结束"), + ERROR_MESSAGE_CODE(0x1B, "错误报文"), + BMS_ABORT_DURING_CHARGING_PHASE_CODE(0x1D, "充电阶段BMS中止"), + THE_CHARGER_IS_ABORTED_DURING_THE_CHARGING_PHASE_CODE(0X21, "充电阶段充电机中止"), + CHARGING_PROCESS_BMS_DEMAND_AND_CHARGER_OUTPUT_CODE(0X23, "充电过程 BMS 需求与充电机输出"), + CHARGING_PROCESS_BMS_INFORMATION_CODE(0X25, "充电过程 BMS 信息"), + + REQUEST_START_CHARGING_CODE(0x31, "充电桩主动申请启动充电"), + CONFIRM_START_CHARGING_CODE(0x32, "运营平台确认启动充电"), + + REMOTE_START_CHARGING_ANSWER_CODE(0x33, "远程启动充电命令回复"), + REMOTE_CONTROL_START_CODE(0x34, "运营平台远程控制启机"), + + REMOTE_STOP_CHARGING_ANSWER_CODE(0x35, "远程停机命令回复"), + REMOTE_STOP_CHARGING_CODE(0x36, "运营平台远程停机"), + + + TRANSACTION_RECORDS_CODE(0x3B, "交易记录"), + TRANSACTION_RECORDS_OLD_VERSION_CODE(0x39, "交易记录V1.3"), + TRANSACTION_RECORDS_CONFIRM_CODE(0x40, "交易记录确认"), + + REMOTE_ACCOUNT_BALANCE_UPDATE_CODE(0x42, "远程账户余额更新"), + REMOTE_ACCOUNT_BALANCE_UPDATE_ANSWER_CODE(0x41, "余额更新应答"), + + OFFLINE_CARD_DATA_SYNCHRONIZATION_CODE(0x44, "离线卡数据同步"), + OFFLINE_CARD_DATA_SYNCHRONIZATION_ANSWER_CODE(0x43, "离线卡数据同步应答"), + + OFFLINE_CARD_DATA_CLEANING_CODE(0x46, "离线卡数据清除"), + OFFLINE_CARD_DATA_CLEANING_ANSWER_CODE(0x45, "离线卡数据清除应答"), + + OFFLINE_CARD_DATA_QUERY_CODE(0x48, "离线卡数据查询"), + OFFLINE_CARD_DATA_QUERY_ANSWER_CODE(0x47, "离线卡数据查询应答"), + + CHARGING_PILE_WORKING_PARAMETER_SETTING_CODE(0x52, "充电桩工作参数设置"), + CHARGING_PILE_WORKING_PARAMETER_SETTING_ANSWER_CODE(0x51, "充电桩工作参数设置应答"), + + TIME_CHECK_SETTING_CODE(0x56, "对时设置"), + TIME_CHECK_SETTING_ANSWER_CODE(0x55, "对时设置应答"), + + BILLING_TEMPLATE_SETTING_CODE(0x58, "计费模型设置"), + BILLING_TEMPLATE_SETTING_ANSWER_CODE(0x57, "计费模型设置应答"), + + GROUND_LOCK_DATA_UPLOAD_CODE(0x61, "地锁数据上送"), + REMOTE_CONTROL_GROUND_LOCK_LIFTING_CODE(0x62, "遥控地锁升降"), + CHARGING_PILE_RESPOND_GROUND_LOCK_LIFTING_CODE(0X63, "充电桩响应地锁升降数据"), + + REMOTE_RESTART_CODE(0x92, "远程重启"), + REMOTE_RESTART_ANSWER_CODE(0x91, "远程重启应答"), + + REMOTE_UPDATE_CODE(0x94, "远程更新"), + REMOTE_UPDATE_ANSWER_CODE(0x93, "远程更新应答"), + + REMOTE_ISSUE_QRCODE_CODE(0xF0, "后台远程下发二维码前缀指令"), + REMOTE_ISSUE_QRCODE_ANSWER_CODE(0xF1, "桩应答远程下发二维码前缀指令"), + + // 自定义FrameType + PILE_LOG_OUT(9999, "充电桩退出"), + + + ; + + YKCFrameTypeCode(int code, String value) { + this.code = code; + this.value = value; + } + + private int code; + private String value; + + public int getCode() { + return code; + } + + public String getValue() { + return value; + } + + public byte[] getBytes() { + return BytesUtil.intToBytesLittle(code, 1); + } + + public static void main(String[] args) { + byte[] bytes = BytesUtil.intToBytesLittle(9999, 1); + System.out.println(YKCUtils.frameType2Str(bytes)); + } + + public static YKCFrameTypeCode fromCode(byte code) { + for (YKCFrameTypeCode item : YKCFrameTypeCode.values()) { + if (item.getCode() == code) { + return item; + } + } + return null; + } + + public static String getFrameTypeStr(String frameType) { + for (YKCFrameTypeCode item : YKCFrameTypeCode.values()) { + String str = YKCUtils.frameType2Str(item.getBytes()); + if (StringUtils.equals(frameType, str)) { + return item.getValue(); + } + } + return ""; + } + + /** + * 请求应答 帧类型关系 + */ + public enum ResponseRelation { + // 登录 + LOGIN(LOGIN_CODE.getCode(), LOGIN_ANSWER_CODE.getCode()), + // 心跳 + HEART_BEAT(HEART_BEAT_CODE.getCode(), HEART_BEAT_ANSWER_CODE.getCode()), + // 计费模板验证 + BILLING_TEMPLATE_VALIDATE(BILLING_TEMPLATE_VALIDATE_CODE.getCode(), BILLING_TEMPLATE_VALIDATE_ANSWER_CODE.getCode()), + // 计费模板请求 + BILLING_TEMPLATE(BILLING_TEMPLATE_CODE.getCode(), BILLING_TEMPLATE_ANSWER_CODE.getCode()), + // 请求开始充电 + START_CHARGING(REQUEST_START_CHARGING_CODE.getCode(), CONFIRM_START_CHARGING_CODE.getCode()), + // 远程请求充电 + REMOTE_START_CHARGING(REMOTE_CONTROL_START_CODE.getCode(), REMOTE_START_CHARGING_ANSWER_CODE.getCode()), + // 远程停止充电 + REMOTE_STOP_CHARGING(REMOTE_STOP_CHARGING_CODE.getCode(), REMOTE_STOP_CHARGING_ANSWER_CODE.getCode()), + // 交易记录 + TRANSACTION_RECORDS(TRANSACTION_RECORDS_CODE.getCode(), TRANSACTION_RECORDS_CONFIRM_CODE.getCode()), + // 远程账户更新 + REMOTE_ACCOUNT_BALANCE_UPDATE(REMOTE_ACCOUNT_BALANCE_UPDATE_CODE.getCode(), REMOTE_ACCOUNT_BALANCE_UPDATE_ANSWER_CODE.getCode()); + + // 请求帧类型 + private int requestFrameType; + + // 响应帧类型 + private int responseFrameType; + + public int getRequestFrameType() { + return requestFrameType; + } + + public void setRequestFrameType(int requestFrameType) { + this.requestFrameType = requestFrameType; + } + + public int getResponseFrameType() { + return responseFrameType; + } + + public void setResponseFrameType(int responseFrameType) { + this.responseFrameType = responseFrameType; + } + + ResponseRelation(int requestFrameType, int responseFrameType) { + this.requestFrameType = requestFrameType; + this.responseFrameType = responseFrameType; + } + + // 根据请求帧类型 获取应答帧类型 + public static int getResponseFrameType(int requestFrameType) { + for (ResponseRelation responseRelation : ResponseRelation.values()) { + if (responseRelation.getRequestFrameType() == requestFrameType) { + return responseRelation.getResponseFrameType(); + } + } + return 0; + } + + public static byte[] getResponseFrameType(byte[] requestFrameType) { + int frameType = BytesUtil.bytesToInt(requestFrameType); + return BytesUtil.intToBytes(getResponseFrameType(frameType), 1); + } + } + +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/page/PageDomain.java b/jsowell-common/src/main/java/com/jsowell/common/core/page/PageDomain.java new file mode 100644 index 000000000..4af749f79 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/page/PageDomain.java @@ -0,0 +1,93 @@ +package com.jsowell.common.core.page; + +import com.jsowell.common.util.StringUtils; + +/** + * 分页数据 + * + * @author jsowell + */ +public class PageDomain { + /** + * 当前记录起始索引 + */ + private Integer pageNum; + + /** + * 每页显示记录数 + */ + private Integer pageSize; + + /** + * 排序列 + */ + private String orderByColumn; + + /** + * 排序的方向desc或者asc + */ + private String isAsc = "asc"; + + /** + * 分页参数合理化 + */ + private Boolean reasonable = true; + + public String getOrderBy() { + if (StringUtils.isEmpty(orderByColumn)) { + return ""; + } + return StringUtils.toUnderScoreCase(orderByColumn) + " " + isAsc; + } + + public Integer getPageNum() { + return pageNum; + } + + public void setPageNum(Integer pageNum) { + this.pageNum = pageNum; + } + + public Integer getPageSize() { + return pageSize; + } + + public void setPageSize(Integer pageSize) { + this.pageSize = pageSize; + } + + public String getOrderByColumn() { + return orderByColumn; + } + + public void setOrderByColumn(String orderByColumn) { + this.orderByColumn = orderByColumn; + } + + public String getIsAsc() { + return isAsc; + } + + public void setIsAsc(String isAsc) { + if (StringUtils.isNotEmpty(isAsc)) { + // 兼容前端排序类型 + if ("ascending".equals(isAsc)) { + isAsc = "asc"; + } else if ("descending".equals(isAsc)) { + isAsc = "desc"; + } + this.isAsc = isAsc; + } + } + + public Boolean getReasonable() { + if (StringUtils.isNull(reasonable)) { + return Boolean.TRUE; + } + return reasonable; + } + + public void setReasonable(Boolean reasonable) { + this.reasonable = reasonable; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/page/PageResponse.java b/jsowell-common/src/main/java/com/jsowell/common/core/page/PageResponse.java new file mode 100644 index 000000000..3d5496e0c --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/page/PageResponse.java @@ -0,0 +1,42 @@ +package com.jsowell.common.core.page; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PageResponse implements Serializable { + private static final long serialVersionUID = -8561048121257788971L; + + /** + * 页码 + */ + private int pageNum; + + /** + * 每页数量 + */ + private int pageSize; + + /** + * 数据集合 + */ + private List list; + + /** + * 结果总数 + */ + private long total; + + /** + * 结果总页数 + */ + private int pages; +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/page/TableDataInfo.java b/jsowell-common/src/main/java/com/jsowell/common/core/page/TableDataInfo.java new file mode 100644 index 000000000..36c7e551d --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/page/TableDataInfo.java @@ -0,0 +1,82 @@ +package com.jsowell.common.core.page; + +import java.io.Serializable; +import java.util.List; + +/** + * 表格分页数据对象 + * + * @author jsowell + */ +public class TableDataInfo implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 总记录数 + */ + private long total; + + /** + * 列表数据 + */ + private List rows; + + /** + * 消息状态码 + */ + private int code; + + /** + * 消息内容 + */ + private String msg; + + /** + * 表格数据对象 + */ + public TableDataInfo() { + } + + /** + * 分页 + * + * @param list 列表数据 + * @param total 总记录数 + */ + public TableDataInfo(List list, int total) { + this.rows = list; + this.total = total; + } + + public long getTotal() { + return total; + } + + public void setTotal(long total) { + this.total = total; + } + + public List getRows() { + return rows; + } + + public void setRows(List rows) { + this.rows = rows; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/page/TableSupport.java b/jsowell-common/src/main/java/com/jsowell/common/core/page/TableSupport.java new file mode 100644 index 000000000..630c2f733 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/page/TableSupport.java @@ -0,0 +1,53 @@ +package com.jsowell.common.core.page; + +import com.jsowell.common.core.text.Convert; +import com.jsowell.common.util.ServletUtils; + +/** + * 表格数据处理 + * + * @author jsowell + */ +public class TableSupport { + /** + * 当前记录起始索引 + */ + public static final String PAGE_NUM = "pageNum"; + + /** + * 每页显示记录数 + */ + public static final String PAGE_SIZE = "pageSize"; + + /** + * 排序列 + */ + public static final String ORDER_BY_COLUMN = "orderByColumn"; + + /** + * 排序的方向 "desc" 或者 "asc". + */ + public static final String IS_ASC = "isAsc"; + + /** + * 分页参数合理化 + */ + public static final String REASONABLE = "reasonable"; + + /** + * 封装分页对象 + */ + public static PageDomain getPageDomain() { + PageDomain pageDomain = new PageDomain(); + pageDomain.setPageNum(Convert.toInt(ServletUtils.getParameter(PAGE_NUM), 1)); + pageDomain.setPageSize(Convert.toInt(ServletUtils.getParameter(PAGE_SIZE), 10)); + pageDomain.setOrderByColumn(ServletUtils.getParameter(ORDER_BY_COLUMN)); + pageDomain.setIsAsc(ServletUtils.getParameter(IS_ASC)); + pageDomain.setReasonable(ServletUtils.getParameterToBool(REASONABLE)); + return pageDomain; + } + + public static PageDomain buildPageRequest() { + return getPageDomain(); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/redis/RedisCache.java b/jsowell-common/src/main/java/com/jsowell/common/core/redis/RedisCache.java new file mode 100644 index 000000000..b355af15c --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/redis/RedisCache.java @@ -0,0 +1,504 @@ +package com.jsowell.common.core.redis; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.BoundSetOperations; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.HashOperations; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * spring redis 工具类 + * + * @author jsowell + **/ +@SuppressWarnings(value = {"unchecked", "rawtypes"}) +@Component +public class RedisCache { + + Logger logger = LoggerFactory.getLogger(RedisCache.class); + + @Autowired + public RedisTemplate redisTemplate; + + // redis锁获取超时时间 + private long timeout = 500l; + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + */ + public void setCacheObject(final String key, final T value) { + redisTemplate.opsForValue().set(key, value); + } + + /** + * 设置缓存 + * + * @param key 缓存的键值 + * @param value 缓存的值 + * @param timeout 超时时间 单位秒 + */ + public void setCacheObject(final String key, final T value, final Integer timeout) { + // redisTemplate.opsForValue().set(key, value, timeout, timeUnit); + setCacheObject(key, value, timeout, TimeUnit.SECONDS); + } + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + * @param timeout 时间 + * @param timeUnit 时间颗粒度 + */ + public void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) { + redisTemplate.opsForValue().set(key, value, timeout, timeUnit); + } + + /** + * 设置有效时间 + * + * @param key Redis键 + * @param timeout 超时时间 + * @return true=设置成功;false=设置失败 + */ + public boolean expire(final String key, final long timeout) { + return expire(key, timeout, TimeUnit.SECONDS); + } + + /** + * 设置有效时间 + * + * @param key Redis键 + * @param timeout 超时时间 + * @param unit 时间单位 + * @return true=设置成功;false=设置失败 + */ + public boolean expire(final String key, final long timeout, final TimeUnit unit) { + return redisTemplate.expire(key, timeout, unit); + } + + /** + * 获取有效时间 + * + * @param key Redis键 + * @return 有效时间 + */ + public long getExpire(final String key) { + return redisTemplate.getExpire(key); + } + + /** + * 判断 key是否存在 + * + * @param key 键 + * @return true 存在 false不存在 + */ + public Boolean hasKey(String key) { + return redisTemplate.hasKey(key); + } + + /** + * 获得缓存的基本对象。 + * + * @param key 缓存键值 + * @return 缓存键值对应的数据 + */ + public T getCacheObject(final String key) { + ValueOperations operation = redisTemplate.opsForValue(); + return operation.get(key); + } + + /** + * 删除单个对象 + * + * @param key + */ + public boolean deleteObject(final String key) { + return redisTemplate.delete(key); + } + + /** + * 删除集合对象 + * + * @param collection 多个对象 + * @return + */ + public long deleteObject(final Collection collection) { + return redisTemplate.delete(collection); + } + + /** + * 缓存List数据 + * + * @param key 缓存的键值 + * @param dataList 待缓存的List数据 + * @return 缓存的对象 + */ + public long setCacheList(final String key, final List dataList) { + Long count = redisTemplate.opsForList().rightPushAll(key, dataList); + return count == null ? 0 : count; + } + + /** + * 获得缓存的list对象 + * + * @param key 缓存的键值 + * @return 缓存键值对应的数据 + */ + public List getCacheList(final String key) { + return redisTemplate.opsForList().range(key, 0, -1); + } + + /** + * 批量获取缓存 + * + * @param keys + * @param + * @return + */ + public List multiGet(final List keys) { + return redisTemplate.opsForValue().multiGet(keys); + } + + /** + * 缓存Set + * + * @param key 缓存键值 + * @param dataSet 缓存的数据 + * @return 缓存数据的对象 + */ + public BoundSetOperations setCacheSet(final String key, final Set dataSet) { + BoundSetOperations setOperation = redisTemplate.boundSetOps(key); + Iterator it = dataSet.iterator(); + while (it.hasNext()) { + setOperation.add(it.next()); + } + return setOperation; + } + + /** + * 获得缓存的set + * + * @param key + * @return + */ + public Set getCacheSet(final String key) { + return redisTemplate.opsForSet().members(key); + } + + /** + * 缓存Map + * + * @param key + * @param dataMap + */ + public void setCacheMap(final String key, final Map dataMap) { + if (dataMap != null) { + redisTemplate.opsForHash().putAll(key, dataMap); + } + } + + /** + * 获得缓存的Map + * + * @param key + * @return + */ + public Map getCacheMap(final String key) { + return redisTemplate.opsForHash().entries(key); + } + + /** + * 往Hash中存入数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @param value 值 + */ + public void setCacheMapValue(final String key, final String hKey, final T value) { + redisTemplate.opsForHash().put(key, hKey, value); + } + + /** + * 获取Hash中的数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @return Hash中的对象 + */ + public T getCacheMapValue(final String key, final String hKey) { + HashOperations opsForHash = redisTemplate.opsForHash(); + return opsForHash.get(key, hKey); + } + + /** + * 删除Hash中的数据 + * + * @param key + * @param hKey + */ + public void delCacheMapValue(final String key, final String hKey) { + HashOperations hashOperations = redisTemplate.opsForHash(); + hashOperations.delete(key, hKey); + } + + /** + * 获取多个Hash中的数据 + * + * @param key Redis键 + * @param hKeys Hash键集合 + * @return Hash对象集合 + */ + public List getMultiCacheMapValue(final String key, final Collection hKeys) { + return redisTemplate.opsForHash().multiGet(key, hKeys); + } + + /** + * 删除Hash中的某条数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @return 是否成功 + */ + public boolean deleteCacheMapValue(final String key, final String hKey) { + return Boolean.TRUE.equals(redisTemplate.opsForHash().delete(key, hKey)); + } + + /** + * 获得缓存的基本对象列表 + * + * @param pattern 字符串前缀 + * @return 对象列表 + */ + public Collection keys(final String pattern) { + return redisTemplate.keys(pattern); + } + + public Long increment(final String key, final long delta) { + return redisTemplate.opsForValue().increment(key, delta); + } + + /** + * scan 实现 + * + * @param pattern 表达式,如:abc*,找出所有以abc开始的键 + */ + public Set scan(String pattern) { + return (Set) redisTemplate.execute((RedisCallback>) connection -> { + Set keysTmp = new HashSet<>(); + try (Cursor cursor = connection.scan(new ScanOptions.ScanOptionsBuilder() + .match(pattern) + .count(10000).build())) { + + while (cursor.hasNext()) { + keysTmp.add(new String(cursor.next(), "Utf-8")); + } + } catch (Exception e) { + logger.error(e.getMessage(), e); + throw new RuntimeException(e); + } + return keysTmp; + }); + } + + /** + * 加锁,无阻塞 + * + * @param lockKey 锁 + * @param requestId 请求标识 + * @param expireTime 超期时间 + * @return 是否获取成功 + */ + public Boolean lock(String lockKey, String requestId, long expireTime) { + Long start = System.currentTimeMillis(); + //在一定时间内获取锁,超时则返回错误 + for (; ; ) { + //Set命令返回OK,则证明获取锁成功 + Boolean ret = redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, expireTime, TimeUnit.SECONDS); + if (ret != null && ret) { + return true; + } + //否则循环等待,在timeout时间内仍未获取到锁,则获取失败 + long end = System.currentTimeMillis() - start; + if (end >= timeout) { + return false; + } + } + } + + /** + * 根据key'删除锁 + * + * @param lockKey + */ + public void unLock(String lockKey) { + // 删除key即可释放锁 + redisTemplate.delete(lockKey); + } + + // ================================Map================================= + + /** + * HashGet + * + * @param key 键 不能为null + * @param item 项 不能为null + */ + public Object hget(String key, String item) { + return redisTemplate.opsForHash().get(key, item); + } + + /** + * 获取hashKey对应的所有键值 + * + * @param key 键 + * @return 对应的多个键值 + */ + public Map hmget(String key) { + return redisTemplate.opsForHash().entries(key); + } + + /** + * HashSet + * + * @param key 键 + * @param map 对应多个键值 + */ + public boolean hmset(String key, Map map) { + try { + redisTemplate.opsForHash().putAll(key, map); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + + /** + * HashSet 并设置时间 + * + * @param key 键 + * @param map 对应多个键值 + * @param time 时间(秒) + * @return true成功 false失败 + */ + public boolean hmset(String key, Map map, long time, TimeUnit timeUnit) { + try { + redisTemplate.opsForHash().putAll(key, map); + if (time > 0) { + expire(key, time, timeUnit); + } + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + + /** + * 向一张hash表中放入数据,如果不存在将创建 + * + * @param key 键 + * @param item 项 + * @param value 值 + * @return true 成功 false失败 + */ + public boolean hset(String key, String item, Object value) { + try { + redisTemplate.opsForHash().put(key, item, value); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + /** + * 向一张hash表中放入数据,如果不存在将创建 + * + * @param key 键 + * @param item 项 + * @param value 值 + * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间 + * @return true 成功 false失败 + */ + public boolean hset(String key, String item, Object value, long time, TimeUnit timeUnit) { + try { + redisTemplate.opsForHash().put(key, item, value); + if (time > 0) { + expire(key, time, timeUnit); + } + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + + /** + * 删除hash表中的值 + * + * @param key 键 不能为null + * @param item 项 可以使多个 不能为null + */ + public void hdel(String key, Object... item) { + redisTemplate.opsForHash().delete(key, item); + } + + + /** + * 判断hash表中是否有该项的值 + * + * @param key 键 不能为null + * @param item 项 不能为null + * @return true 存在 false不存在 + */ + public boolean hHasKey(String key, String item) { + return redisTemplate.opsForHash().hasKey(key, item); + } + + + /** + * hash递增 如果不存在,就会创建一个 并把新增后的值返回 + * + * @param key 键 + * @param item 项 + * @param by 要增加几(大于0) + */ + public double hincr(String key, String item, double by) { + return redisTemplate.opsForHash().increment(key, item, by); + } + + + /** + * hash递减 + * + * @param key 键 + * @param item 项 + * @param by 要减少记(小于0) + */ + public double hdecr(String key, String item, double by) { + return redisTemplate.opsForHash().increment(key, item, -by); + } + +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/text/CharsetKit.java b/jsowell-common/src/main/java/com/jsowell/common/core/text/CharsetKit.java new file mode 100644 index 000000000..f065ee589 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/text/CharsetKit.java @@ -0,0 +1,91 @@ +package com.jsowell.common.core.text; + +import com.jsowell.common.util.StringUtils; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * 字符集工具类 + * + * @author jsowell + */ +public class CharsetKit { + /** + * ISO-8859-1 + */ + public static final String ISO_8859_1 = "ISO-8859-1"; + /** + * UTF-8 + */ + public static final String UTF_8 = "UTF-8"; + /** + * GBK + */ + public static final String GBK = "GBK"; + + /** + * ISO-8859-1 + */ + public static final Charset CHARSET_ISO_8859_1 = Charset.forName(ISO_8859_1); + /** + * UTF-8 + */ + public static final Charset CHARSET_UTF_8 = Charset.forName(UTF_8); + /** + * GBK + */ + public static final Charset CHARSET_GBK = Charset.forName(GBK); + + /** + * 转换为Charset对象 + * + * @param charset 字符集,为空则返回默认字符集 + * @return Charset + */ + public static Charset charset(String charset) { + return StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset); + } + + /** + * 转换字符串的字符集编码 + * + * @param source 字符串 + * @param srcCharset 源字符集,默认ISO-8859-1 + * @param destCharset 目标字符集,默认UTF-8 + * @return 转换后的字符集 + */ + public static String convert(String source, String srcCharset, String destCharset) { + return convert(source, Charset.forName(srcCharset), Charset.forName(destCharset)); + } + + /** + * 转换字符串的字符集编码 + * + * @param source 字符串 + * @param srcCharset 源字符集,默认ISO-8859-1 + * @param destCharset 目标字符集,默认UTF-8 + * @return 转换后的字符集 + */ + public static String convert(String source, Charset srcCharset, Charset destCharset) { + if (null == srcCharset) { + srcCharset = StandardCharsets.ISO_8859_1; + } + + if (null == destCharset) { + destCharset = StandardCharsets.UTF_8; + } + + if (StringUtils.isEmpty(source) || srcCharset.equals(destCharset)) { + return source; + } + return new String(source.getBytes(srcCharset), destCharset); + } + + /** + * @return 系统字符集编码 + */ + public static String systemCharset() { + return Charset.defaultCharset().name(); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/text/Convert.java b/jsowell-common/src/main/java/com/jsowell/common/core/text/Convert.java new file mode 100644 index 000000000..66c5da536 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/text/Convert.java @@ -0,0 +1,849 @@ +package com.jsowell.common.core.text; + +import com.jsowell.common.util.StringUtils; +import org.apache.commons.lang3.ArrayUtils; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.text.NumberFormat; +import java.util.Set; + +/** + * 类型转换器 + * + * @author jsowell + */ +public class Convert { + /** + * 转换为字符串
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static String toStr(Object value, String defaultValue) { + if (null == value) { + return defaultValue; + } + if (value instanceof String) { + return (String) value; + } + return value.toString(); + } + + /** + * 转换为字符串
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static String toStr(Object value) { + return toStr(value, null); + } + + /** + * 转换为字符
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Character toChar(Object value, Character defaultValue) { + if (null == value) { + return defaultValue; + } + if (value instanceof Character) { + return (Character) value; + } + + final String valueStr = toStr(value, null); + return StringUtils.isEmpty(valueStr) ? defaultValue : valueStr.charAt(0); + } + + /** + * 转换为字符
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Character toChar(Object value) { + return toChar(value, null); + } + + /** + * 转换为byte
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Byte toByte(Object value, Byte defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof Byte) { + return (Byte) value; + } + if (value instanceof Number) { + return ((Number) value).byteValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + return Byte.parseByte(valueStr); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为byte
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Byte toByte(Object value) { + return toByte(value, null); + } + + /** + * 转换为Short
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Short toShort(Object value, Short defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof Short) { + return (Short) value; + } + if (value instanceof Number) { + return ((Number) value).shortValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + return Short.parseShort(valueStr.trim()); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为Short
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Short toShort(Object value) { + return toShort(value, null); + } + + /** + * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Number toNumber(Object value, Number defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof Number) { + return (Number) value; + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + return NumberFormat.getInstance().parse(valueStr); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Number toNumber(Object value) { + return toNumber(value, null); + } + + /** + * 转换为int
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Integer toInt(Object value, Integer defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof Integer) { + return (Integer) value; + } + if (value instanceof Number) { + return ((Number) value).intValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + return Integer.parseInt(valueStr.trim()); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为int
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Integer toInt(Object value) { + return toInt(value, null); + } + + /** + * 转换为Integer数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String str) { + return toIntArray(",", str); + } + + /** + * 转换为Long数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static Long[] toLongArray(String str) { + return toLongArray(",", str); + } + + /** + * 转换为Integer数组
+ * + * @param split 分隔符 + * @param split 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String split, String str) { + if (StringUtils.isEmpty(str)) { + return new Integer[]{}; + } + String[] arr = str.split(split); + final Integer[] ints = new Integer[arr.length]; + for (int i = 0; i < arr.length; i++) { + final Integer v = toInt(arr[i], 0); + ints[i] = v; + } + return ints; + } + + /** + * 转换为Long数组
+ * + * @param split 分隔符 + * @param str 被转换的值 + * @return 结果 + */ + public static Long[] toLongArray(String split, String str) { + if (StringUtils.isEmpty(str)) { + return new Long[]{}; + } + String[] arr = str.split(split); + final Long[] longs = new Long[arr.length]; + for (int i = 0; i < arr.length; i++) { + final Long v = toLong(arr[i], null); + longs[i] = v; + } + return longs; + } + + /** + * 转换为String数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String str) { + return toStrArray(",", str); + } + + /** + * 转换为String数组
+ * + * @param split 分隔符 + * @param split 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String split, String str) { + return str.split(split); + } + + /** + * 转换为long
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Long toLong(Object value, Long defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof Long) { + return (Long) value; + } + if (value instanceof Number) { + return ((Number) value).longValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + // 支持科学计数法 + return new BigDecimal(valueStr.trim()).longValue(); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为long
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Long toLong(Object value) { + return toLong(value, null); + } + + /** + * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Double toDouble(Object value, Double defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof Double) { + return (Double) value; + } + if (value instanceof Number) { + return ((Number) value).doubleValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + // 支持科学计数法 + return new BigDecimal(valueStr.trim()).doubleValue(); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Double toDouble(Object value) { + return toDouble(value, null); + } + + /** + * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Float toFloat(Object value, Float defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof Float) { + return (Float) value; + } + if (value instanceof Number) { + return ((Number) value).floatValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + return Float.parseFloat(valueStr.trim()); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Float toFloat(Object value) { + return toFloat(value, null); + } + + /** + * 转换为boolean
+ * String支持的值为:true、false、yes、ok、no,1,0 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Boolean toBool(Object value, Boolean defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof Boolean) { + return (Boolean) value; + } + String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + valueStr = valueStr.trim().toLowerCase(); + switch (valueStr) { + case "true": + case "yes": + case "ok": + case "1": + return true; + case "false": + case "no": + case "0": + return false; + default: + return defaultValue; + } + } + + /** + * 转换为boolean
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Boolean toBool(Object value) { + return toBool(value, null); + } + + /** + * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * + * @param clazz Enum的Class + * @param value 值 + * @param defaultValue 默认值 + * @return Enum + */ + public static > E toEnum(Class clazz, Object value, E defaultValue) { + if (value == null) { + return defaultValue; + } + if (clazz.isAssignableFrom(value.getClass())) { + @SuppressWarnings("unchecked") + E myE = (E) value; + return myE; + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + return Enum.valueOf(clazz, valueStr); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * + * @param clazz Enum的Class + * @param value 值 + * @return Enum + */ + public static > E toEnum(Class clazz, Object value) { + return toEnum(clazz, value, null); + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static BigInteger toBigInteger(Object value, BigInteger defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof BigInteger) { + return (BigInteger) value; + } + if (value instanceof Long) { + return BigInteger.valueOf((Long) value); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + return new BigInteger(valueStr); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static BigInteger toBigInteger(Object value) { + return toBigInteger(value, null); + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static BigDecimal toBigDecimal(Object value, BigDecimal defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof BigDecimal) { + return (BigDecimal) value; + } + if (value instanceof Long) { + return new BigDecimal((Long) value); + } + if (value instanceof Double) { + return new BigDecimal((Double) value); + } + if (value instanceof Integer) { + return new BigDecimal((Integer) value); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + return new BigDecimal(valueStr); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static BigDecimal toBigDecimal(Object value) { + return toBigDecimal(value, null); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @return 字符串 + */ + public static String utf8Str(Object obj) { + return str(obj, CharsetKit.CHARSET_UTF_8); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @param charsetName 字符集 + * @return 字符串 + */ + public static String str(Object obj, String charsetName) { + return str(obj, Charset.forName(charsetName)); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @param charset 字符集 + * @return 字符串 + */ + public static String str(Object obj, Charset charset) { + if (null == obj) { + return null; + } + + if (obj instanceof String) { + return (String) obj; + } else if (obj instanceof byte[]) { + return str((byte[]) obj, charset); + } else if (obj instanceof Byte[]) { + byte[] bytes = ArrayUtils.toPrimitive((Byte[]) obj); + return str(bytes, charset); + } else if (obj instanceof ByteBuffer) { + return str((ByteBuffer) obj, charset); + } + return obj.toString(); + } + + /** + * 将byte数组转为字符串 + * + * @param bytes byte数组 + * @param charset 字符集 + * @return 字符串 + */ + public static String str(byte[] bytes, String charset) { + return str(bytes, StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset)); + } + + /** + * 解码字节码 + * + * @param data 字符串 + * @param charset 字符集,如果此字段为空,则解码的结果取决于平台 + * @return 解码后的字符串 + */ + public static String str(byte[] data, Charset charset) { + if (data == null) { + return null; + } + + if (null == charset) { + return new String(data); + } + return new String(data, charset); + } + + /** + * 将编码的byteBuffer数据转换为字符串 + * + * @param data 数据 + * @param charset 字符集,如果为空使用当前系统字符集 + * @return 字符串 + */ + public static String str(ByteBuffer data, String charset) { + if (data == null) { + return null; + } + + return str(data, Charset.forName(charset)); + } + + /** + * 将编码的byteBuffer数据转换为字符串 + * + * @param data 数据 + * @param charset 字符集,如果为空使用当前系统字符集 + * @return 字符串 + */ + public static String str(ByteBuffer data, Charset charset) { + if (null == charset) { + charset = Charset.defaultCharset(); + } + return charset.decode(data).toString(); + } + + // ----------------------------------------------------------------------- 全角半角转换 + + /** + * 半角转全角 + * + * @param input String. + * @return 全角字符串. + */ + public static String toSBC(String input) { + return toSBC(input, null); + } + + /** + * 半角转全角 + * + * @param input String + * @param notConvertSet 不替换的字符集合 + * @return 全角字符串. + */ + public static String toSBC(String input, Set notConvertSet) { + char[] c = input.toCharArray(); + for (int i = 0; i < c.length; i++) { + if (null != notConvertSet && notConvertSet.contains(c[i])) { + // 跳过不替换的字符 + continue; + } + + if (c[i] == ' ') { + c[i] = '\u3000'; + } else if (c[i] < '\177') { + c[i] = (char) (c[i] + 65248); + + } + } + return new String(c); + } + + /** + * 全角转半角 + * + * @param input String. + * @return 半角字符串 + */ + public static String toDBC(String input) { + return toDBC(input, null); + } + + /** + * 替换全角为半角 + * + * @param text 文本 + * @param notConvertSet 不替换的字符集合 + * @return 替换后的字符 + */ + public static String toDBC(String text, Set notConvertSet) { + char[] c = text.toCharArray(); + for (int i = 0; i < c.length; i++) { + if (null != notConvertSet && notConvertSet.contains(c[i])) { + // 跳过不替换的字符 + continue; + } + + if (c[i] == '\u3000') { + c[i] = ' '; + } else if (c[i] > '\uFF00' && c[i] < '\uFF5F') { + c[i] = (char) (c[i] - 65248); + } + } + String returnString = new String(c); + + return returnString; + } + + /** + * 数字金额大写转换 先写个完整的然后将如零拾替换成零 + * + * @param n 数字 + * @return 中文大写数字 + */ + public static String digitUppercase(double n) { + String[] fraction = {"角", "分"}; + String[] digit = {"零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"}; + String[][] unit = {{"元", "万", "亿"}, {"", "拾", "佰", "仟"}}; + + String head = n < 0 ? "负" : ""; + n = Math.abs(n); + + String s = ""; + for (int i = 0; i < fraction.length; i++) { + s += (digit[(int) (Math.floor(n * 10 * Math.pow(10, i)) % 10)] + fraction[i]).replaceAll("(零.)+", ""); + } + if (s.length() < 1) { + s = "整"; + } + int integerPart = (int) Math.floor(n); + + for (int i = 0; i < unit[0].length && integerPart > 0; i++) { + String p = ""; + for (int j = 0; j < unit[1].length && n > 0; j++) { + p = digit[integerPart % 10] + unit[1][j] + p; + integerPart = integerPart / 10; + } + s = p.replaceAll("(零.)*零$", "").replaceAll("^$", "零") + unit[0][i] + s; + } + return head + s.replaceAll("(零.)*零元", "元").replaceFirst("(零.)+", "").replaceAll("(零.)+", "零").replaceAll("^整$", "零元整"); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/core/text/StrFormatter.java b/jsowell-common/src/main/java/com/jsowell/common/core/text/StrFormatter.java new file mode 100644 index 000000000..cf24adeae --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/core/text/StrFormatter.java @@ -0,0 +1,76 @@ +package com.jsowell.common.core.text; + +import com.jsowell.common.util.StringUtils; + +/** + * 字符串格式化 + * + * @author jsowell + */ +public class StrFormatter { + public static final String EMPTY_JSON = "{}"; + public static final char C_BACKSLASH = '\\'; + public static final char C_DELIM_START = '{'; + public static final char C_DELIM_END = '}'; + + /** + * 格式化字符串
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ * + * @param strPattern 字符串模板 + * @param argArray 参数列表 + * @return 结果 + */ + public static String format(final String strPattern, final Object... argArray) { + if (StringUtils.isEmpty(strPattern) || StringUtils.isEmpty(argArray)) { + return strPattern; + } + final int strPatternLength = strPattern.length(); + + // 初始化定义好的长度以获得更好的性能 + StringBuilder sbuf = new StringBuilder(strPatternLength + 50); + + int handledPosition = 0; + int delimIndex;// 占位符所在位置 + for (int argIndex = 0; argIndex < argArray.length; argIndex++) { + delimIndex = strPattern.indexOf(EMPTY_JSON, handledPosition); + if (delimIndex == -1) { + if (handledPosition == 0) { + return strPattern; + } else { // 字符串模板剩余部分不再包含占位符,加入剩余部分后返回结果 + sbuf.append(strPattern, handledPosition, strPatternLength); + return sbuf.toString(); + } + } else { + if (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == C_BACKSLASH) { + if (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == C_BACKSLASH) { + // 转义符之前还有一个转义符,占位符依旧有效 + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(Convert.utf8Str(argArray[argIndex])); + handledPosition = delimIndex + 2; + } else { + // 占位符被转义 + argIndex--; + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(C_DELIM_START); + handledPosition = delimIndex + 1; + } + } else { + // 正常占位符 + sbuf.append(strPattern, handledPosition, delimIndex); + sbuf.append(Convert.utf8Str(argArray[argIndex])); + handledPosition = delimIndex + 2; + } + } + } + // 加入最后一个占位符后所有的字符 + sbuf.append(strPattern, handledPosition, strPattern.length()); + + return sbuf.toString(); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/BusinessStatus.java b/jsowell-common/src/main/java/com/jsowell/common/enums/BusinessStatus.java new file mode 100644 index 000000000..40bc4746f --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/BusinessStatus.java @@ -0,0 +1,18 @@ +package com.jsowell.common.enums; + +/** + * 操作状态 + * + * @author jsowell + */ +public enum BusinessStatus { + /** + * 成功 + */ + SUCCESS, + + /** + * 失败 + */ + FAIL, +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/BusinessType.java b/jsowell-common/src/main/java/com/jsowell/common/enums/BusinessType.java new file mode 100644 index 000000000..140836eeb --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/BusinessType.java @@ -0,0 +1,58 @@ +package com.jsowell.common.enums; + +/** + * 业务操作类型 + * + * @author jsowell + */ +public enum BusinessType { + /** + * 其它 + */ + OTHER, + + /** + * 新增 + */ + INSERT, + + /** + * 修改 + */ + UPDATE, + + /** + * 删除 + */ + DELETE, + + /** + * 授权 + */ + GRANT, + + /** + * 导出 + */ + EXPORT, + + /** + * 导入 + */ + IMPORT, + + /** + * 强退 + */ + FORCE, + + /** + * 生成代码 + */ + GENCODE, + + /** + * 清空数据 + */ + CLEAN, +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/DataSourceType.java b/jsowell-common/src/main/java/com/jsowell/common/enums/DataSourceType.java new file mode 100644 index 000000000..7ad89880e --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/DataSourceType.java @@ -0,0 +1,18 @@ +package com.jsowell.common.enums; + +/** + * 数据源 + * + * @author jsowell + */ +public enum DataSourceType { + /** + * 主库 + */ + MASTER, + + /** + * 从库 + */ + SLAVE +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/DelFlagEnum.java b/jsowell-common/src/main/java/com/jsowell/common/enums/DelFlagEnum.java new file mode 100644 index 000000000..35d02c237 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/DelFlagEnum.java @@ -0,0 +1,34 @@ +package com.jsowell.common.enums; + +/** + * 删除标识枚举 + */ +public enum DelFlagEnum { + normal("0", "正常"), + delete("1", "删除"), + ; + + private String value; + private String label; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + DelFlagEnum(String value, String label) { + this.value = value; + this.label = label; + } +} \ No newline at end of file diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/HttpMethod.java b/jsowell-common/src/main/java/com/jsowell/common/enums/HttpMethod.java new file mode 100644 index 000000000..990234282 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/HttpMethod.java @@ -0,0 +1,32 @@ +package com.jsowell.common.enums; + +import org.springframework.lang.Nullable; + +import java.util.HashMap; +import java.util.Map; + +/** + * 请求方式 + * + * @author jsowell + */ +public enum HttpMethod { + GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE; + + private static final Map mappings = new HashMap<>(16); + + static { + for (HttpMethod httpMethod : values()) { + mappings.put(httpMethod.name(), httpMethod); + } + } + + @Nullable + public static HttpMethod resolve(@Nullable String method) { + return (method != null ? mappings.get(method) : null); + } + + public boolean matches(String method) { + return (this == resolve(method)); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/LimitType.java b/jsowell-common/src/main/java/com/jsowell/common/enums/LimitType.java new file mode 100644 index 000000000..f138ac610 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/LimitType.java @@ -0,0 +1,19 @@ +package com.jsowell.common.enums; + +/** + * 限流类型 + * + * @author jsowell + */ + +public enum LimitType { + /** + * 默认策略全局限流 + */ + DEFAULT, + + /** + * 根据请求者IP进行限流 + */ + IP +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/MemberWalletEnum.java b/jsowell-common/src/main/java/com/jsowell/common/enums/MemberWalletEnum.java new file mode 100644 index 000000000..2babb195f --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/MemberWalletEnum.java @@ -0,0 +1,53 @@ +package com.jsowell.common.enums; + +/** + * 会员钱包enum + */ +public enum MemberWalletEnum { + + /** + * 更新类型 + * 1-进账;2-出账 + */ + TYPE_IN("1", "进账"), + TYPE_OUT("2", "出账"), + + /** + * 子类型 + * 进账:10-充值, 11-赠送, 12-订单结算退款 + * 出账:20-后管扣款, 21-订单付款, 22-用户退款 + */ + SUBTYPE_TOP_UP("10", "充值"), + SUBTYPE_GIVING("11", "赠送"), + SUBTYPE_ORDER_SETTLEMENT_REFUND("12", "订单结算退款"), + + SUBTYPE_WEB_DEDUCT_MONEY("20", "后管扣款"), + SUBTYPE_PAYMENT_FOR_ORDER("21", "订单付款"), + SUBTYPE_USER_REFUND("22", "用户退款"), + + ; + + private String value; + private String label; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + MemberWalletEnum(String value, String label) { + this.value = value; + this.label = label; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/OperatorType.java b/jsowell-common/src/main/java/com/jsowell/common/enums/OperatorType.java new file mode 100644 index 000000000..3f15ea2ea --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/OperatorType.java @@ -0,0 +1,23 @@ +package com.jsowell.common.enums; + +/** + * 操作人类别 + * + * @author jsowell + */ +public enum OperatorType { + /** + * 其它 + */ + OTHER, + + /** + * 后台用户 + */ + MANAGE, + + /** + * 手机端用户 + */ + MOBILE +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/SoftwareProtocolEnum.java b/jsowell-common/src/main/java/com/jsowell/common/enums/SoftwareProtocolEnum.java new file mode 100644 index 000000000..53181d50f --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/SoftwareProtocolEnum.java @@ -0,0 +1,34 @@ +package com.jsowell.common.enums; + +/** + * 软件协议enum + */ +public enum SoftwareProtocolEnum { + YUN_KUAI_CHONG("1", "云快充"), + YONG_LIAN("2", "永联"), + ; + + private String value; + private String label; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + SoftwareProtocolEnum(String value, String label) { + this.value = value; + this.label = label; + } +} \ No newline at end of file diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/UserStatus.java b/jsowell-common/src/main/java/com/jsowell/common/enums/UserStatus.java new file mode 100644 index 000000000..3503df188 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/UserStatus.java @@ -0,0 +1,28 @@ +package com.jsowell.common.enums; + +/** + * 用户状态 + * + * @author jsowell + */ +public enum UserStatus { + OK("0" , "正常"), + DISABLE("1" , "停用"), + DELETED("2" , "删除"); + + private final String code; + private final String info; + + UserStatus(String code, String info) { + this.code = code; + this.info = info; + } + + public String getCode() { + return code; + } + + public String getInfo() { + return info; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/sim/SimCardStatusCorrespondEnum.java b/jsowell-common/src/main/java/com/jsowell/common/enums/sim/SimCardStatusCorrespondEnum.java new file mode 100644 index 000000000..150b2fcd3 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/sim/SimCardStatusCorrespondEnum.java @@ -0,0 +1,80 @@ +package com.jsowell.common.enums.sim; + + +/** + * Sim卡状态对应Enum + * (WuLian) + * + * @author JS-ZZA + * @date 2023/1/5 10:59 + */ +public enum SimCardStatusCorrespondEnum { + // 正常 + NORMAL("9", "0"), + + // 断网 + OFFLINE("5", "1"), + + // 销卡 + PIN_CARD("99", "6"), + + // 服务结束 + SERVICE_FINISHED("20", "1"), + + // 未开卡 + NOT_OPEN_CARD("0", "7"), + + // 沉默期 + SILENCE_PERIOD("2", "8"), + + // 已停机 + SHUT_DOWN_CARD("4", "9"), + + // 待激活 + INACTIVE_CARD("8", "10"), + + // 已回收 + RECYCLE_CARD("21", "11"), + + // 未知 + UN_KNOW("80", "99"), + + ; + private String WuLianCardStatus; + private String dataBaseCardStatus; + + public String getWuLianCardStatus() { + return WuLianCardStatus; + } + + public void setWuLianCardStatus(String wuLianCardStatus) { + WuLianCardStatus = wuLianCardStatus; + } + + public String getDataBaseCardStatus() { + return dataBaseCardStatus; + } + + public void setDataBaseCardStatus(String dataBaseCardStatus) { + this.dataBaseCardStatus = dataBaseCardStatus; + } + + SimCardStatusCorrespondEnum(String wuLianCardStatus, String dataBaseCardStatus) { + WuLianCardStatus = wuLianCardStatus; + this.dataBaseCardStatus = dataBaseCardStatus; + } + + /** + * 根据 WuLianCardStatus 获取 dataBaseCardStatus + * @param WuLianCardStatus + * @return + */ + public static String getDataBaseCardStatus(String WuLianCardStatus) { + for (SimCardStatusCorrespondEnum item : SimCardStatusCorrespondEnum.values()) { + if (item.getWuLianCardStatus().equals(WuLianCardStatus)) { + return item.getDataBaseCardStatus(); + } + } + return null; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/sim/SimSupplierEnum.java b/jsowell-common/src/main/java/com/jsowell/common/enums/sim/SimSupplierEnum.java new file mode 100644 index 000000000..95132c5e3 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/sim/SimSupplierEnum.java @@ -0,0 +1,53 @@ +package com.jsowell.common.enums.sim; + + +/** + * Sim卡商 + * + * @author JS-ZZA + * @date 2022/12/17 14:30 + */ +public enum SimSupplierEnum { + XUN_ZHONG("1", "讯众物联"), + WU_LIAN_INTERNET("2", "物联网智能云平台") + + + ; + private String code; + private String name; + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + SimSupplierEnum(String code, String name) { + this.code = code; + this.name = name; + } + + /** + * 根据code获取name + * @param code + * @return + */ + public static String getNameByCode(String code) { + for (SimSupplierEnum item : SimSupplierEnum.values()) { + if (item.getCode().equals(code)) { + return item.getName(); + } + } + return null; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/uniapp/BalanceChangesEnum.java b/jsowell-common/src/main/java/com/jsowell/common/enums/uniapp/BalanceChangesEnum.java new file mode 100644 index 000000000..238708f9b --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/uniapp/BalanceChangesEnum.java @@ -0,0 +1,70 @@ +package com.jsowell.common.enums.uniapp; + +import com.jsowell.common.enums.ykc.StopChargingFailedReasonEnum; + +/** + * 用户账户余额Enum + * + * @author JS-ZZA + * @date 2022/11/26 11:34 + */ +public enum BalanceChangesEnum { + /** + * 进账 + */ + CODE_RECHARGE("10", "充值"), + + CODE_PRESENTED("11", "赠送"), + + CODE_ORDER_SETTLEMENT_REFUND("12", "订单结算退款"), + + + /** + * 出账 + */ + CODE_SYSTEM_DEDUCTIONS("20", "后管扣款"), + + CODE_ORDER_PAYMENT("21", "订单付款"), + + CODE_USER_REFUND("22", "用户退款"), + ; + private String code; + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + BalanceChangesEnum(String code, String value) { + this.value = value; + this.code = code; + } + + /** + * 根据code获取value + * @param code + * @return + */ + public static String getValueByCode(String code) { + for (BalanceChangesEnum item : BalanceChangesEnum.values()) { + if (item.getCode().equals(code)) { + return item.getValue(); + } + } + return null; + } + + +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/weixin/BusinessType.java b/jsowell-common/src/main/java/com/jsowell/common/enums/weixin/BusinessType.java new file mode 100644 index 000000000..d3c48f853 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/weixin/BusinessType.java @@ -0,0 +1,34 @@ +package com.jsowell.common.enums.weixin; + +/** + * 业务类型 + * + * @author xiaojiewen + */ + +public enum BusinessType { + + CLIENT("客户端", 6L), + DISTRIBUTOR("分销商", 2L), + MERCHANT("服务商", 3L), + OTHER("其他", 0L) + + ; + + private String type; + + private Long enumType; + + BusinessType(String type, Long enumType) { + this.type = type; + this.enumType = enumType; + } + + public Long getEnumType() { + return enumType; + } + + public String getType() { + return type; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/weixin/WeiXinPayParam.java b/jsowell-common/src/main/java/com/jsowell/common/enums/weixin/WeiXinPayParam.java new file mode 100644 index 000000000..127b90500 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/weixin/WeiXinPayParam.java @@ -0,0 +1,43 @@ +package com.jsowell.common.enums.weixin; + +/** + * 微信支付参数 + * WeiXinPayParam
+ * 创建人:小威
+ * 时间:2015年12月1日-下午4:41:02
+ * + * @version 1.0.0 + */ +public enum WeiXinPayParam { + + WEIXIN_PAY_DATA("data", "微信支付数据"), + WEIXIN_PAY_ID("id", "缓存id"), + WEIXIN_PAY_ORDERID("orderId", "订单id"), + WEIXIN_PAY_MONEY("money", "支付价格"); + + + private String value; + private String desc; + + WeiXinPayParam(String value, String desc) { + this.value = value; + this.desc = desc; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getDesc() { + return desc; + } + + public void setDesc(String desc) { + this.desc = desc; + } + +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/weixin/WeiXinPayTradeStatus.java b/jsowell-common/src/main/java/com/jsowell/common/enums/weixin/WeiXinPayTradeStatus.java new file mode 100644 index 000000000..8f20d844f --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/weixin/WeiXinPayTradeStatus.java @@ -0,0 +1,38 @@ +package com.jsowell.common.enums.weixin; + +/** + * 微信支付交易状态 + * WeiXinPayTradeStatus
+ * 创建人:小威
+ * 时间:2015年12月1日-下午4:37:55
+ * + * @version 1.0.0 + */ +public enum WeiXinPayTradeStatus { + SUCCESS("SUCCESS", "交易成功"), + FAIL("FAIL", "交易失败"); + private String value; + private String desc; + + WeiXinPayTradeStatus(String value, String desc) { + this.value = value; + this.desc = desc; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getDesc() { + return desc; + } + + public void setDesc(String desc) { + this.desc = desc; + } + +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/ActionTypeEnum.java b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/ActionTypeEnum.java new file mode 100644 index 000000000..f0a6f6a96 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/ActionTypeEnum.java @@ -0,0 +1,33 @@ +package com.jsowell.common.enums.ykc; + +/** + * 操作类型枚举 + */ +public enum ActionTypeEnum { + FORWARD("forward", "正向"), + REVERSE("reverse", "逆向"), + ; + private String value; + private String label; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + ActionTypeEnum(String value, String label) { + this.value = value; + this.label = label; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/BillingTimeEnum.java b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/BillingTimeEnum.java new file mode 100644 index 000000000..581502f9b --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/BillingTimeEnum.java @@ -0,0 +1,37 @@ +package com.jsowell.common.enums.ykc; + +/** + * 计费模板 时间类型 + * (1-尖时;2-峰时;3-平时;4-谷时) + */ +public enum BillingTimeEnum { + SHARP("1", "尖时"), + PEAK("2", "峰时"), + FLAT("3", "平时"), + VALLEY("4", "谷时"), + ; + + private String value; + private String label; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + BillingTimeEnum(String value, String label) { + this.value = value; + this.label = label; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/BusinessTypeEnum.java b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/BusinessTypeEnum.java new file mode 100644 index 000000000..73c40f63c --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/BusinessTypeEnum.java @@ -0,0 +1,31 @@ +package com.jsowell.common.enums.ykc; + +public enum BusinessTypeEnum { + OPERATING_PILE("1", "运营桩"), + INDIVIDUAL_PILE("2", "个人桩"), + ; + + private String value; + private String label; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + BusinessTypeEnum(String value, String label) { + this.value = value; + this.label = label; + } +} \ No newline at end of file diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/ChargingFailedReasonEnum.java b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/ChargingFailedReasonEnum.java new file mode 100644 index 000000000..69442758d --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/ChargingFailedReasonEnum.java @@ -0,0 +1,56 @@ +package com.jsowell.common.enums.ykc; + +/** + * 启动充电失败原因 + * + * @author JS-ZZA + * @date 2022/11/16 14:32 + */ +public enum ChargingFailedReasonEnum { + NULL(0x00, "无"), + EQUIPMENT_PILE_SN_NOT_MATCH(0x01, "设备桩号不匹配"), + CONNECTOR_IS_CHARGING(0x02, "枪已在充电"), + EQUIPMENT_BREAKDOWN(0x03, "设备故障"), + EQUIPMENT_OFFLINE(0x04, "设备离线"), + UNPLUGGED_GUN(0x05, "未插枪"), + + ; + private int code; + private String msg; + + ChargingFailedReasonEnum(int code, String msg) { + this.code = code; + this.msg = msg; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + /** + * 根据code获取停止原因描述 + * + * @param code 编码 + * @return 停止原因描述 + */ + public static String getMsgByCode(int code) { + for (ChargingFailedReasonEnum item : ChargingFailedReasonEnum.values()) { + if (item.getCode() == code) { + return item.getMsg(); + } + } + return null; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/OrderPayModeEnum.java b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/OrderPayModeEnum.java new file mode 100644 index 000000000..11cfb0457 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/OrderPayModeEnum.java @@ -0,0 +1,47 @@ +package com.jsowell.common.enums.ykc; + +/** + * 订单支付方式enum + * 前端给的参数 + */ +public enum OrderPayModeEnum { + + PAYMENT_OF_BALANCE("1", "余额支付"), + PAYMENT_OF_WHITELIST("3", "白名单支付"), + PAYMENT_OF_WECHATPAY("4", "微信支付"), + PAYMENT_OF_ALIPAY("5", "支付宝支付"), + ; + + private String value; + private String label; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public static String getPayModeDescription(String value) { + for (OrderPayModeEnum orderPayModeEnum : OrderPayModeEnum.values()) { + if (orderPayModeEnum.getValue().equals(value)) { + return orderPayModeEnum.getLabel(); + } + } + return ""; + } + + OrderPayModeEnum(String value, String label) { + this.value = value; + this.label = label; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/OrderPayRecordEnum.java b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/OrderPayRecordEnum.java new file mode 100644 index 000000000..b15b61afc --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/OrderPayRecordEnum.java @@ -0,0 +1,48 @@ +package com.jsowell.common.enums.ykc; + +/** + * order_pay_record表pay_mode字段 + * 支付方式(1-本金余额支付;2-赠送余额支付;3-白名单支付;4-微信支付;5-支付宝支付) + * 后端数据库 + */ +public enum OrderPayRecordEnum { + PRINCIPAL_BALANCE_PAYMENT("1", "本金余额支付"), + GIFT_BALANCE_PAYMENT("2", "赠送余额支付"), + WHITELIST_PAYMENT("3", "白名单支付"), + WECHATPAY_PAYMENT("4", "微信支付"), + ALIPAY_PAYMENT("5", "支付宝支付"), + ; + + private String value; + private String label; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public static String getPayModeDescription(String value) { + for (OrderPayRecordEnum orderPayModeEnum : OrderPayRecordEnum.values()) { + if (orderPayModeEnum.getValue().equals(value)) { + return orderPayModeEnum.getLabel(); + } + } + return ""; + } + + OrderPayRecordEnum(String value, String label) { + this.value = value; + this.label = label; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/OrderPayStatusEnum.java b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/OrderPayStatusEnum.java new file mode 100644 index 000000000..ca0e90787 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/OrderPayStatusEnum.java @@ -0,0 +1,35 @@ +package com.jsowell.common.enums.ykc; + +/** + * 订单支付状态 + */ +public enum OrderPayStatusEnum { + unpaid("0", "待支付"), + paid("1", "支付完成"), + + ; + + private String value; + private String label; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + OrderPayStatusEnum(String value, String label) { + this.value = value; + this.label = label; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/OrderStatusEnum.java b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/OrderStatusEnum.java new file mode 100644 index 000000000..6aa5e83c4 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/OrderStatusEnum.java @@ -0,0 +1,51 @@ +package com.jsowell.common.enums.ykc; + +/** + * order_basic_info表 order_status字段 + * 订单状态(0-未启动;1-充电中;2-待结算;3-待补缴;4-异常;5-可疑;6-订单完成;7-超时关闭) + */ +public enum OrderStatusEnum { + + NOT_START("0", "未启动"), + IN_THE_CHARGING("1", "充电中"), + STAY_SETTLEMENT("2", "待结算"), + STAY_RETROACTIVE_AMOUNT("3", "待补缴"), + ABNORMAL("4", "异常"), + SUSPICIOUS("5", "可疑"), + ORDER_COMPLETE("6", "订单完成"), + ORDER_CLOSE_TIMEOUT("7", "超时关闭"), + ; + + private String value; + private String label; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + OrderStatusEnum(String value, String label) { + this.value = value; + this.label = label; + } + + public static String getOrderStatus(String value) { + for (OrderStatusEnum orderStatusEnum : OrderStatusEnum.values()) { + if (orderStatusEnum.getValue().equals(value)) { + return orderStatusEnum.getLabel(); + } + } + return ""; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/PayModeEnum.java b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/PayModeEnum.java new file mode 100644 index 000000000..a0ce0637e --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/PayModeEnum.java @@ -0,0 +1,32 @@ +package com.jsowell.common.enums.ykc; + +public enum PayModeEnum { + PAYMENT_OF_BALANCE("balance", "余额支付"), + PAYMENT_OF_WHITELIST("whitelist", "白名单支付"), + PAYMENT_OF_WECHATPAY("wechatpay", "微信支付"), + PAYMENT_OF_ALIPAY("alipay", "支付宝支付"), + ; + private String value; + private String label; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + PayModeEnum(String value, String label) { + this.value = value; + this.label = label; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/PileChannelEntity.java b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/PileChannelEntity.java new file mode 100644 index 000000000..755f70cac --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/PileChannelEntity.java @@ -0,0 +1,55 @@ +package com.jsowell.common.enums.ykc; + +import io.netty.channel.Channel; + +import java.util.HashMap; + +/** + * 桩编号和channel的关联关系处理 entity + */ +public class PileChannelEntity { + + private static HashMap manager = new HashMap<>(); + + public static void put(String pileSn, Channel channel) { + manager.put(pileSn, channel); + } + + /** + * 通过桩编号获取channel链接信息 + * @param pileSn + * @return + */ + public static Channel getChannelByPileSn(String pileSn) { + return manager.get(pileSn); + } + + /** + * 通过channelId获取桩编号 + * @param channelId + * @return + */ + public static String getPileSnByChannelId(String channelId) { + for (HashMap.Entry entry : manager.entrySet()) { + if (entry.getValue().id().asLongText().equals(channelId)) { + return entry.getKey(); + } + } + return null; + } + + /** + * 打印 + */ + public static void output() { + for (HashMap.Entry entry : manager.entrySet()) { + System.out.println("pileSn:" + entry.getKey() + + ",ChannelId:" + entry.getValue().id().asLongText()); + } + } + + public static void removeByPileSn(String pileSn){ + manager.remove(pileSn); + } + +} \ No newline at end of file diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/PileConnectorDataBaseStatusEnum.java b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/PileConnectorDataBaseStatusEnum.java new file mode 100644 index 000000000..f0ca2dc0b --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/PileConnectorDataBaseStatusEnum.java @@ -0,0 +1,48 @@ +package com.jsowell.common.enums.ykc; + +/** + * 充电枪口数据库状态 + */ +public enum PileConnectorDataBaseStatusEnum { + + OFF_NETWORK("0", "离线"), + FREE("1", "空闲"), + OCCUPIED_NOT_CHARGED("2", "占用(未充电)"), + OCCUPIED_CHARGING("3", "占用(充电中)"), + OCCUPIED_APPOINTMENT_LOCK("4", "占用(预约锁定)"), + FAULT("255", "故障") + ; + + private String value; + private String label; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + PileConnectorDataBaseStatusEnum(String value, String label) { + this.value = value; + this.label = label; + } + + public static String getStatusDescription(String value) { + for (PileConnectorDataBaseStatusEnum statusEnum : PileConnectorDataBaseStatusEnum.values()) { + if (statusEnum.getValue().equals(value)) { + return statusEnum.getLabel(); + } + } + return ""; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/PileConnectorStatusEnum.java b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/PileConnectorStatusEnum.java new file mode 100644 index 000000000..e288acf69 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/PileConnectorStatusEnum.java @@ -0,0 +1,36 @@ +package com.jsowell.common.enums.ykc; + +/** + * 充电接口状态 + * 桩传过来的枪口状态: 0x00:离线 0x01:故障 0x02:空闲 0x03:充电 + */ +public enum PileConnectorStatusEnum { + OFF_NETWORK("00", "离线"), + FAULT("01", "故障"), + FREE("02", "空闲"), + OCCUPIED_CHARGING("03", "充电"), + ; + private String value; + private String label; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + PileConnectorStatusEnum(String value, String label) { + this.value = value; + this.label = label; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/PileStatusEnum.java b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/PileStatusEnum.java new file mode 100644 index 000000000..b70bab00e --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/PileStatusEnum.java @@ -0,0 +1,45 @@ +package com.jsowell.common.enums.ykc; + +/** + * 充电桩状态enum + */ +public enum PileStatusEnum { + UNKNOWN("0", "未知"), + ON_LINE("1", "在线"), + OFF_LINE("2","离线"), + FAULT("3", "故障"), + ; + + private String value; + private String label; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + PileStatusEnum(String value, String label) { + this.value = value; + this.label = label; + } + + public static String getStatusDescription(String status) { + for (PileStatusEnum pileStatusEnum : PileStatusEnum.values()) { + if (pileStatusEnum.getValue().equals(status)) { + return pileStatusEnum.getLabel(); + } + } + return ""; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/ReturnCodeEnum.java b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/ReturnCodeEnum.java new file mode 100644 index 000000000..1ef1b7c3f --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/ReturnCodeEnum.java @@ -0,0 +1,138 @@ +package com.jsowell.common.enums.ykc; + +public enum ReturnCodeEnum { + /** + * 成功 + */ + CODE_SUCCESS("00100000", "操作成功"), + + CODE_TOKEN_ERROR("00100002", "身份验证失败,请重新登录"), + + CODE_PARAM_NOT_NULL_ERROR("00100003", "参数不能为空"), + + CODE_VERIFICATION_CODE_ERROR("00100004", "验证码错误"), + + CODE_VERIFICATION_CODE_TIMEOUT_ERROR("00100005", "验证码超时"), + + CODE_SEND_SMS_ERROR("00100006", "发送短信验证码错误"), + + CODE_MEMBER_REGISTER_AND_LOGIN_ERROR("00100007", "会员登录注册接口异常"), + + CODE_WECHAT_LOGIN_ERROR("00100008", "微信登录异常"), + + CODE_GET_MEMBER_PHONE_NUMBER_ERROR("00100009", "获取用户手机号异常"), + + CODE_HANDLE_USER_INFO_ERROR("00100010", "处理用户信息异常"), + + CODE_GET_PILE_STATION_INFO_ERROR("00100011", "查询充电站信息列表异常"), + + CODE_GET_CONNECTOR_INFO_BY_STATION_ID_ERROR("00100012", "通过站点id查询充电枪口列表异常"), + + CODE_PILE_CONNECTOR_STATUS_ERROR("00100013", "该枪口状态不正确"), + + CODE_GENERATE_ORDER_ERROR("00100014", "生成订单失败"), + + CODE_CONNECTOR_INFO_NULL_ERROR("00100015", "充电枪口为空"), + + CODE_BILLING_TEMPLATE_NULL_ERROR("00100016", "查询充电桩的计费模板为空"), + + CODE_DATA_LENGTH_ERROR("00100017", "数据长度不正确"), + + CODE_SETTLE_ORDER_ERROR("00100018", "订单结算异常"), + + CODE_ORDER_INFO_ERROR("00100019", "订单信息有误"), + + CODE_QUERY_ORDER_NULL_ERROR("00100020", "查询订单为空"), + + CODE_ORDER_PILE_MAPPING_ERROR("00100021", "订单与当前桩不匹配"), + + CODE_GET_MEMBER_ACCOUNT_AMOUNT_ERROR("00100022", "查询用户账户总余额异常"), + + CODE_BALANCE_IS_INSUFFICIENT("00100023", "账户余额不足"), + + CODE_GET_MOBILE_NUMBER_BY_CODE_ERROR("00100024", "获取微信登录手机号失败"), + + CODE_GET_MERCHANT_ID_BY_APP_ID_ERROR("00100024", "获取商户id失败"), + + CODE_GET_ORDER_INFO_BY_MEMBER_ID_ERROR("00100025", "通过会员Id查询某状态订单失败"), + + CODE_GET_OPEN_ID_BY_CODE_ERROR("00100026", "获取openid失败"), + + CODE_GET_WECHAT_PAY_PARAMETER_ERROR("00100027", "获取微信支付参数失败"), + + CODE_GET_BALANCE_CHANGES_ERROR("00100028", "查询用户账户余额变动信息异常"), + + CODE_ORDER_PAY_ERROR("00100029", "订单支付失败"), + + CODE_ORDER_PAY_CALLBACK_ERROR("00100030", "支付回调失败"), + + CODE_ORDER_IS_NOT_TO_BE_PAID_ERROR("00100031", "订单不是待支付状态"), + + CODE_ORDER_MEMBER_NOT_MATCH_ERROR("00100032", "订单与会员信息不匹配"), + + CODE_STOP_CHARGING_ERROR("00100033", "停止充电失败"), + + CODE_ORDER_IS_NOT_STAY_SETTLEMENT_ERROR("00100034", "订单不是待结算状态"), + + CODE_GET_PILE_DETAIL_ERROR("00100035", "查询充电桩详情失败"), + + CODE_WEIXIN_REFUND_ERROR("00100036", "微信退款接口失败"), + + CODE_REFUND_ORDER_AMOUNT_ERROR("00100037", "订单退款金额不能大于可退金额"), + + CODE_REFUND_ORDER_CALLBACK_RECORD_ERROR("00100038", "订单退款处理逻辑, 查询订单微信支付记录为空!"), + + CODE_SELECT_MEMBER_NULL_ERROR("00100039", "没有查询到会员信息"), + + CODE_REFUND_MEMBER_BALANCE_ERROR("00100040", "退款金额不能大于本金金额"), + + CODE_GET_ORDER_DETAIL_ERROR("00100041", "小程序获取订单详情失败"), + + CODE_SELECT_PILE_STARTER_STATUS("00100042", "根据订单号查询充电桩启动状态失败"), + + CODE_PILE_HAS_BEEN_BINDING_ERROR("00400001", "此桩已被绑定,请联系管理员!"), + + CODE_AUTHENTICATION_ERROR("00400002", "您的身份信息验证有误,请重试!"), + + CODE_USER_IS_NOT_REGISTER("00400003", "此用户未注册平台账号,请核实身份!"), + + CODE_USER_HAS_BEEN_THIS_PILE("00400004", "此用户已绑定该桩,请检查!"), + + CODE_NO_CHARGING_ORDER_ERROR("00400005", "当前无正在充电的订单"), + + CODE_NO_REAL_TIME_INFO("00400006", "未查到充电枪口实时信息"), + + CODE_BINDING_PERSONAL_PILE_ERROR("00400007", "获取个人桩枪口实时数据异常"), + + CODE_ADMIN_ISSUE_ERROR("00400008", "桩管理员下发个人桩异常"), + + CODE_GET_PERSONAL_PILE_BY_MEMBER_ID_ERROR("00400009", "通过memberId查个人桩列表异常"), + + CODE_GET_PERSONAL_PILE_CONNECTOR_INFO_ERROR("00400010", "获取个人桩枪口实时数据异常"), + ; + + private String value; + + private String label; + + private ReturnCodeEnum(String value, String label) { + this.value = value; + this.label = label; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/ScenarioEnum.java b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/ScenarioEnum.java new file mode 100644 index 000000000..9d7dfe828 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/ScenarioEnum.java @@ -0,0 +1,35 @@ +package com.jsowell.common.enums.ykc; + +/** + * 支付场景枚举 + */ +public enum ScenarioEnum { + ORDER("order", "支付订单"), + + BALANCE("balance", "支付余额"), + ; + + private String value; + private String label; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + ScenarioEnum(String value, String label) { + this.value = value; + this.label = label; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/StopChargingFailedReasonEnum.java b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/StopChargingFailedReasonEnum.java new file mode 100644 index 000000000..81f94e6da --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/StopChargingFailedReasonEnum.java @@ -0,0 +1,55 @@ +package com.jsowell.common.enums.ykc; + +/** + * 远程停止充电失败原因enum + * + * @author JS-ZZA + * @date 2022/11/16 8:31 + */ +public enum StopChargingFailedReasonEnum { + NULL("00", "无"), + EQUIPMENT_CODE_NOT_MATCH("01", "设备编号不匹配"), + CONNECTOR_NOT_IN_CHARGING("02", "枪未处于充电状态"), + OTHER("03","其他"), + + + ; + private String code; + private String msg; + + StopChargingFailedReasonEnum(String code, String msg) { + this.code = code; + this.msg = msg; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + /** + * 根据code获取msg + * @param code + * @return + */ + public static String getMsgByCode(String code) { + for (StopChargingFailedReasonEnum item : StopChargingFailedReasonEnum.values()) { + if (item.getCode().equals(code)) { + return item.getMsg(); + } + } + return null; + } + +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/YKCChargingStopReasonEnum.java b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/YKCChargingStopReasonEnum.java new file mode 100644 index 000000000..c9a0f2e29 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/YKCChargingStopReasonEnum.java @@ -0,0 +1,167 @@ +package com.jsowell.common.enums.ykc; + +import com.jsowell.common.util.BytesUtil; + +/** + * 云快充充电停止原因enum + * + * @author JS-ZZA + * @date 2022/11/4 16:46 + */ +public enum YKCChargingStopReasonEnum { + /** + * 充电完成 + */ + APP_REMOTE_STOP(0x40, "结束充电,APP远程停止"), + SOC_FULL(0x41, "结束充电,SOC 达到 100%"), + CHARGING_ELECTRICITY_ELIGIBLE(0x42, "结束充电,充电电量满足设定条件"), + CHARGING_AMOUNT_ELIGIBLE(0x43, "结束充电,充电金额满足设定条件"), + CHARGING_TIME_ELIGIBLE(0x44, "结束充电,充电时间满足设定条件"), + MANUALLY_STOP_CHARGING(0x45, "结束充电,手动停止充电"), + + OTHER_FORTY_SIX(0x46, "其他方式(预留)"), + OTHER_FORTY_SEVEN(0x47, "其他方式(预留)"), + OTHER_FORTY_EIGHT(0x48, "其他方式(预留)"), + OTHER_FORTY_NINE(0x49, "其他方式(预留)"), + + /** + * 充电启动失败 + */ + PILE_CONTROL_SYSTEM_FAULT(0x4A, "充电启动失败,充电桩控制系统故障(需要重启或自动恢复)"), + CONTROL_GUIDANCE_BREAK(0x4B, "充电启动失败,控制导引断开"), + CIRCUIT_BREAKER_TRIP(0x4C, "充电启动失败,断路器跳位"), + AMMETER_COMMUNICATION_BREAK(0x4D, "充电启动失败,电表通信中断"), + LACK_OF_BALANCE(0x4E, "充电启动失败,余额不足"), + CHARGING_MODEL_FAULT(0x4F, "充电启动失败,充电模块故障"), + EMERGENCY_STOP_FAULT(0x50, "充电启动失败,急停开入"), + SPD_ABNORMAL(0x51, "充电启动失败,防雷器异常"), + BMS_UNREADY(0x52, "充电启动失败, BMS 未就绪"), + TEMPERATURE_ABNORMAL(0x53, "充电启动失败,温度异常"), + BATTERY_REVERSE_CONNECT_FAULT(0x54, "充电启动失败,电池反接故障"), + ELECTRICITY_LOCK_FAULT(0x55, "充电启动失败,电子锁异常"), + FAIL_CLOSE_ACT(0x56, "充电启动失败,合闸失败"), + INSULATION_FAULT(0x57, "充电启动失败,绝缘异常"), + RESERVED_FIFTY_EIGHT(0x58, "预留"), + RECEIVED_BMS_HANDSHAKE_REPORT_TIMEOUT(0x59, "充电启动失败,接收 BMS 握手报文 BHM 超时"), + RECEIVED_BMS_AND_CAR_IDENTIFY_REPORT_TIMEOUT(0x5A, "充电启动失败,接收 BMS 和车辆的辨识报文超时 BRM"), + RECEIVED_BATTERY_CHARGING_DATA_TIMEOUT(0x5B, "充电启动失败,接收电池充电参数报文超时 BCP"), + RECEIVED_CHARGING_COMPLETE_REPORT_TIMEOUT(0x5C, "充电启动失败,接收 BMS 完成充电准备报文超时 BRO AA"), + RECEIVED_CHARGING_STATUS_REPORT_TIMEOUT(0x5D, "充电启动失败,接收电池充电总状态报文超时 BCS"), + RECEIVED_CHARGING_REQUEST_REPORT_TIMEOUT(0x5E, "充电启动失败,接收电池充电要求报文超时 BCL"), + RECEIVED_BATTERY_STATUS_TIMEOUT(0x5F, "充电启动失败,接收电池状态信息报文超时 BSM"), + GB_2015_PROHIBIT_CHARGING_AT_BHM(0x60, "充电启动失败, GB2015 电池在 BHM 阶段有电压不允许充电"), + GB_2015_BATTERY_VOLTAGE_GAP(0x61, "充电启动失败, GB2015 辨识阶段在 BRO_AA 时候电池实际电压 与 BCP 报文电池电压差距大于 5%"), + B_2015_CHARGER_BRO_AA_TO_BRO_OO(0x62, "充电启动失败, B2015 充电机在预充电阶段从 BRO_AA 变成 BRO_00 状态"), + RECEIVED_HOST_CONFIG_REPORT_TIMEOUT(0x63, "充电启动失败,接收主机配置报文超时"), + CHARGER_UNREADY(0x64, "充电启动失败,充电机未准备就绪,我们没有回 CRO AA,对应 老国标"), + + OTHER_SIXTY_FIVE(0x65, "(其他原因)预留"), + OTHER_SIXTY_SIX(0x66, "(其他原因)预留"), + OTHER_SIXTY_SEVEN(0x67, "(其他原因)预留"), + OTHER_SIXTY_EIGHT(0x68, "(其他原因)预留"), + OTHER_SIXTY_NINE(0x69, "(其他原因)预留"), + + /** + * 充电异常中止 + */ + SYSTEM_CLOSE_LOCK(0x6A, "充电异常中止,系统闭锁"), + GUIDANCE_BREAK(0x6B, "充电异常中止,导引断开"), + BREAKER_TRIP(0x6C, "充电异常中止,断路器跳位"), + AMMETER_COMMUNICATION_INTERRUPT(0x6D, "充电异常中止,电表通信中断"), + NOT_SUFFICIENT_FUNDS(0x6E, "充电异常中止,余额不足"), + AC_PROTECT_ACTION(0x6F, "充电异常中止,交流保护动作"), + DC_PROTECT_ACTION(0x70, "充电异常中止,直流保护动作"), + CHARGE_MODEL_UNUSUAL(0x71, "充电异常中止,充电模块故障"), + FETCH_UP_UNUSUAL(0x72, "充电异常中止,急停开入"), + SPD_UNUSUAL(0x73, "充电异常中止,防雷器异常"), + TEMPERATURE_UNUSUAL(0x74, "充电异常中止,温度异常"), + OUTPUT_UNUSUAL(0x75, "充电异常中止,输出异常"), + CHARGING_NO_CURRENT(0x76, "充电异常中止,充电无流"), + ELECTRICITY_LOCK_UNUSUAL(0x77, "充电异常中止,电子锁异常"), + + OTHER_SEVENTY_EIGHT(0x78, "预留"), + + TOTAL_CHARGING_VOLTAGE_UNUSUAL(0x79, "充电异常中止,总充电电压异常"), + TOTAL_CHARGING_CURRENT_UNUSUAL(0x7A, "充电异常中止,总充电电流异常"), + SINGLE_CHARGING_VOLTAGE_UNUSUAL(0x7B, "充电异常中止,单体充电电压异常"), + BATTERY_TEMPERATURE_TOO_HIGH(0x7C, "充电异常中止,电池组过温"), + HIGHEST_SINGLE_CHARGING_VOLTAGE_UNUSUAL(0x7D, "充电异常中止,最高单体充电电压异常"), + HIGHEST_BATTERY_TEMPERATURE_TOO_HIGH(0x7E, "充电异常中止,最高电池组过温"), + BMV_SINGLE_CHARGING_VOLTAGE_UNUSUAL(0x7F, "充电异常中止, BMV 单体充电电压异常"), + BMT_BATTERY_TEMPERATURE_TOO_HIGH(0x80, "充电异常中止, BMT 电池组过温"), + BATTERY_STATUS_UNUSUAL_STOP_CHARGING(0x81, "充电异常中止,电池状态异常停止充电"), + CAR_SEND_REPORT_FORBID_CHARGING(0x82, "充电异常中止,车辆发报文禁止充电"), + CHARGING_PILE_OUTAGE(0x83, "充电异常中止,充电桩断电"), + RECEIVED_BATTERY_CHARGING_STATUS_TIMEOUT(0x84, "充电异常中止,接收电池充电总状态报文超时"), + RECEIVED_BATTERY_CHARGING_REQUEST_TIMEOUT(0x85, "充电异常中止,接收电池充电要求报文超时"), + RECEIVED_BATTERY_STATUS_DATA_TIMEOUT(0x86, "充电异常中止,接收电池状态信息报文超时"), + RECEIVED_BMS_STOP_CHARGING_TIMEOUT(0x87, "充电异常中止,接收 BMS 中止充电报文超时"), + RECEIVED_BMS_CHARGING_STATISTICAL_TIMEOUT(0x88, "充电异常中止,接收 BMS 充电统计报文超时"), + RECEIVED_CCS_TIMEOUT(0x89, "充电异常中止,接收对侧 CCS 报文超时"), + + OTHER_EIGHTY_A(0x8A, "(其他原因)预留"), + OTHER_EIGHTY_B(0x8B, "(其他原因)预留"), + OTHER_EIGHTY_C(0x8C, "(其他原因)预留"), + OTHER_EIGHTY_D(0x8D, "(其他原因)预留"), + OTHER_EIGHTY_E(0x8E, "(其他原因)预留"), + OTHER_EIGHTY_F(0x8F, "(其他原因)预留"), + + + /** + * 未知原因停止 + */ + UNKNOWN_REASON_STOP_CHARGING(0x90, "未知原因停止"), + + ; + + + + private int code; + private String msg; + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + YKCChargingStopReasonEnum(int code, String msg) { + this.code = code; + this.msg = msg; + } + + /** + * 根据code获取停止原因描述 + * + * @param code 编码 + * @return 停止原因描述 + */ + public static String getMsgByCode(int code) { + for (YKCChargingStopReasonEnum item : YKCChargingStopReasonEnum.values()) { + if (item.getCode() == code) { + return item.getMsg(); + } + } + return null; + } + + public static void main(String[] args) { + String stopReason = "6B"; + byte[] stopReasonByteArr = new byte[]{0x6B}; + String s = BytesUtil.bin2HexStr(stopReasonByteArr); + + int i = Integer.parseInt(stopReason, 16); + String stopReasonMsg = YKCChargingStopReasonEnum.getMsgByCode(i); + System.out.println(stopReasonMsg); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/YKCPileFaultReasonEnum.java b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/YKCPileFaultReasonEnum.java new file mode 100644 index 000000000..1799716e9 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/enums/ykc/YKCPileFaultReasonEnum.java @@ -0,0 +1,64 @@ +package com.jsowell.common.enums.ykc; + +/** + * 充电桩故障原因enum + * + * @author JS-ZZA + * @date 2022/10/17 9:03 + */ +public enum YKCPileFaultReasonEnum { + + STOP_BUTTON_FAULT(1, "急停按钮动作故障"), + NO_CAN_USE_RECTIFICATION_MODEL_FAULT(2, "无可用整流模块"), + OUTLET_TEMPERATURE_TOO_HIGH_FAULT(3, "出风口温度过高"), + ALTERNATING_LIGHTING_PROTECTION_FAULT(4, "交流防雷故障"), + DC20_COMMUNICATION_INTERRUPT_FAULT(5, "交直流模块 DC20 通信中断"), + FC08_COMMUNICATION_INTERRUPT_FAULT(6, "交直流模块 FC08 通信中断"), + WATT_HOUR_METER_COMMUNICATION_INTERRUPT_FAULT(7, "电度表通信中断"), + CARD_READER_COMMUNICATION_INTERRUPT_FAULT(8, "读卡器通信中断"), + RC10_COMMUNICATION_INTERRUPT_FAULT(9, "RC10 通信中断"), + FAN_SPEED_CONTROL_FAULT(10, "风扇调速板故障"), + DC_FUSE_FAULT(11, "直流熔断器故障"), + HIGH_PRESSURE_CONTACTOR_FAULT(12, "高压接触器故障"), + DOOR_OPEN_FAULT(13, "门打开"), + ; + + /** + * 根据code获取故障描述 + * + * @param code 故障编码 + * @return 故障描述 + */ + public static String getValueByCode(int code) { + for (YKCPileFaultReasonEnum item : YKCPileFaultReasonEnum.values()) { + if (item.getCode() == code) { + return item.getValue(); + } + } + return null; + } + + private int code; + private String value; + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + YKCPileFaultReasonEnum(int code, String value) { + this.code = code; + this.value = value; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/exception/BusinessException.java b/jsowell-common/src/main/java/com/jsowell/common/exception/BusinessException.java new file mode 100644 index 000000000..e55b9cdb0 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/exception/BusinessException.java @@ -0,0 +1,29 @@ +package com.jsowell.common.exception; + +import com.jsowell.common.enums.ykc.ReturnCodeEnum; +import lombok.Data; + +@Data +public class BusinessException extends RuntimeException{ + private static final long serialVersionUID = 1L; + + /** + * 错误码 + */ + private String code; + + /** + * 错误提示 + */ + private String message; + + public BusinessException(ReturnCodeEnum returnCodeEnum) { + this.code = returnCodeEnum.getValue(); + this.message = returnCodeEnum.getLabel(); + } + + public BusinessException(String code, String message) { + this.code = code; + this.message = message; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/exception/DemoModeException.java b/jsowell-common/src/main/java/com/jsowell/common/exception/DemoModeException.java new file mode 100644 index 000000000..d1dfd53d9 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/exception/DemoModeException.java @@ -0,0 +1,13 @@ +package com.jsowell.common.exception; + +/** + * 演示模式异常 + * + * @author jsowell + */ +public class DemoModeException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public DemoModeException() { + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/exception/GlobalException.java b/jsowell-common/src/main/java/com/jsowell/common/exception/GlobalException.java new file mode 100644 index 000000000..46b893228 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/exception/GlobalException.java @@ -0,0 +1,51 @@ +package com.jsowell.common.exception; + +/** + * 全局异常 + * + * @author jsowell + */ +public class GlobalException extends RuntimeException { + private static final long serialVersionUID = 1L; + + /** + * 错误提示 + */ + private String message; + + /** + * 错误明细,内部调试错误 + *

+ * 和 {@link CommonResult#getDetailMessage()} 一致的设计 + */ + private String detailMessage; + + /** + * 空构造方法,避免反序列化问题 + */ + public GlobalException() { + } + + public GlobalException(String message) { + this.message = message; + } + + public String getDetailMessage() { + return detailMessage; + } + + public GlobalException setDetailMessage(String detailMessage) { + this.detailMessage = detailMessage; + return this; + } + + @Override + public String getMessage() { + return message; + } + + public GlobalException setMessage(String message) { + this.message = message; + return this; + } +} \ No newline at end of file diff --git a/jsowell-common/src/main/java/com/jsowell/common/exception/ServiceException.java b/jsowell-common/src/main/java/com/jsowell/common/exception/ServiceException.java new file mode 100644 index 000000000..153a48329 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/exception/ServiceException.java @@ -0,0 +1,65 @@ +package com.jsowell.common.exception; + +/** + * 业务异常 + * + * @author jsowell + */ +public final class ServiceException extends RuntimeException { + private static final long serialVersionUID = 1L; + + /** + * 错误码 + */ + private Integer code; + + /** + * 错误提示 + */ + private String message; + + /** + * 错误明细,内部调试错误 + *

+ * 和 {@link CommonResult#getDetailMessage()} 一致的设计 + */ + private String detailMessage; + + /** + * 空构造方法,避免反序列化问题 + */ + public ServiceException() { + } + + public ServiceException(String message) { + this.message = message; + } + + public ServiceException(String message, Integer code) { + this.message = message; + this.code = code; + } + + public String getDetailMessage() { + return detailMessage; + } + + @Override + public String getMessage() { + return message; + } + + public Integer getCode() { + return code; + } + + public ServiceException setMessage(String message) { + this.message = message; + return this; + } + + public ServiceException setDetailMessage(String detailMessage) { + this.detailMessage = detailMessage; + return this; + } +} \ No newline at end of file diff --git a/jsowell-common/src/main/java/com/jsowell/common/exception/UtilException.java b/jsowell-common/src/main/java/com/jsowell/common/exception/UtilException.java new file mode 100644 index 000000000..fa48fb4e0 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/exception/UtilException.java @@ -0,0 +1,22 @@ +package com.jsowell.common.exception; + +/** + * 工具类异常 + * + * @author jsowell + */ +public class UtilException extends RuntimeException { + private static final long serialVersionUID = 8247610319171014183L; + + public UtilException(Throwable e) { + super(e.getMessage(), e); + } + + public UtilException(String message) { + super(message); + } + + public UtilException(String message, Throwable throwable) { + super(message, throwable); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/exception/base/BaseException.java b/jsowell-common/src/main/java/com/jsowell/common/exception/base/BaseException.java new file mode 100644 index 000000000..2439e1af4 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/exception/base/BaseException.java @@ -0,0 +1,84 @@ +package com.jsowell.common.exception.base; + +import com.jsowell.common.util.MessageUtils; +import com.jsowell.common.util.StringUtils; + +/** + * 基础异常 + * + * @author jsowell + */ +public class BaseException extends RuntimeException { + private static final long serialVersionUID = 1L; + + /** + * 所属模块 + */ + private String module; + + /** + * 错误码 + */ + private String code; + + /** + * 错误码对应的参数 + */ + private Object[] args; + + /** + * 错误消息 + */ + private String defaultMessage; + + public BaseException(String module, String code, Object[] args, String defaultMessage) { + this.module = module; + this.code = code; + this.args = args; + this.defaultMessage = defaultMessage; + } + + public BaseException(String module, String code, Object[] args) { + this(module, code, args, null); + } + + public BaseException(String module, String defaultMessage) { + this(module, null, null, defaultMessage); + } + + public BaseException(String code, Object[] args) { + this(null, code, args, null); + } + + public BaseException(String defaultMessage) { + this(null, null, null, defaultMessage); + } + + @Override + public String getMessage() { + String message = null; + if (!StringUtils.isEmpty(code)) { + message = MessageUtils.message(code, args); + } + if (message == null) { + message = defaultMessage; + } + return message; + } + + public String getModule() { + return module; + } + + public String getCode() { + return code; + } + + public Object[] getArgs() { + return args; + } + + public String getDefaultMessage() { + return defaultMessage; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/exception/file/FileException.java b/jsowell-common/src/main/java/com/jsowell/common/exception/file/FileException.java new file mode 100644 index 000000000..582c71ead --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/exception/file/FileException.java @@ -0,0 +1,17 @@ +package com.jsowell.common.exception.file; + +import com.jsowell.common.exception.base.BaseException; + +/** + * 文件信息异常类 + * + * @author jsowell + */ +public class FileException extends BaseException { + private static final long serialVersionUID = 1L; + + public FileException(String code, Object[] args) { + super("file", code, args, null); + } + +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/exception/file/FileNameLengthLimitExceededException.java b/jsowell-common/src/main/java/com/jsowell/common/exception/file/FileNameLengthLimitExceededException.java new file mode 100644 index 000000000..91ba7a2cb --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/exception/file/FileNameLengthLimitExceededException.java @@ -0,0 +1,14 @@ +package com.jsowell.common.exception.file; + +/** + * 文件名称超长限制异常类 + * + * @author jsowell + */ +public class FileNameLengthLimitExceededException extends FileException { + private static final long serialVersionUID = 1L; + + public FileNameLengthLimitExceededException(int defaultFileNameLength) { + super("upload.filename.exceed.length", new Object[]{defaultFileNameLength}); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/exception/file/FileSizeLimitExceededException.java b/jsowell-common/src/main/java/com/jsowell/common/exception/file/FileSizeLimitExceededException.java new file mode 100644 index 000000000..c9c89f761 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/exception/file/FileSizeLimitExceededException.java @@ -0,0 +1,14 @@ +package com.jsowell.common.exception.file; + +/** + * 文件名大小限制异常类 + * + * @author jsowell + */ +public class FileSizeLimitExceededException extends FileException { + private static final long serialVersionUID = 1L; + + public FileSizeLimitExceededException(long defaultMaxSize) { + super("upload.exceed.maxSize", new Object[]{defaultMaxSize}); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/exception/file/InvalidExtensionException.java b/jsowell-common/src/main/java/com/jsowell/common/exception/file/InvalidExtensionException.java new file mode 100644 index 000000000..51a9b0aa8 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/exception/file/InvalidExtensionException.java @@ -0,0 +1,69 @@ +package com.jsowell.common.exception.file; + +import org.apache.commons.fileupload.FileUploadException; + +import java.util.Arrays; + +/** + * 文件上传 误异常类 + * + * @author jsowell + */ +public class InvalidExtensionException extends FileUploadException { + private static final long serialVersionUID = 1L; + + private String[] allowedExtension; + private String extension; + private String filename; + + public InvalidExtensionException(String[] allowedExtension, String extension, String filename) { + super("文件[" + filename + "]后缀[" + extension + "]不正确,请上传" + Arrays.toString(allowedExtension) + "格式"); + this.allowedExtension = allowedExtension; + this.extension = extension; + this.filename = filename; + } + + public String[] getAllowedExtension() { + return allowedExtension; + } + + public String getExtension() { + return extension; + } + + public String getFilename() { + return filename; + } + + public static class InvalidImageExtensionException extends InvalidExtensionException { + private static final long serialVersionUID = 1L; + + public InvalidImageExtensionException(String[] allowedExtension, String extension, String filename) { + super(allowedExtension, extension, filename); + } + } + + public static class InvalidFlashExtensionException extends InvalidExtensionException { + private static final long serialVersionUID = 1L; + + public InvalidFlashExtensionException(String[] allowedExtension, String extension, String filename) { + super(allowedExtension, extension, filename); + } + } + + public static class InvalidMediaExtensionException extends InvalidExtensionException { + private static final long serialVersionUID = 1L; + + public InvalidMediaExtensionException(String[] allowedExtension, String extension, String filename) { + super(allowedExtension, extension, filename); + } + } + + public static class InvalidVideoExtensionException extends InvalidExtensionException { + private static final long serialVersionUID = 1L; + + public InvalidVideoExtensionException(String[] allowedExtension, String extension, String filename) { + super(allowedExtension, extension, filename); + } + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/exception/job/TaskException.java b/jsowell-common/src/main/java/com/jsowell/common/exception/job/TaskException.java new file mode 100644 index 000000000..8bf5a49af --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/exception/job/TaskException.java @@ -0,0 +1,29 @@ +package com.jsowell.common.exception.job; + +/** + * 计划策略异常 + * + * @author jsowell + */ +public class TaskException extends Exception { + private static final long serialVersionUID = 1L; + + private Code code; + + public TaskException(String msg, Code code) { + this(msg, code, null); + } + + public TaskException(String msg, Code code, Exception nestedEx) { + super(msg, nestedEx); + this.code = code; + } + + public Code getCode() { + return code; + } + + public enum Code { + TASK_EXISTS, NO_TASK_EXISTS, TASK_ALREADY_STARTED, UNKNOWN, CONFIG_ERROR, TASK_NODE_NOT_AVAILABLE + } +} \ No newline at end of file diff --git a/jsowell-common/src/main/java/com/jsowell/common/exception/user/CaptchaException.java b/jsowell-common/src/main/java/com/jsowell/common/exception/user/CaptchaException.java new file mode 100644 index 000000000..e8e411616 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/exception/user/CaptchaException.java @@ -0,0 +1,14 @@ +package com.jsowell.common.exception.user; + +/** + * 验证码错误异常类 + * + * @author jsowell + */ +public class CaptchaException extends UserException { + private static final long serialVersionUID = 1L; + + public CaptchaException() { + super("user.jcaptcha.error", null); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/exception/user/CaptchaExpireException.java b/jsowell-common/src/main/java/com/jsowell/common/exception/user/CaptchaExpireException.java new file mode 100644 index 000000000..7cfcd66a0 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/exception/user/CaptchaExpireException.java @@ -0,0 +1,14 @@ +package com.jsowell.common.exception.user; + +/** + * 验证码失效异常类 + * + * @author jsowell + */ +public class CaptchaExpireException extends UserException { + private static final long serialVersionUID = 1L; + + public CaptchaExpireException() { + super("user.jcaptcha.expire", null); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/exception/user/UserException.java b/jsowell-common/src/main/java/com/jsowell/common/exception/user/UserException.java new file mode 100644 index 000000000..807c84575 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/exception/user/UserException.java @@ -0,0 +1,16 @@ +package com.jsowell.common.exception.user; + +import com.jsowell.common.exception.base.BaseException; + +/** + * 用户信息异常类 + * + * @author jsowell + */ +public class UserException extends BaseException { + private static final long serialVersionUID = 1L; + + public UserException(String code, Object[] args) { + super("user", code, args, null); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/exception/user/UserPasswordNotMatchException.java b/jsowell-common/src/main/java/com/jsowell/common/exception/user/UserPasswordNotMatchException.java new file mode 100644 index 000000000..001ad00e2 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/exception/user/UserPasswordNotMatchException.java @@ -0,0 +1,14 @@ +package com.jsowell.common.exception.user; + +/** + * 用户密码不正确或不符合规范异常类 + * + * @author jsowell + */ +public class UserPasswordNotMatchException extends UserException { + private static final long serialVersionUID = 1L; + + public UserPasswordNotMatchException() { + super("user.password.not.match", null); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/exception/user/UserPasswordRetryLimitExceedException.java b/jsowell-common/src/main/java/com/jsowell/common/exception/user/UserPasswordRetryLimitExceedException.java new file mode 100644 index 000000000..6d117c8df --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/exception/user/UserPasswordRetryLimitExceedException.java @@ -0,0 +1,14 @@ +package com.jsowell.common.exception.user; + +/** + * 用户错误最大次数异常类 + * + * @author jsowell + */ +public class UserPasswordRetryLimitExceedException extends UserException { + private static final long serialVersionUID = 1L; + + public UserPasswordRetryLimitExceedException(int retryLimitCount, int lockTime) { + super("user.password.retry.limit.exceed", new Object[]{retryLimitCount, lockTime}); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/filter/PropertyPreExcludeFilter.java b/jsowell-common/src/main/java/com/jsowell/common/filter/PropertyPreExcludeFilter.java new file mode 100644 index 000000000..f17fdcc3f --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/filter/PropertyPreExcludeFilter.java @@ -0,0 +1,20 @@ +package com.jsowell.common.filter; + +import com.alibaba.fastjson2.filter.SimplePropertyPreFilter; + +/** + * 排除JSON敏感属性 + * + * @author jsowell + */ +public class PropertyPreExcludeFilter extends SimplePropertyPreFilter { + public PropertyPreExcludeFilter() { + } + + public PropertyPreExcludeFilter addExcludes(String... filters) { + for (int i = 0; i < filters.length; i++) { + this.getExcludes().add(filters[i]); + } + return this; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/filter/RepeatableFilter.java b/jsowell-common/src/main/java/com/jsowell/common/filter/RepeatableFilter.java new file mode 100644 index 000000000..ae154f099 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/filter/RepeatableFilter.java @@ -0,0 +1,40 @@ +package com.jsowell.common.filter; + +import com.jsowell.common.util.StringUtils; +import org.springframework.http.MediaType; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * Repeatable 过滤器 + * + * @author jsowell + */ +public class RepeatableFilter implements Filter { + @Override + public void init(FilterConfig filterConfig) throws ServletException { + + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + ServletRequest requestWrapper = null; + if (request instanceof HttpServletRequest + && StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) { + requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response); + } + if (null == requestWrapper) { + chain.doFilter(request, response); + } else { + chain.doFilter(requestWrapper, response); + } + } + + @Override + public void destroy() { + + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/filter/RepeatedlyRequestWrapper.java b/jsowell-common/src/main/java/com/jsowell/common/filter/RepeatedlyRequestWrapper.java new file mode 100644 index 000000000..921793758 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/filter/RepeatedlyRequestWrapper.java @@ -0,0 +1,67 @@ +package com.jsowell.common.filter; + +import com.jsowell.common.constant.Constants; +import com.jsowell.common.util.http.HttpHelper; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * 构建可重复读取inputStream的request + * + * @author jsowell + */ +public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper { + private final byte[] body; + + public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException { + super(request); + request.setCharacterEncoding(Constants.UTF8); + response.setCharacterEncoding(Constants.UTF8); + + body = HttpHelper.getBodyString(request).getBytes(Constants.UTF8); + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(getInputStream())); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + final ByteArrayInputStream bais = new ByteArrayInputStream(body); + return new ServletInputStream() { + @Override + public int read() throws IOException { + return bais.read(); + } + + @Override + public int available() throws IOException { + return body.length; + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) { + + } + }; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/filter/XssFilter.java b/jsowell-common/src/main/java/com/jsowell/common/filter/XssFilter.java new file mode 100644 index 000000000..5a364c0aa --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/filter/XssFilter.java @@ -0,0 +1,62 @@ +package com.jsowell.common.filter; + +import com.jsowell.common.enums.HttpMethod; +import com.jsowell.common.util.StringUtils; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * 防止XSS攻击的过滤器 + * + * @author jsowell + */ +public class XssFilter implements Filter { + /** + * 排除链接 + */ + public List excludes = new ArrayList<>(); + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + String tempExcludes = filterConfig.getInitParameter("excludes"); + if (StringUtils.isNotEmpty(tempExcludes)) { + String[] url = tempExcludes.split(","); + for (int i = 0; url != null && i < url.length; i++) { + excludes.add(url[i]); + } + } + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + if (handleExcludeURL(req, resp)) { + chain.doFilter(request, response); + return; + } + XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request); + chain.doFilter(xssRequest, response); + } + + private boolean handleExcludeURL(HttpServletRequest request, HttpServletResponse response) { + String url = request.getServletPath(); + String method = request.getMethod(); + // GET DELETE 不过滤 + if (method == null || HttpMethod.GET.matches(method) || HttpMethod.DELETE.matches(method)) { + return true; + } + return StringUtils.matches(url, excludes); + } + + @Override + public void destroy() { + + } +} \ No newline at end of file diff --git a/jsowell-common/src/main/java/com/jsowell/common/filter/XssHttpServletRequestWrapper.java b/jsowell-common/src/main/java/com/jsowell/common/filter/XssHttpServletRequestWrapper.java new file mode 100644 index 000000000..de20af9bd --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/filter/XssHttpServletRequestWrapper.java @@ -0,0 +1,97 @@ +package com.jsowell.common.filter; + +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.html.EscapeUtil; +import org.apache.commons.io.IOUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.ByteArrayInputStream; +import java.io.IOException; + +/** + * XSS过滤处理 + * + * @author jsowell + */ +public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { + /** + * @param request + */ + public XssHttpServletRequestWrapper(HttpServletRequest request) { + super(request); + } + + @Override + public String[] getParameterValues(String name) { + String[] values = super.getParameterValues(name); + if (values != null) { + int length = values.length; + String[] escapseValues = new String[length]; + for (int i = 0; i < length; i++) { + // 防xss攻击和过滤前后空格 + escapseValues[i] = EscapeUtil.clean(values[i]).trim(); + } + return escapseValues; + } + return super.getParameterValues(name); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + // 非json类型,直接返回 + if (!isJsonRequest()) { + return super.getInputStream(); + } + + // 为空,直接返回 + String json = IOUtils.toString(super.getInputStream(), "utf-8"); + if (StringUtils.isEmpty(json)) { + return super.getInputStream(); + } + + // xss过滤 + json = EscapeUtil.clean(json).trim(); + byte[] jsonBytes = json.getBytes("utf-8"); + final ByteArrayInputStream bis = new ByteArrayInputStream(jsonBytes); + return new ServletInputStream() { + @Override + public boolean isFinished() { + return true; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public int available() throws IOException { + return jsonBytes.length; + } + + @Override + public void setReadListener(ReadListener readListener) { + } + + @Override + public int read() throws IOException { + return bis.read(); + } + }; + } + + /** + * 是否是Json请求 + * + * @param request + */ + public boolean isJsonRequest() { + String header = super.getHeader(HttpHeaders.CONTENT_TYPE); + return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE); + } +} \ No newline at end of file diff --git a/jsowell-common/src/main/java/com/jsowell/common/response/RestApiResponse.java b/jsowell-common/src/main/java/com/jsowell/common/response/RestApiResponse.java new file mode 100644 index 000000000..7ec0312f8 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/response/RestApiResponse.java @@ -0,0 +1,43 @@ +package com.jsowell.common.response; + +import com.jsowell.common.enums.ykc.ReturnCodeEnum; +import lombok.Data; + +@Data +public class RestApiResponse { + /** + * 返回码 + */ + private String resCode; + + /** + * 信息 + */ + private String msg; + + /** + * 数据 + */ + private T obj; + + public RestApiResponse() { + this.resCode = ReturnCodeEnum.CODE_SUCCESS.getValue(); + this.msg = ReturnCodeEnum.CODE_SUCCESS.getLabel(); + } + + public RestApiResponse(T t) { + this.resCode = ReturnCodeEnum.CODE_SUCCESS.getValue(); + this.msg = ReturnCodeEnum.CODE_SUCCESS.getLabel(); + this.obj = t; + } + + public RestApiResponse(String resCode, String msg) { + this.resCode = resCode; + this.msg = msg; + } + + public RestApiResponse(ReturnCodeEnum returnCodeEnum) { + this.resCode = returnCodeEnum.getValue(); + this.msg = returnCodeEnum.getLabel(); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/AESUtil.java b/jsowell-common/src/main/java/com/jsowell/common/util/AESUtil.java new file mode 100644 index 000000000..29892f7d6 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/AESUtil.java @@ -0,0 +1,176 @@ +package com.jsowell.common.util; + +import org.apache.commons.lang3.RandomStringUtils; +import sun.misc.BASE64Decoder; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.NoSuchAlgorithmException; + +/** + * Copyright (c) 2019-present ShenBao + * + * @author ShenBao + * @homepage https://github.com/ShenBao/rsa-aes-utils + */ +public class AESUtil { + /** + * 随机生成秘钥 + * + * @return String + */ + public static String createAesKey() { + try { + KeyGenerator kg = KeyGenerator.getInstance("AES"); + kg.init(128); + //要生成多少位,只需要修改这里即可 128, 192 或 256 + SecretKey sk = kg.generateKey(); + byte[] b = sk.getEncoded(); + String k = byteToHexString(b); + return k; + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + return null; + } + } + + /** + * 随机生成 IV + * + * @return String + */ + public static String createAesIv() { + String iv = RandomStringUtils.randomAlphanumeric(16); + return iv.toLowerCase(); + } + + /** + * byte 数组转化为 16 进制字符串 + * + * @param bytes + * @return String + */ + public static String byteToHexString(byte[] bytes) { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < bytes.length; i++) { + String strHex = Integer.toHexString(bytes[i]); + if (strHex.length() > 3) { + sb.append(strHex.substring(6)); + } else { + if (strHex.length() < 2) { + sb.append("0" + strHex); + } else { + sb.append(strHex); + } + } + } + return sb.toString(); + } + + /** + * 加密 CBC + * + * @param data String + * @param key String + * @param iv String + * @return String + */ + public static String encryptByCBC(String data, String key, String iv) throws Exception { + try { + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + int blockSize = cipher.getBlockSize(); + byte[] dataBytes = data.getBytes(); + int plaintextLength = dataBytes.length; + if (plaintextLength % blockSize != 0) { + plaintextLength = plaintextLength + (blockSize - (plaintextLength % blockSize)); + } + byte[] plaintext = new byte[plaintextLength]; + System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length); + SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES"); + IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes()); + cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec); + byte[] encrypted = cipher.doFinal(plaintext); + return new sun.misc.BASE64Encoder().encode(encrypted); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * 解密 CBC + * + * @param data String + * @param key String + * @param iv String + * @return String + */ + public static String decryptByCBC(String data, String key, String iv) throws Exception { + try { + byte[] encrypted1 = new BASE64Decoder().decodeBuffer(data); + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES"); + IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes()); + cipher.init(Cipher.DECRYPT_MODE, keyspec, ivspec); + byte[] original = cipher.doFinal(encrypted1); + String originalString = new String(original); + return originalString.trim(); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * 加密 ECB + * + * @param data String + * @param key String + * @return String + */ + public static String encryptByECB(String data, String key) throws Exception { + try { + Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); + int blockSize = cipher.getBlockSize(); + byte[] dataBytes = data.getBytes(); + int plaintextLength = dataBytes.length; + if (plaintextLength % blockSize != 0) { + plaintextLength = plaintextLength + (blockSize - (plaintextLength % blockSize)); + } + byte[] plaintext = new byte[plaintextLength]; + System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length); + SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES"); + cipher.init(Cipher.ENCRYPT_MODE, keyspec); + byte[] encrypted = cipher.doFinal(plaintext); + return new sun.misc.BASE64Encoder().encode(encrypted); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * 解密 ECB + * + * @param data String + * @param key String + * @return String + */ + public static String decryptByECB(String data, String key) throws Exception { + try { + byte[] encrypted1 = new BASE64Decoder().decodeBuffer(data); + Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); + SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES"); + cipher.init(Cipher.DECRYPT_MODE, keyspec); + byte[] original = cipher.doFinal(encrypted1); + String originalString = new String(original); + return originalString.trim(); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } +} \ No newline at end of file diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/Arith.java b/jsowell-common/src/main/java/com/jsowell/common/util/Arith.java new file mode 100644 index 000000000..4620d447a --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/Arith.java @@ -0,0 +1,113 @@ +package com.jsowell.common.util; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * 精确的浮点数运算 + * + * @author jsowell + */ +public class Arith { + + /** + * 默认除法运算精度 + */ + private static final int DEF_DIV_SCALE = 10; + + /** + * 这个类不能实例化 + */ + private Arith() { + } + + /** + * 提供精确的加法运算。 + * + * @param v1 被加数 + * @param v2 加数 + * @return 两个参数的和 + */ + public static double add(double v1, double v2) { + BigDecimal b1 = new BigDecimal(Double.toString(v1)); + BigDecimal b2 = new BigDecimal(Double.toString(v2)); + return b1.add(b2).doubleValue(); + } + + /** + * 提供精确的减法运算。 + * + * @param v1 被减数 + * @param v2 减数 + * @return 两个参数的差 + */ + public static double sub(double v1, double v2) { + BigDecimal b1 = new BigDecimal(Double.toString(v1)); + BigDecimal b2 = new BigDecimal(Double.toString(v2)); + return b1.subtract(b2).doubleValue(); + } + + /** + * 提供精确的乘法运算。 + * + * @param v1 被乘数 + * @param v2 乘数 + * @return 两个参数的积 + */ + public static double mul(double v1, double v2) { + BigDecimal b1 = new BigDecimal(Double.toString(v1)); + BigDecimal b2 = new BigDecimal(Double.toString(v2)); + return b1.multiply(b2).doubleValue(); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到 + * 小数点以后10位,以后的数字四舍五入。 + * + * @param v1 被除数 + * @param v2 除数 + * @return 两个参数的商 + */ + public static double div(double v1, double v2) { + return div(v1, v2, DEF_DIV_SCALE); + } + + /** + * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指 + * 定精度,以后的数字四舍五入。 + * + * @param v1 被除数 + * @param v2 除数 + * @param scale 表示表示需要精确到小数点以后几位。 + * @return 两个参数的商 + */ + public static double div(double v1, double v2, int scale) { + if (scale < 0) { + throw new IllegalArgumentException( + "The scale must be a positive integer or zero"); + } + BigDecimal b1 = new BigDecimal(Double.toString(v1)); + BigDecimal b2 = new BigDecimal(Double.toString(v2)); + if (b1.compareTo(BigDecimal.ZERO) == 0) { + return BigDecimal.ZERO.doubleValue(); + } + return b1.divide(b2, scale, RoundingMode.HALF_UP).doubleValue(); + } + + /** + * 提供精确的小数位四舍五入处理。 + * + * @param v 需要四舍五入的数字 + * @param scale 小数点后保留几位 + * @return 四舍五入后的结果 + */ + public static double round(double v, int scale) { + if (scale < 0) { + throw new IllegalArgumentException( + "The scale must be a positive integer or zero"); + } + BigDecimal b = new BigDecimal(Double.toString(v)); + BigDecimal one = BigDecimal.ONE; + return b.divide(one, scale, RoundingMode.HALF_UP).doubleValue(); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/BytesUtil.java b/jsowell-common/src/main/java/com/jsowell/common/util/BytesUtil.java new file mode 100644 index 000000000..ce07711cd --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/BytesUtil.java @@ -0,0 +1,706 @@ +package com.jsowell.common.util; + +import com.google.common.primitives.Bytes; + +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.Stack; + +public class BytesUtil { + + static final long fx = 0xffl; + + /** + * 将int数值转换为占两个字节的byte数组,本方法适用于(高位在前,低位在后)的顺序。 + */ + public static byte[] intToBytes(int value) { + //limit 传入2 + return intToBytes(value, 2); + } + + public static byte[] intToBytes(int value, int limit) { + byte[] src = new byte[limit]; + for (int i = 0; i < limit; i++) { + int x = 8 * (limit - i - 1); + if (x == 0) { + src[i] = (byte) (value & 0xFF); + } else { + src[i] = (byte) ((value >> x) & 0xFF); + } + } + return src; + } + + public static int bytesToInt(byte[] src) { + return bytesToInt(src, 0); + } + + /** + * byte数组中取int数值,本方法适用于(低位在后,高位在前)的顺序。 + */ + public static int bytesToInt(byte[] src, int offset) { + if (src == null) { + return 0; + } + while (src.length > 0 && src[0] == 0x00) { + src = BytesUtil.copyBytes(src, 1, src.length - 1); + } + if (src.length == 0) { + return 0; + } + int len = src.length; + if (len == 0) { + return 0; + } + int value = 0; + for (int i = 0; i < len; i++) { + if (i == (len - 1)) { + value = value | ((src[i] & 0xFF)); + } + value = value | ((src[i] & 0xFF) << (8 * (len - i - 1))); + } + return value; + } + + /** + * long转字节,大端模式 + * + * @param number + * @return + */ + public static byte[] long2Byte(long number) { + long temp = number; + byte[] b = new byte[8]; + for (int i = (b.length - 1); i >= 0; i--) { + b[i] = new Long(temp & 0xff).byteValue();// + //将最低位保存在最低位 + temp = temp >> 8;// 向右移8位 + } + return b; + } + + /** + * 字节转long 大端模式 + * + * @param b + * @return + */ + public static long byte2Long(byte[] b) { + long s = 0; + long s0 = b[7] & 0xff; + long s1 = b[6] & 0xff; + long s2 = b[5] & 0xff; + long s3 = b[4] & 0xff; + long s4 = b[3] & 0xff; + long s5 = b[2] & 0xff; + long s6 = b[1] & 0xff; + long s7 = b[0] & 0xff; + + // s0不变 + s1 <<= 8; + s2 <<= 16; + s3 <<= 24; + s4 <<= 8 * 4; + s5 <<= 8 * 5; + s6 <<= 8 * 6; + s7 <<= 8 * 7; + s = s0 | s1 | s2 | s3 | s4 | s5 | s6 | s7; + return s; + } + + /** + * 从一个byte数组中拷贝一部分出来 + * + * @param oriBytes + * @param startIndex + * @param length + * @return + */ + public static byte[] copyBytes(byte[] oriBytes, int startIndex, int length) { + int endIndex = startIndex + length; + byte[] bts = new byte[length]; + int index = 0; + for (int i = 0; i < oriBytes.length; i++) { + if (i >= startIndex && i < endIndex) { + bts[index] = oriBytes[i]; + index++; + } + } + return bts; + } + + /** + * 将byte[]转为各种进制的字符串 + * + * @param bytes byte[] + * @param radix 基数可以转换进制的范围,从Character.MIN_RADIX到Character.MAX_RADIX,超出范围后变为10进制 + * @return 转换后的字符串 + */ + public static String binary(byte[] bytes, int radix) { + return new BigInteger(1, bytes).toString(radix);// 这里的1代表正数 + } + + /** + * @函数功能: BCD码转为10进制串(阿拉伯数据) + * @输入参数: BCD码 + * @输出结果: 10进制串 + */ + public static String bcd2Str(byte[] bytes) { + StringBuffer temp = new StringBuffer(bytes.length * 2); + for (int i = 0; i < bytes.length; i++) { + temp.append((byte) ((bytes[i] & 0xf0) >>> 4)); + temp.append((byte) (bytes[i] & 0x0f)); + } + return temp.toString(); + } + + public static String bcd2StrContainA(byte[] bytes) { + char temp[] = new char[bytes.length * 2], val; + for (int i = 0; i < bytes.length; i++) { + val = (char) (((bytes[i] & 0xf0) >> 4) & 0x0f); + temp[i * 2] = (char) (val > 9 ? val + 'A' - 10 : val + '0'); + val = (char) (bytes[i] & 0x0f); + temp[i * 2 + 1] = (char) (val > 9 ? val + 'A' - 10 : val + '0'); + } + return new String(temp); + } + + /** + * @函数功能: BCD码转为10进制串(阿拉伯数据) 小端模式 + * @输入参数: BCD码 + * @输出结果: 10进制串 + */ + public static String bcd2StrLittle(byte[] bytes) { + Stack strings = new Stack<>(); + String temp = bcd2Str(bytes); + for (int i = 0; i < temp.length(); i = i + 2) { + strings.push(temp.substring(i, i + 2)); + } + StringBuilder stringBuilder = new StringBuilder(); + while (!strings.isEmpty()) { + stringBuilder.append(strings.pop()); + } + return stringBuilder.toString(); + } + + /** + * @函数功能: BCD码转为10进制串(阿拉伯数据) 小端模式 + * @输入参数: BCD码 + * @输出结果: 10进制串 + */ + public static String bcd2StrLittleContainA(byte[] bytes) { + Stack strings = new Stack<>(); + String temp = bcd2StrContainA(bytes); + for (int i = 0; i < temp.length(); i = i + 2) { + strings.push(temp.substring(i, i + 2)); + } + StringBuilder stringBuilder = new StringBuilder(); + while (!strings.isEmpty()) { + stringBuilder.append(strings.pop()); + } + return stringBuilder.toString(); + } + + /** + * @函数功能: 10进制串转为BCD码 + * @输入参数: 10进制串 + * @输出结果: BCD码 + */ + public static byte[] str2Bcd(String asc) { + if (asc == null) { + asc = ""; + } + int len = asc.length(); + int mod = len % 2; + if (mod != 0) { + asc = "0" + asc; + len = asc.length(); + } + byte abt[] = new byte[len]; + if (len >= 2) { + len = len / 2; + } + byte bbt[] = new byte[len]; + abt = asc.getBytes(); + int j, k; + for (int p = 0; p < asc.length() / 2; p++) { + if ((abt[2 * p] >= '0') && (abt[2 * p] <= '9')) { + j = abt[2 * p] - '0'; + } else if ((abt[2 * p] >= 'a') && (abt[2 * p] <= 'z')) { + j = abt[2 * p] - 'a' + 0x0a; + } else { + j = abt[2 * p] - 'A' + 0x0a; + } + if ((abt[2 * p + 1] >= '0') && (abt[2 * p + 1] <= '9')) { + k = abt[2 * p + 1] - '0'; + } else if ((abt[2 * p + 1] >= 'a') && (abt[2 * p + 1] <= 'z')) { + k = abt[2 * p + 1] - 'a' + 0x0a; + } else { + k = abt[2 * p + 1] - 'A' + 0x0a; + } + int a = (j << 4) + k; + byte b = (byte) a; + bbt[p] = b; + } + return bbt; + } + + /** + * @函数功能: 10进制串转为BCD码 + * @输入参数: 10进制串 + * @输出结果: BCD码 + */ + public static byte[] str2BcdLittle(String asc) { + byte[] temp = str2Bcd(asc); + return revert(temp); + } + + /** + * 将int转为byte 小端模式 + * + * @param value int值 + * @return + */ + public static byte[] intToBytesLittle(int value) { + //limit 传入2 + return intToBytes(value, 2); + } + + /** + * 将int转换byte 小端模式 + * + * @param value int值 + * @param limit 保留长度 + * @return + */ + public static byte[] intToBytesLittle(int value, int limit) { + byte[] src = new byte[limit]; + for (int i = 0; i < limit; i++) { + int x = 8 * i; + if (x == 0) { + src[i] = (byte) (value & 0xFF); + } else { + src[i] = (byte) ((value >> x) & 0xFF); + } + } + return src; + } + + /** + * byte数组中取int数值,本方法适用于(低位在前,高位在后 )的顺序。小端模式 + */ + public static int bytesToIntLittle(byte[] src) { + if (src == null) { + return 0; + } + int len = src.length; + if (len == 0) { + return 0; + } + int value = 0; + for (int i = 0; i < len; i++) { + value = value | ((src[i] & 0xFF) << (8 * i)); + } + return value; + } + + /** + * long转字节,小端模式 + * + * @param number + * @param limit 保留字节位 + * @return + */ + public static byte[] long2ByteLittle(long number, int limit) { + long temp = number; + byte[] b = new byte[limit]; + for (int i = 0; i < b.length; i++) { + b[i] = new Long(temp & 0xff).byteValue();// + //将最低位保存在最前面 + temp = temp >> 8;// 向右移8位 + } + return b; + } + + + /** + * 字节转long 小端模式 + * + * @param src + * @return + */ + public static long byte2LongLittle(byte[] src) { + long s = 0; + for (int i = 0; i < src.length; i++) { + //防止转为int + long si = src[i] & 0xFF; + si = si << (8 * i); + s = s | si; + } + return s; + } + + /** + * 使用字节数组替换目标数组从指定位置开始替换字节 + * + * @param target 被替换的数组 + * @param startIndex 开始位置 + * @param replace 用于替换的数组 + */ + public static void replaceBytes(byte[] target, int startIndex, byte[] replace) { + // 暂时由外界保证不会出数组越界的异常 + for (int i = 0; i < replace.length; i++) { + int targetIndex = startIndex + i; + target[targetIndex] = replace[i]; + } + } + + /** + * 创建数组 + * + * @param bytes 用于替换的数组 + */ + public static byte[] createByteArray(byte... bytes) { + byte[] temp = new byte[bytes.length]; + for (int i = 0; i < bytes.length; i++) { + temp[i] = bytes[i]; + } + return temp; + } + + public static byte[] rightPadBytes(byte[] target, int len, byte b) { + int length = target.length; + if (len <= length) { + return target; + } + int addedLen = len - length; + byte[] added = new byte[addedLen]; + for (int i = 0; i < addedLen; i++) { + added[i] = b; + } + return Bytes.concat(target, added); + } + + public static byte[] rightPadBytes(String targetStr, int len, byte b) { + if (targetStr == null) { + targetStr = ""; + } + byte[] target = targetStr.getBytes(); + return rightPadBytes(target, len, b); + } + + public static String ascii2StrLittle(byte[] ascs) { + byte[] data = revert(ascs); + String asciiStr = null; + try { + asciiStr = new String(data, "ISO8859-1"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + return asciiStr; + } + + public static String ascii2Str(byte[] ascs) { + byte[] data = ascs; + String asciiStr = null; + try { + asciiStr = new String(data, "ISO8859-1"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + return asciiStr; + } + + public static byte[] str2AscLittle(String str) { + return revert(str2Asc(str)); + } + + public static byte[] str2Asc(String str) { + byte[] bytes = null; + try { + bytes = str.getBytes("ISO8859-1"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + return bytes; + } + + /** + * 反转byte数组 + * + * @param temp + * @return + */ + public static byte[] revert(byte[] temp) { + byte[] ret = new byte[temp.length]; + for (int i = 0; i < temp.length; i++) { + ret[temp.length - i - 1] = temp[i]; + } + return ret; + } + + /** + * cp56time2a 格式转date格式 + */ + public static Date byteCp2Date(byte[] bytes) { + if (bytes == null || bytes.length != 7) { + return null; + } + int ms = bytesToIntLittle(copyBytes(bytes, 0, 2)); + int min = bytesToIntLittle(copyBytes(bytes, 2, 1)); + int hour = bytesToIntLittle(copyBytes(bytes, 3, 1)); + int day = bytesToIntLittle(copyBytes(bytes, 4, 1)); + int month = bytesToIntLittle(copyBytes(bytes, 5, 1)); + int year = bytesToIntLittle(copyBytes(bytes, 6, 1)); + if (month == 0 || day == 0 || year == 0) { + return null; + } + LocalDateTime localDateTime = LocalDateTime.of(year + 2000, month, day, hour, min, ms / 1000); + Date date = DateUtils.localDateTime2Date(localDateTime); + return date; + } + + private static String hexStr = "0123456789ABCDEF"; + private static String[] binaryArray = { + "0000", "0001", "0010", "0011", + "0100", "0101", "0110", "0111", + "1000", "1001", "1010", "1011", + "1100", "1101", "1110", "1111" + }; + + /** + * @param bArray + * @return 二进制数组转换为二进制字符串 2-2 + */ + public static String bytes2BinStr(byte[] bArray) { + String outStr = ""; + int pos = 0; + for (byte b : bArray) { + //高四位 + pos = (b & 0xF0) >> 4; + outStr += binaryArray[pos]; + //低四位 + pos = b & 0x0F; + outStr += binaryArray[pos]; + } + return outStr; + } + + /** + * @param bytes + * @return 将二进制数组转换为十六进制字符串 2-16 + */ + public static String bin2HexStr(byte[] bytes) { + String result = ""; + String hex = ""; + for (byte aByte : bytes) { + //字节高4位 + hex = String.valueOf(hexStr.charAt((aByte & 0xF0) >> 4)); + //字节低4位 + hex += String.valueOf(hexStr.charAt(aByte & 0x0F)); + result += hex; //+" " + } + return result; + } + + public static void main(String[] args) { + // byte[] a = new byte[] {0x0C, 0x00, 0x00, 0x00, 0x02, 0x20, 0x22, 0x12, 0x14, 0x00, 0x00, 0x01, 0x00}; + // byte[] bytes = intToBytes(CRC16Util.calcCrc16(a)); + // String binary = binary(bytes, 16); + // System.out.println(binary); + + // String a = "10"; + // byte[] bytes = bigDecimal2Bcd(new BigDecimal(a)); + // + // System.out.println(bytes); + + // byte[] startTimeByteArr = new byte[] {(byte) 0x98, (byte) 0xB7, 0x0E, 0x11, 0x10, 0x03, 0x14}; + // String binary = binary(startTimeByteArr, 16); + // String s = DateUtils.decodeCP56Time2a(binary); + // System.out.println(s); + + BigDecimal chargeAmount = new BigDecimal("10.5").setScale(2, BigDecimal.ROUND_HALF_UP); + byte[] accountBalanceByteArr = BytesUtil.getFloatBytes(chargeAmount.floatValue()); + + float aFloat = BytesUtil.getFloat(accountBalanceByteArr); + + System.out.println(aFloat); + + } + + /** + * @param hexString + * @return 将十六进制转换为二进制字节数组 16-2 + */ + public static byte[] hexStr2BinArr(String hexString) { + //hexString的长度对2取整,作为bytes的长度 + int len = hexString.length() / 2; + byte[] bytes = new byte[len]; + byte high = 0;//字节高四位 + byte low = 0;//字节低四位 + for (int i = 0; i < len; i++) { + //右移四位得到高位 + high = (byte) ((hexStr.indexOf(hexString.charAt(2 * i))) << 4); + low = (byte) hexStr.indexOf(hexString.charAt(2 * i + 1)); + bytes[i] = (byte) (high | low);//高地位做或运算 + } + return bytes; + } + + /** + * @param src 16进制字符串 + * @return 字节数组 + * @Title hexString2Bytes + * @Description 16进制字符串转字节数组 + */ + public static byte[] hexString2Bytes(String src) { + int l = src.length() / 2; + byte[] ret = new byte[l]; + for (int i = 0; i < l; i++) { + ret[i] = (byte) Integer.valueOf(src.substring(i * 2, i * 2 + 2), 16).byteValue(); + } + return ret; + } + + /** + * @param hexString + * @return 将十六进制转换为二进制字符串 16-2 + */ + public static String hexStr2BinStr(String hexString) { + return bytes2BinStr(hexStr2BinArr(hexString)); + } + + /** + * 校验数据长度是否达到要求,如果够长,就直接返回,如果不够,则在数据 后补0 直至到达该长度 + * + * @param msg byte类型数组 + * @parm length 需要检验的数据长度(为实际字节数) + */ + public static byte[] checkLengthAndBehindAppendZero(byte[] msg, int length) { + String s = BytesUtil.binary(msg, 16); + int msgLen = msg.length; + if (msgLen < length) { + while (msgLen < length) { + StringBuffer sb = new StringBuffer(); + // 后补零 + sb.append(s).append("0"); + s = sb.toString(); + msgLen = s.length(); + } + } else { + return msg; + } + return BytesUtil.str2Bcd(s); + } + + /** + * 校验数据长度是否达到要求,如果够长,就直接返回,如果不够,则在数据 前补0 直至到达该长度 + * + * @param msg byte类型数组 + * @parm length 需要检验的数据长度(为实际字节数) + */ + public static byte[] checkLengthAndFrontAppendZero(byte[] msg, int length) { + String s = BytesUtil.binary(msg, 16); + int msgLen = msg.length; + if (msgLen < length) { + while (msgLen < length) { + StringBuffer sb = new StringBuffer(); + // 前补零 + sb.append("0").append(s); + s = sb.toString(); + msgLen = s.length(); + } + } else { + return msg; + } + return BytesUtil.str2Bcd(s); + } + + /** + * 将金额转换成BCD数组 + * + * @param bigDecimal + * @return + */ + public static byte[] bigDecimal2Bcd(BigDecimal bigDecimal) { + int i = Float.floatToIntBits(bigDecimal.floatValue()); + String hexString = Integer.toHexString(i); + return hexString2Bytes(hexString); + + } + + /** + * int 转 byte[] + * 小端 + * + * @param data + * @return + */ + public static byte[] getIntBytes(int data) { + int length = 4; + byte[] bytes = new byte[length]; + for (int i = 0; i < length; i++) { + bytes[i] = (byte) ((data >> (i * 8)) & fx); + } + return bytes; + } + + /** + * float 转 byte[] + * 小端 + * + * @param data + * @return + */ + public static byte[] getFloatBytes(float data) { + int intBits = Float.floatToIntBits(data); + + byte[] bytes = getIntBytes(intBits); + + return bytes; + } + + /** + * byte[] 转 int + * + * @param bytes + * @return + */ + public static int getInt(byte[] bytes) { + int result = (int) ((fx & bytes[0]) + | ((fx & bytes[1]) << 8) + | ((fx & bytes[2]) << 16) + | ((fx & bytes[3]) << 24)); + + return result; + } + + /** + * byte[] 转 float + * + * @param b + * @return + */ + public static float getFloat(byte[] b) { + int l = getInt(b); + return Float.intBitsToFloat(l); + } + + /** + * 将String转换为byte[] + * + * @param s String + * @return byte[] + */ + public static byte[] hexStringToByteArray(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16)); + } + return data; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/CRC16Util.java b/jsowell-common/src/main/java/com/jsowell/common/util/CRC16Util.java new file mode 100644 index 000000000..c9f3d820b --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/CRC16Util.java @@ -0,0 +1,235 @@ +package com.jsowell.common.util; + +import com.google.common.primitives.Bytes; + +/** + * CRC16相关计算 + *

+ * encode: utf-8 + */ +public class CRC16Util { + + private static byte[] crc16_tab_h = { + (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, + (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, + (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, + (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, + (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, + (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, + (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, + (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, + (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, + (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, + (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, + (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, + (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, + (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, + (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, + (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, + (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, + (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, + (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, + (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, + (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, + (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, + (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, + (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, + (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, + (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, + (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, + (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, + (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, + (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, + (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, + (byte) 0x01, (byte) 0xc0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xc1, (byte) 0x81, (byte) 0x40 + + }; + + private static byte[] crc16_tab_l = { + 0x00, (byte) 0xc0, (byte) 0xc1, (byte) 0x01, (byte) 0xc3, (byte) 0x03, (byte) 0x02, (byte) 0xc2, (byte) 0xc6, (byte) 0x06, (byte) + 0x07, (byte) 0xc7, (byte) 0x05, (byte) 0xc5, (byte) 0xc4, (byte) 0x04, (byte) 0xcc, (byte) 0x0c, (byte) 0x0d, (byte) 0xcd, (byte) + 0x0f, (byte) 0xcf, (byte) 0xce, (byte) 0x0e, (byte) 0x0a, (byte) 0xca, (byte) 0xcb, (byte) 0x0b, (byte) 0xc9, (byte) 0x09, (byte) + 0x08, (byte) 0xc8, (byte) 0xd8, (byte) 0x18, (byte) 0x19, (byte) 0xd9, (byte) 0x1b, (byte) 0xdb, (byte) 0xda, (byte) 0x1a, (byte) + 0x1e, (byte) 0xde, (byte) 0xdf, (byte) 0x1f, (byte) 0xdd, (byte) 0x1d, (byte) 0x1c, (byte) 0xdc, (byte) 0x14, (byte) 0xd4, (byte) + 0xd5, (byte) 0x15, (byte) 0xd7, (byte) 0x17, (byte) 0x16, (byte) 0xd6, (byte) 0xd2, (byte) 0x12, (byte) 0x13, (byte) 0xd3, (byte) + 0x11, (byte) 0xd1, (byte) 0xd0, (byte) 0x10, (byte) 0xf0, (byte) 0x30, (byte) 0x31, (byte) 0xf1, (byte) 0x33, (byte) 0xf3, (byte) + 0xf2, (byte) 0x32, (byte) 0x36, (byte) 0xf6, (byte) 0xf7, (byte) 0x37, (byte) 0xf5, (byte) 0x35, (byte) 0x34, (byte) 0xf4, (byte) + 0x3c, (byte) 0xfc, (byte) 0xfd, (byte) 0x3d, (byte) 0xff, (byte) 0x3f, (byte) 0x3e, (byte) 0xfe, (byte) 0xfa, (byte) 0x3a, (byte) + 0x3b, (byte) 0xfb, (byte) 0x39, (byte) 0xf9, (byte) 0xf8, (byte) 0x38, (byte) 0x28, (byte) 0xe8, (byte) 0xe9, (byte) 0x29, (byte) + 0xeb, (byte) 0x2b, (byte) 0x2a, (byte) 0xea, (byte) 0xee, (byte) 0x2e, (byte) 0x2f, (byte) 0xef, (byte) 0x2d, (byte) 0xed, (byte) + 0xec, (byte) 0x2c, (byte) 0xe4, (byte) 0x24, (byte) 0x25, (byte) 0xe5, (byte) 0x27, (byte) 0xe7, (byte) 0xe6, (byte) 0x26, (byte) + 0x22, (byte) 0xe2, (byte) 0xe3, (byte) 0x23, (byte) 0xe1, (byte) 0x21, (byte) 0x20, (byte) 0xe0, (byte) 0xa0, (byte) 0x60, (byte) + 0x61, (byte) 0xa1, (byte) 0x63, (byte) 0xa3, (byte) 0xa2, (byte) 0x62, (byte) 0x66, (byte) 0xa6, (byte) 0xa7, (byte) 0x67, (byte) + 0xa5, (byte) 0x65, (byte) 0x64, (byte) 0xa4, (byte) 0x6c, (byte) 0xac, (byte) 0xad, (byte) 0x6d, (byte) 0xaf, (byte) 0x6f, (byte) + 0x6e, (byte) 0xae, (byte) 0xaa, (byte) 0x6a, (byte) 0x6b, (byte) 0xab, (byte) 0x69, (byte) 0xa9, (byte) 0xa8, (byte) 0x68, (byte) + 0x78, (byte) 0xb8, (byte) 0xb9, (byte) 0x79, (byte) 0xbb, (byte) 0x7b, (byte) 0x7a, (byte) 0xba, (byte) 0xbe, (byte) 0x7e, (byte) + 0x7f, (byte) 0xbf, (byte) 0x7d, (byte) 0xbd, (byte) 0xbc, (byte) 0x7c, (byte) 0xb4, (byte) 0x74, (byte) 0x75, (byte) 0xb5, (byte) + 0x77, (byte) 0xb7, (byte) 0xb6, (byte) 0x76, (byte) 0x72, (byte) 0xb2, (byte) 0xb3, (byte) 0x73, (byte) 0xb1, (byte) 0x71, (byte) + 0x70, (byte) 0xb0, (byte) 0x50, (byte) 0x90, (byte) 0x91, (byte) 0x51, (byte) 0x93, (byte) 0x53, (byte) 0x52, (byte) 0x92, (byte) + 0x96, (byte) 0x56, (byte) 0x57, (byte) 0x97, (byte) 0x55, (byte) 0x95, (byte) 0x94, (byte) 0x54, (byte) 0x9c, (byte) 0x5c, (byte) + 0x5d, (byte) 0x9d, (byte) 0x5f, (byte) 0x9f, (byte) 0x9e, (byte) 0x5e, (byte) 0x5a, (byte) 0x9a, (byte) 0x9b, (byte) 0x5b, (byte) + 0x99, (byte) 0x59, (byte) 0x58, (byte) 0x98, (byte) 0x88, (byte) 0x48, (byte) 0x49, (byte) 0x89, (byte) 0x4b, (byte) 0x8b, (byte) + 0x8a, (byte) 0x4a, (byte) 0x4e, (byte) 0x8e, (byte) 0x8f, (byte) 0x4f, (byte) 0x8d, (byte) 0x4d, (byte) 0x4c, (byte) 0x8c, (byte) + 0x44, (byte) 0x84, (byte) 0x85, (byte) 0x45, (byte) 0x87, (byte) 0x47, (byte) 0x46, (byte) 0x86, (byte) 0x82, (byte) 0x42, (byte) + 0x43, (byte) 0x83, (byte) 0x41, (byte) 0x81, (byte) 0x80, (byte) 0x40 + }; + + private static byte[] crc16_tab_l_old = { + 0x00, (byte) 0xc0, (byte) 0xc1, (byte) 0x01, (byte) 0xc3, (byte) 0x03, (byte) 0x02, (byte) 0xc2, (byte) 0xc6, (byte) 0x06, (byte) + 0x07, (byte) 0xc7, (byte) 0x05, (byte) 0xc5, (byte) 0xc4, (byte) 0x04, (byte) 0xcc, (byte) 0x0c, (byte) 0x0d, (byte) 0xcd, (byte) + 0x0f, (byte) 0xcf, (byte) 0xce, (byte) 0x0e, (byte) 0x0a, (byte) 0xca, (byte) 0xcb, (byte) 0x0b, (byte) 0xc9, (byte) 0x09, (byte) + 0x08, (byte) 0xc8, (byte) 0xd8, (byte) 0x18, (byte) 0x19, (byte) 0xd9, (byte) 0x1b, (byte) 0xdb, (byte) 0xda, (byte) 0x1a, (byte) + 0x1e, (byte) 0xde, (byte) 0xdf, (byte) 0x1f, (byte) 0xdd, (byte) 0x1d, (byte) 0x1c, (byte) 0xdc, (byte) 0x14, (byte) 0xd4, (byte) + 0xd5, (byte) 0x15, (byte) 0xd7, (byte) 0x17, (byte) 0x16, (byte) 0xd6, (byte) 0xd2, (byte) 0x12, (byte) 0x13, (byte) 0xd3, (byte) + 0x11, (byte) 0xd1, (byte) 0xd0, (byte) 0x10, (byte) 0xf0, (byte) 0x30, (byte) 0x31, (byte) 0xf1, (byte) 0x33, (byte) 0xf3, (byte) + 0xf2, (byte) 0x32, (byte) 0x36, (byte) 0xf6, (byte) 0xf7, (byte) 0x37, (byte) 0xf5, (byte) 0x35, (byte) 0x34, (byte) 0xf4, (byte) + 0x3c, (byte) 0xfc, (byte) 0xfd, (byte) 0x3d, (byte) 0xff, (byte) 0x3f, (byte) 0x3e, (byte) 0xfe, (byte) 0xfa, (byte) 0x3a, (byte) + 0x3b, (byte) 0xfb, (byte) 0x39, (byte) 0xf9, (byte) 0xf8, (byte) 0x40, (byte) 0x28, (byte) 0xe8, (byte) 0xe9, (byte) 0x29, (byte) + 0xeb, (byte) 0x2b, (byte) 0x2a, (byte) 0xea, (byte) 0xee, (byte) 0x2e, (byte) 0x2f, (byte) 0xef, (byte) 0x2d, (byte) 0xed, (byte) + 0xec, (byte) 0x2c, (byte) 0xe4, (byte) 0x24, (byte) 0x25, (byte) 0xe5, (byte) 0x27, (byte) 0xe7, (byte) 0xe6, (byte) 0x26, (byte) + 0x22, (byte) 0xe2, (byte) 0xe3, (byte) 0x23, (byte) 0xe1, (byte) 0x21, (byte) 0x20, (byte) 0xe0, (byte) 0xa0, (byte) 0x60, (byte) + 0x61, (byte) 0xa1, (byte) 0x63, (byte) 0xa3, (byte) 0xa2, (byte) 0x62, (byte) 0x66, (byte) 0xa6, (byte) 0xa7, (byte) 0x67, (byte) + 0xa5, (byte) 0x65, (byte) 0x64, (byte) 0xa4, (byte) 0x6c, (byte) 0xac, (byte) 0xad, (byte) 0x6d, (byte) 0xaf, (byte) 0x6f, (byte) + 0x6e, (byte) 0xae, (byte) 0xaa, (byte) 0x6a, (byte) 0x6b, (byte) 0xab, (byte) 0x69, (byte) 0xa9, (byte) 0xa8, (byte) 0x68, (byte) + 0x78, (byte) 0xb8, (byte) 0xb9, (byte) 0x79, (byte) 0xbb, (byte) 0x7b, (byte) 0x7a, (byte) 0xba, (byte) 0xbe, (byte) 0x7e, (byte) + 0x7f, (byte) 0xbf, (byte) 0x7d, (byte) 0xbd, (byte) 0xbc, (byte) 0x7c, (byte) 0xb4, (byte) 0x74, (byte) 0x75, (byte) 0xb5, (byte) + 0x77, (byte) 0xb7, (byte) 0xb6, (byte) 0x76, (byte) 0x72, (byte) 0xb2, (byte) 0xb3, (byte) 0x73, (byte) 0xb1, (byte) 0x71, (byte) + 0x70, (byte) 0xb0, (byte) 0x50, (byte) 0x90, (byte) 0x91, (byte) 0x51, (byte) 0x93, (byte) 0x53, (byte) 0x52, (byte) 0x92, (byte) + 0x96, (byte) 0x56, (byte) 0x57, (byte) 0x97, (byte) 0x55, (byte) 0x95, (byte) 0x94, (byte) 0x54, (byte) 0x9c, (byte) 0x5c, (byte) + 0x5d, (byte) 0x9d, (byte) 0x5f, (byte) 0x9f, (byte) 0x9e, (byte) 0x5e, (byte) 0x5a, (byte) 0x9a, (byte) 0x9b, (byte) 0x5b, (byte) + 0x99, (byte) 0x59, (byte) 0x58, (byte) 0x98, (byte) 0x88, (byte) 0x48, (byte) 0x49, (byte) 0x89, (byte) 0x4b, (byte) 0x8b, (byte) + 0x8a, (byte) 0x4a, (byte) 0x4e, (byte) 0x8e, (byte) 0x8f, (byte) 0x4f, (byte) 0x8d, (byte) 0x4d, (byte) 0x4c, (byte) 0x8c, (byte) + 0x44, (byte) 0x84, (byte) 0x85, (byte) 0x45, (byte) 0x87, (byte) 0x47, (byte) 0x46, (byte) 0x86, (byte) 0x82, (byte) 0x42, (byte) + 0x43, (byte) 0x83, (byte) 0x41, (byte) 0x81, (byte) 0x80, (byte) 0x40 + }; + + /** + * 一个字节包含位的数量 8 + */ + private static final int BITS_OF_BYTE = 8; + + /** + * 多项式 + */ + private static final int POLYNOMIAL = 0x180D; + + /** + * 初始值 + */ + private static final int INITIAL_VALUE = 0xFFFF; + + // 测试 + public static void main(String[] args) { + // 序列号域 + byte[] serialNumber = BytesUtil.str2Bcd("3c40"); + + // 加密标志 + byte[] encryptFlag = BytesUtil.str2Bcd("00"); + + // 帧类型标志 + byte[] frameType = BytesUtil.str2Bcd("03"); + + // 消息体 + byte[] msgBody = BytesUtil.str2Bcd("880000000000270100"); + + byte[] data = Bytes.concat(serialNumber, encryptFlag, frameType, msgBody); + + String old_crc = String.format("%04x", CRC16Util.calcCrc16Old(data)); + String crc = String.format("%04x", CRC16Util.calcCrc16(data)); + + System.out.println("old_低位在前,高位在后:" + old_crc); + System.out.println("new_低位在前,高位在后:" + crc); + } + + /** + * @param bytes 编码内容 + * @return 编码结果 + * @function CRC16 编码 + */ + public static int crc16(int[] bytes) { + int res = INITIAL_VALUE; + for (int data : bytes) { + res = res ^ data; + for (int i = 0; i < BITS_OF_BYTE; i++) { + res = (res & 0x0001) == 1 ? (res >> 1) ^ POLYNOMIAL : res >> 1; + } + } + return revert(res); + } + + /** + * @param src 翻转数字 + * @return 翻转结果 + * @function 翻转16位的高八位和低八位字节,低位在前,高位在后 + */ + public static int revert(int src) { + int lowByte = (src & 0xFF00) >> 8; + int highByte = (src & 0x00FF) << 8; + return lowByte | highByte; + } + + /*以下方法得出的校验位:低位在前,高位在后*/ + + /** + * @param data 需要计算的数组 + * @return CRC16校验值 + * @function 计算CRC16校验 + */ + public static int calcCrc16(byte[] data) { + return calcCrc16(data, 0, data.length); + } + + /** + * @param data 需要计算的数组 + * @param offset 起始位置 + * @param len 长度 + * @return CRC16校验值 + * @function 计算CRC16校验 + */ + public static int calcCrc16(byte[] data, int offset, int len) { + return calcCrc16(data, offset, len, 0xffff); + } + + /** + * @param data 需要计算的数组 + * @param offset 起始位置 + * @param len 长度 + * @param preval 之前的校验值 + * @return CRC16校验值 + * @function 计算CRC16校验 + */ + public static int calcCrc16(byte[] data, int offset, int len, int preval) { + int ucCRCHi = (preval & 0xff00) >> 8; + int ucCRCLo = preval & 0x00ff; + int iIndex; + for (int i = 0; i < len; ++i) { + iIndex = (ucCRCLo ^ data[offset + i]) & 0x00ff; + ucCRCLo = ucCRCHi ^ crc16_tab_h[iIndex]; + ucCRCHi = crc16_tab_l[iIndex]; + } + return revert(((ucCRCHi & 0x00ff) << 8) | (ucCRCLo & 0x00ff) & 0xffff); + } + + /////////////////////////使用老协议中的低位码表//////////////////////////////////////////////////////// + + public static int calcCrc16Old(byte[] data) { + return calcCrc16Old(data, 0, data.length); + } + + public static int calcCrc16Old(byte[] data, int offset, int len) { + return calcCrc16Old(data, offset, len, 0xffff); + } + + public static int calcCrc16Old(byte[] data, int offset, int len, int preval) { + int ucCRCHi = (preval & 0xff00) >> 8; + int ucCRCLo = preval & 0x00ff; + int iIndex; + for (int i = 0; i < len; ++i) { + iIndex = (ucCRCLo ^ data[offset + i]) & 0x00ff; + ucCRCLo = ucCRCHi ^ crc16_tab_h[iIndex]; + ucCRCHi = crc16_tab_l_old[iIndex]; + } + return revert(((ucCRCHi & 0x00ff) << 8) | (ucCRCLo & 0x00ff) & 0xffff); + } + +} \ No newline at end of file diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/Cp56Time2a/Cp56Time2aUtil.java b/jsowell-common/src/main/java/com/jsowell/common/util/Cp56Time2a/Cp56Time2aUtil.java new file mode 100644 index 000000000..a8bad6f1e --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/Cp56Time2a/Cp56Time2aUtil.java @@ -0,0 +1,91 @@ +package com.jsowell.common.util.Cp56Time2a; + +import java.io.ByteArrayOutputStream; +import java.util.Calendar; +import java.util.Date; + +/** + * TODO + * + * @author JS-ZZA + * @date 2023/2/28 16:50 + */ +public class Cp56Time2aUtil { + + /** + * int 转换成 byte数组 + */ + public static byte[] intToByteArray(int i) { + byte[] result = new byte[4]; + result[0] = (byte) ((i >> 24) & 0xFF); + result[1] = (byte) ((i >> 16) & 0xFF); + result[2] = (byte) ((i >> 8) & 0xFF); + result[3] = (byte) (i & 0xFF); + return result; + } + + /** + * 日期转换成 CP56Time2a + * + * @param date Date类型日期 + * @return {@link Byte} + */ + public static byte[] date2Hbyte(Date date) { + ByteArrayOutputStream bOutput = new ByteArrayOutputStream(); + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + // 毫秒需要转换成两个字节其中 低位在前高位在后 + // 先转换成short + int millisecond = calendar.get(Calendar.SECOND) * 1000 + calendar.get(Calendar.MILLISECOND); + + // 默认的高位在前 + byte[] millisecondByte = intToByteArray(millisecond); + bOutput.write(millisecondByte[3]); + bOutput.write(millisecondByte[2]); + + // 分钟 只占6个比特位 需要把前两位置为零 + bOutput.write((byte) calendar.get(Calendar.MINUTE)); + // 小时需要把前三位置零 + bOutput.write((byte) calendar.get(Calendar.HOUR_OF_DAY)); + // 星期日的时候 week 是0 + int week = 0; + // if (week == Calendar.SUNDAY) { + // week = 7; + // } else { + // week--; + // } + // 前三个字节是 星期 因此需要将星期向左移5位 后五个字节是日期 需要将两个数字相加 相加之前需要先将前三位置零 + bOutput.write((byte) (week << 5) + (calendar.get(Calendar.DAY_OF_MONTH))); + // 前四字节置零 + bOutput.write((byte) ((byte) calendar.get(Calendar.MONTH) + 1)); + bOutput.write((byte) (calendar.get(Calendar.YEAR) - 2000)); + return bOutput.toByteArray(); + } + + /** + * CP56Time2a转换成 时间 + * + * @param dataByte 数据报文 + */ + public static Date byte2Hdate(byte[] dataByte) { + int year = (dataByte[6] & 0x7F) + 2000; + int month = dataByte[5] & 0x0F; + int day = dataByte[4] & 0x1F; + int hour = dataByte[3] & 0x1F; + int minute = dataByte[2] & 0x3F; + int second = dataByte[1] > 0 ? dataByte[1] : (dataByte[1] & 0xff); + int millisecond = dataByte[0] > 0 ? dataByte[0] : (dataByte[0] & 0xff); + millisecond = (second << 8) + millisecond; + second = millisecond / 1000; + millisecond = millisecond % 1000; + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.YEAR, year); + calendar.set(Calendar.MONTH, month); + calendar.set(Calendar.DAY_OF_MONTH, day); + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, minute); + calendar.set(Calendar.SECOND, second); + calendar.set(Calendar.MILLISECOND, millisecond); + return calendar.getTime(); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/DateUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/DateUtils.java new file mode 100644 index 000000000..b6631ead0 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/DateUtils.java @@ -0,0 +1,813 @@ +package com.jsowell.common.util; + +import com.google.common.collect.Lists; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.time.DateFormatUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.management.ManagementFactory; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Objects; + +/** + * 时间工具类 + * + * @author jsowell + */ +public class DateUtils extends org.apache.commons.lang3.time.DateUtils { + static Logger log = LoggerFactory.getLogger(DateUtils.class); + + public static String YYYY = "yyyy"; + + public static String YYYY_MM = "yyyy-MM"; + + public static String YYYY_MM_DD = "yyyy-MM-dd"; + + public static String YYYYMMDDHHMMSS = "yyyyMMddHHmmss"; + + public static String YYYYMMDDHHMM = "yyyyMMddHHmm"; + + public static String YYMMDDHHMMSS = "yyMMddHHmmss"; + + public static String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss"; + + public static String RFC3339 = "yyyy-MM-dd'T'HH:mm:ssXXX"; + + private static String[] parsePatterns = { + "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM", + "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM", + "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"}; + + /** + * 获取当前Date型日期 + * + * @return Date() 当前日期 + */ + public static Date getNowDate() { + return new Date(); + } + + /** + * 获取当前日期, 默认格式为yyyy-MM-dd + * + * @return String + */ + public static String getDate() { + return dateTimeNow(YYYY_MM_DD); + } + + public static String getTime() { + return dateTimeNow(YYYY_MM_DD_HH_MM_SS); + } + + public static String dateTimeNow() { + return dateTimeNow(YYYYMMDDHHMMSS); + } + + public static String dateTimeNow(final String format) { + return parseDateToStr(format, new Date()); + } + + public static String dateTime(final Date date) { + return parseDateToStr(YYYY_MM_DD, date); + } + + public static String parseDateToStr(final String format, final Date date) { + return new SimpleDateFormat(format).format(date); + } + + + public static String timeStampToRfc3339(long timeStamp) { + Date date = new Date(timeStamp); + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(RFC3339); + String formatDate = simpleDateFormat.format(date); + return formatDate; + } + + public static LocalDateTime toLocalDateTime(String str, String format) { + if (StringUtils.isBlank(str)) { + return null; + } + if (StringUtils.isBlank(format)) { + format = YYYY_MM_DD_HH_MM_SS; + } + DateTimeFormatter df = DateTimeFormatter.ofPattern(format); + return LocalDateTime.parse(str, df); + } + + public static Date dateTime(final String format, final String ts) { + try { + return new SimpleDateFormat(format).parse(ts); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + + /** + * 日期路径 即年/月/日 如2018/08/08 + */ + public static String datePath() { + Date now = new Date(); + return DateFormatUtils.format(now, "yyyy/MM/dd"); + } + + /** + * 日期路径 即年/月/日 如20180808 + */ + public static String dateTime() { + Date now = new Date(); + return DateFormatUtils.format(now, "yyyyMMdd"); + } + + /** + * 日期型字符串转化为日期 格式 + */ + public static Date parseDate(Object str) { + if (str == null) { + return null; + } + try { + return parseDate(str.toString(), parsePatterns); + } catch (ParseException e) { + return null; + } + } + + public static void main(String[] args) { + // String str = "2023-01-07 11:17:12"; + // Date date = parseDate(str); + // String str1 = parseDateToStr(YYYY_MM_DD_HH_MM_SS, date); + // System.out.println(str1); + // + // + // Date date1 = addMinute(new Date(), -15); + // String s = parseDateToStr(YYYY_MM_DD_HH_MM_SS, date1); + // System.out.println(s); + // + // String time = getDate(); + // System.out.println(time); + // + // System.out.println(dateTimeNow("yyyy-MM-dd HH:mm")); + + // String s2 = formatDateTime(new Date()); + // System.out.println(s2); + + String time = DateUtils.getTime(); + System.out.println(time); + + System.out.println(DateUtils.getDate()); + // String s = date2HexStr(new Date()); + // String s = BytesUtil.binary(bytes, 16); + // System.out.println(s); + + String hexString = "9401270a1b0217"; + byte[] bytes1 = BytesUtil.hexStringToByteArray(hexString); + // String s1 = toDateString(bytes1); + // Date date = CP56Time2aToDate(bytes1); + // System.out.println(s1); + + // String encodeCP56Time2a = DateUtils.encodeCP56Time2a(new Date()); + // byte[] bytes = BytesUtil.hexString2Bytes(encodeCP56Time2a); + // String s4 = CP56Time2aToDateStr(bytes); + // // byte[] msg = BytesUtil.str2Bcd("88000000000021" + encodeCP56Time2a); + // // Date date1 = toDate(bytes); + // // String s3 = formatDateTime(date1); + // // System.out.println(s3); + // System.out.println(s4); + long chargingTime = DateUtils.intervalTime("2023-02-24 16:00:00", "2023-02-24 17:03:06"); + System.out.println(chargingTime); + + } + + /** + * 获取服务器启动时间 + */ + public static Date getServerStartDate() { + long time = ManagementFactory.getRuntimeMXBean().getStartTime(); + return new Date(time); + } + + /** + * 计算相差天数 + */ + public static int differentDaysByMillisecond(Date date1, Date date2) { + return Math.abs((int) ((date2.getTime() - date1.getTime()) / (1000 * 3600 * 24))); + } + + /** + * 计算两个时间差 + */ + public static String getDatePoor(Date endDate, Date nowDate) { + long nd = 1000 * 24 * 60 * 60; + long nh = 1000 * 60 * 60; + long nm = 1000 * 60; + long ns = 1000; + // 获得两个时间的毫秒时间差异 + long diff = endDate.getTime() - nowDate.getTime(); + // 计算差多少天 + long day = diff / nd; + // 计算差多少小时 + long hour = diff % nd / nh; + // 计算差多少分钟 + long min = diff % nd % nh / nm; + // 计算差多少秒//输出结果 + long sec = diff % nd % nh % nm / ns; + + StringBuilder sb = new StringBuilder(); + if (day != 0) { + sb.append(day).append("天"); + } + if (hour != 0) { + sb.append(hour).append("小时"); + } + if (min != 0) { + sb.append(min).append("分"); + } + if (sec != 0) { + sb.append(sec).append("秒"); + } + return sb.toString(); + } + + /** + * LocalDateTime转Date + * + * @param localDateTime + * @return + */ + public static Date localDateTime2Date(LocalDateTime localDateTime) { + if (localDateTime == null) { + return null; + } + ZoneId zone = ZoneId.systemDefault(); + Instant instant = localDateTime.atZone(zone).toInstant(); + return Date.from(instant); + } + + public static Date localDate2Date(LocalDate localDate) { + if (localDate == null) { + return null; + } + LocalDateTime localDateTime = LocalDateTime.of(localDate, LocalTime.of(0, 0, 0)); + return localDateTime2Date(localDateTime); + } + + /** + * Date转LocalDateTime + */ + public static LocalDateTime date2LocalDateTime(Date date) { + if (date == null) { + return null; + } + return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); + } + + /** + * 获取云快充协议所需时间段 + * 0: 00~0: 30 时段费率号 + * 0: 30~1: 00 时段费率号 + * …… + * 23: 00~23: 30 时段费率号 + * 23: 30~0: 00 时段费率号 + */ + public static List getPeriodOfTime() { + int intervalMinutes = 30; + // 结果集 + List resultList = Lists.newArrayList(); + LocalTime startTime = LocalTime.of(0, 0); + + LocalTime tempStartDateTime = startTime; + LocalTime tempEndDateTime = null; + while (true) { + // 获取加intervalMinutes分钟后的时间 + tempEndDateTime = tempStartDateTime.plusMinutes(intervalMinutes); + resultList.add(tempStartDateTime.format(DateTimeFormatter.ofPattern("HH:mm:ss")) + + "-" + tempEndDateTime.format(DateTimeFormatter.ofPattern("HH:mm:ss"))); + // 下次的开始时间,为上次的结束时间 + tempStartDateTime = tempEndDateTime; + // 当tempStartDateTime等于startTime时,停止 + if (tempStartDateTime.compareTo(startTime) == 0) { + break; + } + } + // 特殊处理 + resultList = resultList.subList(0, resultList.size() - 1); + // 每天的最后半小时 + resultList.add("23:30:00-23:59:59"); + return resultList; + } + + /** + * 时分秒转LocalTime + * 可以是 10:10:23 或者 10:10 + * + * @param time + * @return + */ + public static LocalTime getLocalTime(String time) { + if (StringUtils.equals("24:00", time)) { + // System.out.println("time为24:00, 转换为23:59"); + time = "23:59"; + } + List list = Lists.newArrayList(time.split(":")); + if (CollectionUtils.isNotEmpty(list)) { + if (list.size() == 2) { + return LocalTime.of(Integer.parseInt(list.get(0)), Integer.parseInt(list.get(1))); + } else if (list.size() == 3) { + return LocalTime.of(Integer.parseInt(list.get(0)), Integer.parseInt(list.get(1)), Integer.parseInt(list.get(2))); + } + } + return null; + } + + public enum IntervalType { + DAY, + HOUR, + MINUTE, + SECOND, + ; + } + + /** + * 时间切割 + * + * @param startTime 被切割的开始时间 + * @param endTime 被切割的结束时间 + * @param intervalType + * @param interval >0 + * @return + */ + public static List splitDate(Date startTime, Date endTime, IntervalType intervalType, int interval) { + if (interval < 0) { + return null; + } + if (endTime.getTime() <= startTime.getTime()) { + return null; + } + + if (intervalType == IntervalType.DAY) { + return splitByDay(startTime, endTime, interval); + } + if (intervalType == IntervalType.HOUR) { + return splitByHour(startTime, endTime, interval); + } + if (intervalType == IntervalType.MINUTE) { + return splitByMinute(startTime, endTime, interval); + } + if (intervalType == IntervalType.SECOND) { + return splitBySecond(startTime, endTime, interval); + } + return null; + } + + /** + * 按照小时切割时间区间 + */ + public static List splitByHour(Date startTime, Date endTime, int intervalHours) { + if (endTime.getTime() <= startTime.getTime()) { + return null; + } + + List dateSplits = new ArrayList<>(256); + + DateSplit param = new DateSplit(); + param.setStartDateTime(startTime); + param.setEndDateTime(endTime); + param.setEndDateTime(addHour(startTime, intervalHours)); + while (true) { + param.setStartDateTime(startTime); + Date tempEndTime = addHour(startTime, intervalHours); + if (tempEndTime.getTime() >= endTime.getTime()) { + tempEndTime = endTime; + } + param.setEndDateTime(tempEndTime); + + dateSplits.add(new DateSplit(param.getStartDateTime(), param.getEndDateTime())); + + startTime = addHour(startTime, intervalHours); + if (startTime.getTime() >= endTime.getTime()) { + break; + } + if (param.getEndDateTime().getTime() >= endTime.getTime()) { + break; + } + } + return dateSplits; + } + + /** + * 按照秒切割时间区间 + * getRealTimeDetectionData + */ + public static List splitBySecond(Date startTime, Date endTime, int intervalSeconds) { + if (endTime.getTime() <= startTime.getTime()) { + return null; + } + List dateSplits = new ArrayList<>(256); + + DateSplit param = new DateSplit(); + param.setStartDateTime(startTime); + param.setEndDateTime(endTime); + param.setEndDateTime(addSecond(startTime, intervalSeconds)); + while (true) { + param.setStartDateTime(startTime); + Date tempEndTime = addSecond(startTime, intervalSeconds); + if (tempEndTime.getTime() >= endTime.getTime()) { + tempEndTime = endTime; + } + param.setEndDateTime(tempEndTime); + + dateSplits.add(new DateSplit(param.getStartDateTime(), param.getEndDateTime())); + + startTime = addSecond(startTime, intervalSeconds); + if (startTime.getTime() >= endTime.getTime()) { + break; + } + if (param.getEndDateTime().getTime() >= endTime.getTime()) { + break; + } + } + return dateSplits; + } + + /** + * 按照天切割时间区间 + */ + public static List splitByDay(Date startTime, Date endTime, int intervalDays) { + if (endTime.getTime() <= startTime.getTime()) { + return null; + } + List dateSplits = new ArrayList<>(256); + + DateSplit param = new DateSplit(); + param.setStartDateTime(startTime); + param.setEndDateTime(endTime); + param.setEndDateTime(addDay(startTime, intervalDays)); + while (true) { + param.setStartDateTime(startTime); + Date tempEndTime = addDay(startTime, intervalDays); + if (tempEndTime.getTime() >= endTime.getTime()) { + tempEndTime = endTime; + } + param.setEndDateTime(tempEndTime); + + dateSplits.add(new DateSplit(param.getStartDateTime(), param.getEndDateTime())); + + startTime = addDay(startTime, intervalDays); + if (startTime.getTime() >= endTime.getTime()) { + break; + } + if (param.getEndDateTime().getTime() >= endTime.getTime()) { + break; + } + } + return dateSplits; + } + + + /** + * 按照分钟切割时间区间 + * + * @param startTime + * @param endTime + * @param intervalMinutes + * @return + */ + public static List splitByMinute(Date startTime, Date endTime, int intervalMinutes) { + if (endTime.getTime() <= startTime.getTime()) { + return null; + } + List dateSplits = new ArrayList<>(256); + + DateSplit param = new DateSplit(); + param.setStartDateTime(startTime); + param.setEndDateTime(endTime); + param.setEndDateTime(addMinute(startTime, intervalMinutes)); + while (true) { + param.setStartDateTime(startTime); + Date tempEndTime = addMinute(startTime, intervalMinutes); + if (tempEndTime.getTime() >= endTime.getTime()) { + tempEndTime = endTime; + } + param.setEndDateTime(tempEndTime); + + dateSplits.add(new DateSplit(param.getStartDateTime(), param.getEndDateTime())); + + startTime = addMinute(startTime, intervalMinutes); + if (startTime.getTime() >= endTime.getTime()) { + break; + } + if (param.getEndDateTime().getTime() >= endTime.getTime()) { + break; + } + } + return dateSplits; + } + + private static Date addDay(Date date, int days) { + return add(date, Calendar.DAY_OF_MONTH, days); + } + + private static Date addHour(Date date, int hours) { + return add(date, Calendar.HOUR_OF_DAY, hours); + } + + public static Date addMinute(Date date, int minute) { + return add(date, Calendar.MINUTE, minute); + } + + private static Date addSecond(Date date, int second) { + return add(date, Calendar.SECOND, second); + } + + private static Date add(final Date date, final int calendarField, final int amount) { + final Calendar c = Calendar.getInstance(); + c.setTime(date); + c.add(calendarField, amount); + return c.getTime(); + } + + public static String formatDateTime(Date date) { + if (date == null) { + return ""; + } + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(YYYY_MM_DD_HH_MM_SS); + return simpleDateFormat.format(date); + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class DateSplit { + private Date startDateTime; + private Date endDateTime; + + public String getStartDateTimeStr() { + return formatDateTime(startDateTime); + } + + public String getEndDateTimeStr() { + return formatDateTime(endDateTime); + } + } + + /** + * CP56Time2a 转 日期字符串 + * 0x3b交易记录 会使用这个方法,解析开始时间 结束时间 交易时间 + * @param bytes + * @return + */ + // public static String CP56Time2aToDateStr(byte[] bytes) { + // Date date = CP56Time2aToDate(bytes); + // if (date == null) { + // return null; + // } + // return DateUtils.formatDateTime(date); + // } + // public static String toDateString(byte[] bytes) { + // return Cp56Time2aUtil.toDateString(bytes); + // } + + /** + * cp56time2a 格式转date格式 + */ + // public static Date CP56Time2aToDate(byte[] bytes) { + // if (bytes == null || bytes.length != 7) { + // return null; + // } + // int ms = BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(bytes, 0, 2)); + // int min = BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(bytes, 2, 1)); + // int hour = BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(bytes, 3, 1)); + // int day = BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(bytes, 4, 1)); + // int month = BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(bytes, 5, 1)); + // int year = BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(bytes, 6, 1)); + // if (month == 0 || day == 0 || year == 0) { + // return null; + // } + // LocalDateTime localDateTime = LocalDateTime.of(year + 2000, month, day, hour, min, ms / 1000); + // Date date = DateUtils.localDateTime2Date(localDateTime); + // return date; + // } + + // public static Date CP56Time2aToDate(byte[] bytes) { + // return Cp56Time2aUtil.toDate(bytes); + // } + + /** + * 时间转16进制字符串 + * Date转成CP56Time2a + * 发送对时请求,会用到这个方法 + */ + // public static String encodeCP56Time2a(Date date) { + // // Calendar calendar = Calendar.getInstance(); + // // calendar.setTime(date); + // // StringBuilder builder = new StringBuilder(); + // // String milliSecond = String.format("%04X", (calendar.get(Calendar.SECOND) * 1000) + calendar.get(Calendar.MILLISECOND)); + // // builder.append(milliSecond.substring(2, 4)); + // // builder.append(milliSecond.substring(0, 2)); + // // builder.append(String.format("%02X", calendar.get(Calendar.MINUTE) & 0x3F)); + // // builder.append(String.format("%02X", calendar.get(Calendar.HOUR_OF_DAY) & 0x1F)); + // // int week = calendar.get(Calendar.DAY_OF_WEEK); + // // if (week == Calendar.SUNDAY) + // // week = 7; + // // else week--; + // // builder.append(String.format("%02X", (week << 5) + (calendar.get(Calendar.DAY_OF_MONTH) & 0x1F))); + // // builder.append(String.format("%02X", calendar.get(Calendar.MONTH) + 1)); + // // builder.append(String.format("%02X", calendar.get(Calendar.YEAR) - 2000)); + // // return builder.toString(); + // + // byte[] result = new byte[7]; + // final Calendar aTime = Calendar.getInstance(); + // aTime.setTime(date); + // final int milliseconds = aTime.get(Calendar.MILLISECOND); + // result[0] = (byte) (milliseconds % 256); + // result[1] = (byte) (milliseconds / 256); + // result[2] = (byte) aTime.get(Calendar.MINUTE); + // result[3] = (byte) aTime.get(Calendar.HOUR_OF_DAY); + // result[4] = (byte) aTime.get(Calendar.DAY_OF_MONTH); + // result[5] = (byte) (aTime.get(Calendar.MONTH) + 1); + // result[6] = (byte) (aTime.get(Calendar.YEAR) % 100); + // System.out.println("Year->" + aTime.get(Calendar.YEAR)); + // return BytesUtil.binary(result, 16); + // } + // public static String date2HexStr(Date date) { + // return Cp56Time2aUtil.date2HexStr(date); + // } + + + /** + * 获取两个时间的间隔时间 + * + * @return 间隔时间 单位:分钟 + */ + public static long intervalTime(String begin, String end) { + return intervalTime(parseDate(begin), parseDate(end)); + } + + /** + * 获取两个时间的间隔时间 + * + * @return 间隔时间 单位:分钟 + */ + public static long intervalTime(Date begin, Date end) { + return intervalTime(date2LocalDateTime(begin), date2LocalDateTime(end)); + } + + /** + * 获取两个时间的间隔时间 + * + * @return 间隔时间 单位:分钟 + */ + public static long intervalTime(LocalDateTime begin, LocalDateTime end) { + return ChronoUnit.MINUTES.between(begin, end); + } + + /** + * 判断2个时间段是否有重叠(交集) + * + * @param startDate1 时间段1开始时间戳 + * @param endDate1 时间段1结束时间戳 + * @param startDate2 时间段2开始时间戳 + * @param endDate2 时间段2结束时间戳 + * @param isStrict 是否严格重叠,true 严格,没有任何相交或相等;false 不严格,可以首尾相等,比如2021/5/29-2021/5/31和2021/5/31-2021/6/1,不重叠。 + * @return 返回是否重叠 + */ + public static boolean isOverlap(long startDate1, long endDate1, long startDate2, long endDate2, boolean isStrict) { + if (isStrict) { + if (!(endDate1 < startDate2 || startDate1 > endDate2)) { + return true; + } + } else { + if (!(endDate1 <= startDate2 || startDate1 >= endDate2)) { + return true; + } + } + return false; + } + + /** + * @param time1 17:02:00 - 17:02:00 + * @param time2 00:00-06:30 + * @return + */ + public static boolean checkTime(String time1, String time2) { + String[] split = time1.split("-"); + LocalTime startTime1 = DateUtils.getLocalTime(split[0]); + LocalTime endTime1 = DateUtils.getLocalTime(split[1]); + String[] split2 = time2.split("-"); + LocalTime startTime2 = DateUtils.getLocalTime(split2[0]); + LocalTime endTime2 = DateUtils.getLocalTime(split2[1]); + return DateUtils.isOverlap(startTime1, endTime1, startTime2, endTime2, false); + } + + /** + * 判断时间段是否重叠 + * + * @param startDate1 + * @param endDate1 + * @param startDate2 + * @param endDate2 + * @param isStrict + * @return + */ + public static boolean isOverlap(LocalTime startDate1, LocalTime endDate1, LocalTime startDate2, LocalTime endDate2, boolean isStrict) { + if (startDate1 == null || endDate1 == null || startDate2 == null || endDate2 == null) { + log.warn("判断时间段是否重叠缺少参数,返回false"); + return false; + } + boolean result = false; + if (isStrict) { + if (!(endDate1.isBefore(startDate2) || startDate1.isAfter(endDate2))) { + result = true; + } + } else { + if (!((endDate1.isBefore(startDate2) || endDate1.equals(startDate2)) || (startDate1.isAfter(endDate2) || startDate1.equals(endDate2)))) { + result = true; + } + } + // log.info("时间段1={}, 时间段2={}, 结果:{}", startDate1 + "-" + endDate1, startDate2 + "-" + endDate2, result); + return result; + } + + /** + * 判断2个时间段是否有重叠(交集) + * + * @param startDate1 时间段1开始时间 + * @param endDate1 时间段1结束时间 + * @param startDate2 时间段2开始时间 + * @param endDate2 时间段2结束时间 + * @param isStrict 是否严格重叠,true 严格,没有任何相交或相等;false 不严格,可以首尾相等,比如2021-05-29到2021-05-31和2021-05-31到2021-06-01,不重叠。 + * @return 返回是否重叠 + */ + public static boolean isOverlap(Date startDate1, Date endDate1, Date startDate2, Date endDate2, boolean isStrict) { + Objects.requireNonNull(startDate1, "startDate1"); + Objects.requireNonNull(endDate1, "endDate1"); + Objects.requireNonNull(startDate2, "startDate2"); + Objects.requireNonNull(endDate2, "endDate2"); + return isOverlap(startDate1.getTime(), endDate1.getTime(), startDate2.getTime(), endDate2.getTime(), isStrict); + } + + /** + * 秒 转 天时分秒 + * + * @param mss 秒数 + * @return xx天xx小时xx分钟xx秒 + */ + public static String formatDateTime(long mss) { + String DateTimes = null; + long days = mss / (60 * 60 * 24); + long hours = (mss % (60 * 60 * 24)) / (60 * 60); + long minutes = (mss % (60 * 60)) / 60; + long seconds = mss % 60; + if (days > 0) { + DateTimes = days + "天" + hours + "小时" + minutes + "分钟" + + seconds + "秒"; + } else if (hours > 0) { + DateTimes = hours + "小时" + minutes + "分钟" + + seconds + "秒"; + } else if (minutes > 0) { + DateTimes = minutes + "分钟" + + seconds + "秒"; + } else { + DateTimes = seconds + "秒"; + } + + return DateTimes; + } + + /** + * 当前时间是否在时间指定范围内
+ * + * @param time 被检查的时间 + * @param beginTime 起始时间 + * @param endTime 结束时间 + * @return 是否在范围内 + * @since 3.0.8 + */ + public static boolean isIn(LocalTime time, LocalTime beginTime, LocalTime endTime) { + // 判断是否在范围内 包含开始结束时间 + if (time.compareTo(beginTime) == 0 || time.compareTo(endTime) == 0) { + return true; + } + if (beginTime.isBefore(time) && endTime.isAfter(time)){ + return true; + } + return false; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/DictUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/DictUtils.java new file mode 100644 index 000000000..f00518ec5 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/DictUtils.java @@ -0,0 +1,159 @@ +package com.jsowell.common.util; + +import com.alibaba.fastjson2.JSONArray; +import com.jsowell.common.constant.CacheConstants; +import com.jsowell.common.core.domain.entity.SysDictData; +import com.jsowell.common.core.redis.RedisCache; +import com.jsowell.common.util.spring.SpringUtils; + +import java.util.Collection; +import java.util.List; + +/** + * 字典工具类 + * + * @author jsowell + */ +public class DictUtils { + /** + * 分隔符 + */ + public static final String SEPARATOR = ","; + + /** + * 设置字典缓存 + * + * @param key 参数键 + * @param dictDatas 字典数据列表 + */ + public static void setDictCache(String key, List dictDatas) { + SpringUtils.getBean(RedisCache.class).setCacheObject(getCacheKey(key), dictDatas); + } + + /** + * 获取字典缓存 + * + * @param key 参数键 + * @return dictDatas 字典数据列表 + */ + public static List getDictCache(String key) { + JSONArray arrayCache = SpringUtils.getBean(RedisCache.class).getCacheObject(getCacheKey(key)); + if (StringUtils.isNotNull(arrayCache)) { + return arrayCache.toList(SysDictData.class); + } + return null; + } + + /** + * 根据字典类型和字典值获取字典标签 + * + * @param dictType 字典类型 + * @param dictValue 字典值 + * @return 字典标签 + */ + public static String getDictLabel(String dictType, String dictValue) { + return getDictLabel(dictType, dictValue, SEPARATOR); + } + + /** + * 根据字典类型和字典标签获取字典值 + * + * @param dictType 字典类型 + * @param dictLabel 字典标签 + * @return 字典值 + */ + public static String getDictValue(String dictType, String dictLabel) { + return getDictValue(dictType, dictLabel, SEPARATOR); + } + + /** + * 根据字典类型和字典值获取字典标签 + * + * @param dictType 字典类型 + * @param dictValue 字典值 + * @param separator 分隔符 + * @return 字典标签 + */ + public static String getDictLabel(String dictType, String dictValue, String separator) { + StringBuilder propertyString = new StringBuilder(); + List datas = getDictCache(dictType); + + if (StringUtils.isNotNull(datas)) { + if (StringUtils.containsAny(separator, dictValue)) { + for (SysDictData dict : datas) { + for (String value : dictValue.split(separator)) { + if (value.equals(dict.getDictValue())) { + propertyString.append(dict.getDictLabel()).append(separator); + break; + } + } + } + } else { + for (SysDictData dict : datas) { + if (dictValue.equals(dict.getDictValue())) { + return dict.getDictLabel(); + } + } + } + } + return StringUtils.stripEnd(propertyString.toString(), separator); + } + + /** + * 根据字典类型和字典标签获取字典值 + * + * @param dictType 字典类型 + * @param dictLabel 字典标签 + * @param separator 分隔符 + * @return 字典值 + */ + public static String getDictValue(String dictType, String dictLabel, String separator) { + StringBuilder propertyString = new StringBuilder(); + List datas = getDictCache(dictType); + + if (StringUtils.containsAny(separator, dictLabel) && StringUtils.isNotEmpty(datas)) { + for (SysDictData dict : datas) { + for (String label : dictLabel.split(separator)) { + if (label.equals(dict.getDictLabel())) { + propertyString.append(dict.getDictValue()).append(separator); + break; + } + } + } + } else { + for (SysDictData dict : datas) { + if (dictLabel.equals(dict.getDictLabel())) { + return dict.getDictValue(); + } + } + } + return StringUtils.stripEnd(propertyString.toString(), separator); + } + + /** + * 删除指定字典缓存 + * + * @param key 字典键 + */ + public static void removeDictCache(String key) { + SpringUtils.getBean(RedisCache.class).deleteObject(getCacheKey(key)); + } + + /** + * 清空字典缓存 + */ + public static void clearDictCache() { + Collection keys = SpringUtils.getBean(RedisCache.class).keys(CacheConstants.SYS_DICT_KEY + "*"); + SpringUtils.getBean(RedisCache.class).deleteObject(keys); + } + + /** + * 设置cache key + * + * @param configKey 参数键 + * @return 缓存键key + */ + public static String getCacheKey(String configKey) { + return CacheConstants.SYS_DICT_KEY + configKey; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/DistanceUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/DistanceUtils.java new file mode 100644 index 000000000..a41a700d2 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/DistanceUtils.java @@ -0,0 +1,42 @@ +package com.jsowell.common.util; + +public final class DistanceUtils { + + /** + * 地球半径,单位 km + */ + private static final double EARTH_RADIUS = 6378.137; + + /** + * 根据经纬度,计算两点间的距离 + * + * @param longitude1 第一个点的经度 + * @param latitude1 第一个点的纬度 + * @param longitude2 第二个点的经度 + * @param latitude2 第二个点的纬度 + * @return 返回距离 单位千米 + */ + public static double getDistance(double longitude1, double latitude1, double longitude2, double latitude2) { + // 纬度 + double lat1 = Math.toRadians(latitude1); + double lat2 = Math.toRadians(latitude2); + // 经度 + double lng1 = Math.toRadians(longitude1); + double lng2 = Math.toRadians(longitude2); + // 纬度之差 + double a = lat1 - lat2; + // 经度之差 + double b = lng1 - lng2; + // 计算两点距离的公式 + double s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + + Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(b / 2), 2))); + // 弧长乘地球半径, 返回单位: 千米 + s = s * EARTH_RADIUS; + return s; + } + + public static void main(String[] args) { + double d = getDistance(116.308479, 39.983171, 116.353454, 39.996059); + System.out.println(d); + } +} \ No newline at end of file diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/ExceptionUtil.java b/jsowell-common/src/main/java/com/jsowell/common/util/ExceptionUtil.java new file mode 100644 index 000000000..5433bf2d6 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/ExceptionUtil.java @@ -0,0 +1,33 @@ +package com.jsowell.common.util; + +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * 错误信息处理类。 + * + * @author jsowell + */ +public class ExceptionUtil { + /** + * 获取exception的详细错误信息。 + */ + public static String getExceptionMessage(Throwable e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw, true)); + return sw.toString(); + } + + public static String getRootErrorMessage(Exception e) { + Throwable root = org.apache.commons.lang3.exception.ExceptionUtils.getRootCause(e); + root = (root == null ? e : root); + if (root == null) { + return ""; + } + String msg = root.getMessage(); + if (msg == null) { + return "null"; + } + return StringUtils.defaultString(msg); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/JWTUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/JWTUtils.java new file mode 100644 index 000000000..8a4c33f10 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/JWTUtils.java @@ -0,0 +1,135 @@ +package com.jsowell.common.util; + +import com.jsowell.common.constant.Constants; +import com.jsowell.common.enums.ykc.ReturnCodeEnum; +import com.jsowell.common.exception.BusinessException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.spec.SecretKeySpec; +import javax.xml.bind.DatatypeConverter; +import java.security.Key; +import java.util.Date; + +@Component +public class JWTUtils { + // 令牌自定义标识 + private static String header; + + @Value("${token.header}") + public void setHeader(String header) { + JWTUtils.header = header; + } + + // 令牌秘钥 + private static String secret; + + @Value("${token.secret}") + public void setSecret(String secret) { + JWTUtils.secret = secret; + } + + // 接口服务 令牌有效期 + private static int serviceExpireTime; + + @Value("${token.serviceExpireTime}") + public void setServiceExpireTime(int serviceExpireTime) { + JWTUtils.serviceExpireTime = serviceExpireTime; + } + + protected static final long MILLIS_SECOND = 1000; + + protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND; + + private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L; + + /** + * 生成Jwt的方法 + * + * @param id 用户ID + * @param subject 用户昵称 + * @param ttlMillis 过期时间 毫秒 + * @return Token String 凭证 + */ + private static String createToken(String id, String subject, long ttlMillis) { + // 签名方法 HS256 + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; + + // 生成Jwt的时间 + long nowMillis = System.currentTimeMillis(); + Date now = new Date(nowMillis); + + // 生成秘钥 + byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(secret); + Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName()); + + // 设置JWT所存储的信息 + JwtBuilder builder = Jwts.builder().setId(id).setIssuedAt(now).setSubject(subject).signWith(signatureAlgorithm, signingKey); + + //builder.claim("name", "value"); //存储自定义信息 + + // 设置过期时间 + if (ttlMillis >= 0) { + long expMillis = nowMillis + ttlMillis * MILLIS_MINUTE; + Date exp = new Date(expMillis); + builder.setExpiration(exp); + } + + // 构建JWT并将其序列化为紧凑的URL安全字符串 + return builder.compact(); + } + + /** + * 从令牌中获取数据声明 + * + * @param token 令牌 + * @return 数据声明 + */ + private static Claims parseToken(String token) { + return Jwts.parser() + .setSigningKey(secret) + .parseClaimsJws(token) + .getBody(); + } + + /** + * 获取会员token + * + * @return + */ + public static String createMemberToken(String memberId, String nickName) { + return createToken(memberId, nickName, serviceExpireTime); + } + + /** + * 获取会员id + * + * @param memberToken + * @return + */ + public static String getMemberId(String memberToken) { + memberToken = getToken(memberToken); + if (StringUtils.isBlank(memberToken)) { + throw new BusinessException(ReturnCodeEnum.CODE_TOKEN_ERROR); + } + Claims claims = parseToken(memberToken); + return claims.getId(); + } + + /** + * 替换Bearer + * + * @param memberToken 会员token + * @return + */ + private static String getToken(String memberToken) { + if (StringUtils.isNotEmpty(memberToken) && memberToken.startsWith(Constants.TOKEN_PREFIX)) { + memberToken = memberToken.replace(Constants.TOKEN_PREFIX, "").trim(); + } + return memberToken; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/LogUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/LogUtils.java new file mode 100644 index 000000000..7d7a0f0c2 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/LogUtils.java @@ -0,0 +1,15 @@ +package com.jsowell.common.util; + +/** + * 处理并记录日志文件 + * + * @author jsowell + */ +public class LogUtils { + public static String getBlock(Object msg) { + if (msg == null) { + msg = ""; + } + return "[" + msg.toString() + "]"; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/MessageUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/MessageUtils.java new file mode 100644 index 000000000..709bf7d07 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/MessageUtils.java @@ -0,0 +1,24 @@ +package com.jsowell.common.util; + +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import com.jsowell.common.util.spring.SpringUtils; + +/** + * 获取i18n资源文件 + * + * @author jsowell + */ +public class MessageUtils { + /** + * 根据消息键和参数 获取消息 委托给spring messageSource + * + * @param code 消息键 + * @param args 参数 + * @return 获取国际化翻译值 + */ + public static String message(String code, Object... args) { + MessageSource messageSource = SpringUtils.getBean(MessageSource.class); + return messageSource.getMessage(code, args, LocaleContextHolder.getLocale()); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/PageUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/PageUtils.java new file mode 100644 index 000000000..aa681c08a --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/PageUtils.java @@ -0,0 +1,32 @@ +package com.jsowell.common.util; + +import com.github.pagehelper.PageHelper; +import com.jsowell.common.core.page.PageDomain; +import com.jsowell.common.core.page.TableSupport; +import com.jsowell.common.util.sql.SqlUtil; + +/** + * 分页工具类 + * + * @author jsowell + */ +public class PageUtils extends PageHelper { + /** + * 设置请求分页数据 + */ + public static void startPage() { + PageDomain pageDomain = TableSupport.buildPageRequest(); + Integer pageNum = pageDomain.getPageNum(); + Integer pageSize = pageDomain.getPageSize(); + String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy()); + Boolean reasonable = pageDomain.getReasonable(); + PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable); + } + + /** + * 清理分页的线程变量 + */ + public static void clearPage() { + PageHelper.clearPage(); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/RandomUtil.java b/jsowell-common/src/main/java/com/jsowell/common/util/RandomUtil.java new file mode 100644 index 000000000..4c7a56ab4 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/RandomUtil.java @@ -0,0 +1,84 @@ +package com.jsowell.common.util; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; + +import java.security.SecureRandom; +import java.util.List; +import java.util.Random; +import java.util.Set; + +/** + * 获取随机数的工具类 + */ +public class RandomUtil { + private static final String SYMBOLS_NUM = "0123456789"; // 纯数字 + //如果需加入字母就改成0123456789abcdefg........... + private static final String SYMBOLS_NUM_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789"; + + private static final Random RANDOM = new SecureRandom(); + + /** + * 获取6位的短信验证码 纯数字 + * + * @return 随机数字 + */ + public static String getSMSVerificationCode() { + return getRandomNumber(6); + } + + /** + * 获取指定长度随机数 + * + * @param length 需要的长度 + * @return 随机数 + */ + public static String getRandomNumber(int length) { + char[] nonceChars = new char[length]; //指定长度,自己可以设置 + for (int index = 0; index < nonceChars.length; ++index) { + nonceChars[index] = SYMBOLS_NUM.charAt(RANDOM.nextInt(SYMBOLS_NUM.length())); + } + return new String(nonceChars); + } + + /** + * 获取一组不重复的6位数随机数 + * + * @param num 数量 + * @return + */ + public static List getRandomNumberList(int num) { + List list = Lists.newArrayList(); + for (int i = 0; i < num; i++) { + String code = getNotRepeatCode(list); + // System.out.println(code); + list.add(code); + } + return list; + } + + + /** + * 获取一组不重复的6位数随机数 + */ + private static String getNotRepeatCode(List list) { + // 获取随机验证码 + String code = RandomUtil.getSMSVerificationCode(); + // 判断list中是否存在 + boolean contains = list.contains(code); + if (contains) { + // 已经存在,重新获取一个 + code = getNotRepeatCode(list); + } + return code; + } + + + public static void main(String[] args) { + List randomNumberList = getRandomNumberList(100000); + Set set = Sets.newHashSet(randomNumberList); + System.out.println("list长度:" + randomNumberList.size()); + System.out.println("set长度:" + set.size()); + } + +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/SMSUtil.java b/jsowell-common/src/main/java/com/jsowell/common/util/SMSUtil.java new file mode 100644 index 000000000..ec3dc4d9e --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/SMSUtil.java @@ -0,0 +1,68 @@ +package com.jsowell.common.util; + +import com.github.qcloudsms.SmsSingleSender; +import com.github.qcloudsms.SmsSingleSenderResult; +import com.github.qcloudsms.httpclient.HTTPException; +import com.jsowell.common.constant.CacheConstants; +import com.jsowell.common.core.redis.RedisCache; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * 发送短信验证码工具类 + */ +@Component +public class SMSUtil { + + private static RedisCache redisCache; + + @Autowired + public void setRedisCache(RedisCache redisCache) { + SMSUtil.redisCache = redisCache; + } + + + // 短信应用SDK AppKey + private static final String APP_KEY = "8ebcf52de98416814b440891350cd594"; + + // 短信模板ID,需要在短信应用中申请 + private static final int TEMPLATE_ID = 1002460; + + // 短信应用SDK AppID 1400开头 + private static final int APP_ID = 1400536771; + + // 签名,使用的是签名内容,而不是签名ID + private static final String SMS_SIGN = "举视新能源"; + + // 国家码 如 86 为中国 + private static final String NATION_CODE = "86"; + + public static String sendSMS(HttpServletRequest request, String phoneNumber) throws HTTPException, IOException { + String reStr = "success"; //定义返回值 + //随机生成六位验证码 + String code = RandomUtil.getSMSVerificationCode(); + //参数,一定要对应短信模板中的参数顺序和个数, + String[] params = {code}; + //创建ssender对象 + SmsSingleSender ssender = new SmsSingleSender(APP_ID, APP_KEY); + //发送 + SmsSingleSenderResult result = ssender.sendWithParam(NATION_CODE, phoneNumber, TEMPLATE_ID, params, SMS_SIGN, "", ""); + if (result.result != 0) { + reStr = "error"; + } else { + // 改为保存redis + String redisKey = CacheConstants.SMS_VERIFICATION_CODE_KEY + phoneNumber; + redisCache.setCacheObject(redisKey, code, CacheConstants.cache_expire_time_10m); + } + return reStr; + } + + + public static void main(String[] args) { + System.out.println(); + } + +} \ No newline at end of file diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/SecurityUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/SecurityUtils.java new file mode 100644 index 000000000..a63c8cfd5 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/SecurityUtils.java @@ -0,0 +1,99 @@ +package com.jsowell.common.util; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import com.jsowell.common.constant.HttpStatus; +import com.jsowell.common.core.domain.model.LoginUser; +import com.jsowell.common.exception.ServiceException; + +/** + * 安全服务工具类 + * + * @author jsowell + */ +public class SecurityUtils { + /** + * 用户ID + **/ + public static Long getUserId() { + try { + return getLoginUser().getUserId(); + } catch (Exception e) { + throw new ServiceException("获取用户ID异常", HttpStatus.UNAUTHORIZED); + } + } + + /** + * 获取部门ID + **/ + public static Long getDeptId() { + try { + return getLoginUser().getDeptId(); + } catch (Exception e) { + throw new ServiceException("获取部门ID异常", HttpStatus.UNAUTHORIZED); + } + } + + /** + * 获取用户账户 + **/ + public static String getUsername() { + try { + return getLoginUser().getUsername(); + } catch (Exception e) { + throw new ServiceException("获取用户账户异常", HttpStatus.UNAUTHORIZED); + } + } + + /** + * 获取用户 + **/ + public static LoginUser getLoginUser() { + try { + return (LoginUser) getAuthentication().getPrincipal(); + } catch (Exception e) { + throw new ServiceException("获取用户信息异常", HttpStatus.UNAUTHORIZED); + } + } + + /** + * 获取Authentication + */ + public static Authentication getAuthentication() { + return SecurityContextHolder.getContext().getAuthentication(); + } + + /** + * 生成BCryptPasswordEncoder密码 + * + * @param password 密码 + * @return 加密字符串 + */ + public static String encryptPassword(String password) { + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + return passwordEncoder.encode(password); + } + + /** + * 判断密码是否相同 + * + * @param rawPassword 真实密码 + * @param encodedPassword 加密后字符 + * @return 结果 + */ + public static boolean matchesPassword(String rawPassword, String encodedPassword) { + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + return passwordEncoder.matches(rawPassword, encodedPassword); + } + + /** + * 是否为管理员 + * + * @param userId 用户ID + * @return 结果 + */ + public static boolean isAdmin(Long userId) { + return userId != null && 1L == userId; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/ServletUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/ServletUtils.java new file mode 100644 index 000000000..2b0e7ba6f --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/ServletUtils.java @@ -0,0 +1,160 @@ +package com.jsowell.common.util; + +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.text.Convert; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; + +/** + * 客户端工具类 + * + * @author jsowell + */ +public class ServletUtils { + /** + * 获取String参数 + */ + public static String getParameter(String name) { + return getRequest().getParameter(name); + } + + /** + * 获取String参数 + */ + public static String getParameter(String name, String defaultValue) { + return Convert.toStr(getRequest().getParameter(name), defaultValue); + } + + /** + * 获取Integer参数 + */ + public static Integer getParameterToInt(String name) { + return Convert.toInt(getRequest().getParameter(name)); + } + + /** + * 获取Integer参数 + */ + public static Integer getParameterToInt(String name, Integer defaultValue) { + return Convert.toInt(getRequest().getParameter(name), defaultValue); + } + + /** + * 获取Boolean参数 + */ + public static Boolean getParameterToBool(String name) { + return Convert.toBool(getRequest().getParameter(name)); + } + + /** + * 获取Boolean参数 + */ + public static Boolean getParameterToBool(String name, Boolean defaultValue) { + return Convert.toBool(getRequest().getParameter(name), defaultValue); + } + + /** + * 获取request + */ + public static HttpServletRequest getRequest() { + return getRequestAttributes().getRequest(); + } + + /** + * 获取response + */ + public static HttpServletResponse getResponse() { + return getRequestAttributes().getResponse(); + } + + /** + * 获取session + */ + public static HttpSession getSession() { + return getRequest().getSession(); + } + + public static ServletRequestAttributes getRequestAttributes() { + RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); + return (ServletRequestAttributes) attributes; + } + + /** + * 将字符串渲染到客户端 + * + * @param response 渲染对象 + * @param string 待渲染的字符串 + */ + public static void renderString(HttpServletResponse response, String string) { + try { + response.setStatus(200); + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + response.getWriter().print(string); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * 是否是Ajax异步请求 + * + * @param request + */ + public static boolean isAjaxRequest(HttpServletRequest request) { + String accept = request.getHeader("accept"); + if (accept != null && accept.contains("application/json")) { + return true; + } + + String xRequestedWith = request.getHeader("X-Requested-With"); + if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest")) { + return true; + } + + String uri = request.getRequestURI(); + if (StringUtils.inStringIgnoreCase(uri, ".json", ".xml")) { + return true; + } + + String ajax = request.getParameter("__ajax"); + return StringUtils.inStringIgnoreCase(ajax, "json", "xml"); + } + + /** + * 内容编码 + * + * @param str 内容 + * @return 编码后的内容 + */ + public static String urlEncode(String str) { + try { + return URLEncoder.encode(str, Constants.UTF8); + } catch (UnsupportedEncodingException e) { + return StringUtils.EMPTY; + } + } + + /** + * 内容解码 + * + * @param str 内容 + * @return 解码后的内容 + */ + public static String urlDecode(String str) { + try { + return URLDecoder.decode(str, Constants.UTF8); + } catch (UnsupportedEncodingException e) { + return StringUtils.EMPTY; + } + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/StringUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/StringUtils.java new file mode 100644 index 000000000..bd10649d7 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/StringUtils.java @@ -0,0 +1,561 @@ +package com.jsowell.common.util; + +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.text.StrFormatter; +import org.springframework.util.AntPathMatcher; + +import java.util.*; + +/** + * 字符串工具类 + * + * @author jsowell + */ +public class StringUtils extends org.apache.commons.lang3.StringUtils { + /** + * 空字符串 + */ + private static final String NULL_STR = ""; + + /** + * 下划线 + */ + private static final char SEPARATOR = '_'; + + /** + * 获取参数不为空值 + * + * @param value defaultValue 要判断的value + * @return value 返回值 + */ + public static T nvl(T value, T defaultValue) { + return value != null ? value : defaultValue; + } + + /** + * * 判断一个Collection是否为空, 包含List,Set,Queue + * + * @param coll 要判断的Collection + * @return true:为空 false:非空 + */ + public static boolean isEmpty(Collection coll) { + return isNull(coll) || coll.isEmpty(); + } + + /** + * * 判断一个Collection是否非空,包含List,Set,Queue + * + * @param coll 要判断的Collection + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Collection coll) { + return !isEmpty(coll); + } + + /** + * * 判断一个对象数组是否为空 + * + * @param objects 要判断的对象数组 + * * @return true:为空 false:非空 + */ + public static boolean isEmpty(Object[] objects) { + return isNull(objects) || (objects.length == 0); + } + + /** + * * 判断一个对象数组是否非空 + * + * @param objects 要判断的对象数组 + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Object[] objects) { + return !isEmpty(objects); + } + + /** + * * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true:为空 false:非空 + */ + public static boolean isEmpty(Map map) { + return isNull(map) || map.isEmpty(); + } + + /** + * * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Map map) { + return !isEmpty(map); + } + + /** + * * 判断一个字符串是否为空串 + * + * @param str String + * @return true:为空 false:非空 + */ + public static boolean isEmpty(String str) { + return isNull(str) || NULL_STR.equals(str.trim()); + } + + /** + * * 判断一个字符串是否为非空串 + * + * @param str String + * @return true:非空串 false:空串 + */ + public static boolean isNotEmpty(String str) { + return !isEmpty(str); + } + + /** + * * 判断一个对象是否为空 + * + * @param object Object + * @return true:为空 false:非空 + */ + public static boolean isNull(Object object) { + return object == null; + } + + /** + * * 判断一个对象是否非空 + * + * @param object Object + * @return true:非空 false:空 + */ + public static boolean isNotNull(Object object) { + return !isNull(object); + } + + /** + * 去空格 + */ + public static String trim(String str) { + return (str == null ? "" : str.trim()); + } + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @return 结果 + */ + public static String substring(final String str, int start) { + if (str == null) { + return NULL_STR; + } + if (start < 0) { + start = str.length() + start; + } + if (start < 0) { + start = 0; + } + if (start > str.length()) { + return NULL_STR; + } + return str.substring(start); + } + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @param end 结束 + * @return 结果 + */ + public static String substring(final String str, int start, int end) { + if (str == null) { + return NULL_STR; + } + if (end < 0) { + end = str.length() + end; + } + if (start < 0) { + start = str.length() + start; + } + if (end > str.length()) { + end = str.length(); + } + if (start > end) { + return NULL_STR; + } + if (start < 0) { + start = 0; + } + if (end < 0) { + end = 0; + } + return str.substring(start, end); + } + + /** + * 格式化文本, {} 表示占位符
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ * + * @param template 文本模板,被替换的部分用 {} 表示 + * @param params 参数值 + * @return 格式化后的文本 + */ + public static String format(String template, Object... params) { + if (isEmpty(params) || isEmpty(template)) { + return template; + } + return StrFormatter.format(template, params); + } + + /** + * 是否为http(s)://开头 + * + * @param link 链接 + * @return 结果 + */ + public static boolean isHttp(String link) { + return StringUtils.startsWithAny(link, Constants.HTTP, Constants.HTTPS); + } + + /** + * 字符串转set + * + * @param str 字符串 + * @param sep 分隔符 + * @return set集合 + */ + public static Set str2Set(String str, String sep) { + return new HashSet(str2List(str, sep, true, false)); + } + + /** + * 字符串转list + * + * @param str 字符串 + * @param sep 分隔符 + * @param filterBlank 过滤纯空白 + * @param trim 去掉首尾空白 + * @return list集合 + */ + public static List str2List(String str, String sep, boolean filterBlank, boolean trim) { + List list = new ArrayList(); + if (StringUtils.isEmpty(str)) { + return list; + } + + // 过滤空白字符串 + if (filterBlank && StringUtils.isBlank(str)) { + return list; + } + String[] split = str.split(sep); + for (String string : split) { + if (filterBlank && StringUtils.isBlank(string)) { + continue; + } + if (trim) { + string = string.trim(); + } + list.add(string); + } + + return list; + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写 + * + * @param cs 指定字符串 + * @param searchCharSequences 需要检查的字符串数组 + * @return 是否包含任意一个字符串 + */ + public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences) { + if (isEmpty(cs) || isEmpty(searchCharSequences)) { + return false; + } + for (CharSequence testStr : searchCharSequences) { + if (containsIgnoreCase(cs, testStr)) { + return true; + } + } + return false; + } + + /** + * 驼峰转下划线命名 + */ + public static String toUnderScoreCase(String str) { + if (str == null) { + return null; + } + StringBuilder sb = new StringBuilder(); + // 前置字符是否大写 + boolean preCharIsUpperCase = true; + // 当前字符是否大写 + boolean curreCharIsUpperCase = true; + // 下一字符是否大写 + boolean nexteCharIsUpperCase = true; + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (i > 0) { + preCharIsUpperCase = Character.isUpperCase(str.charAt(i - 1)); + } else { + preCharIsUpperCase = false; + } + curreCharIsUpperCase = Character.isUpperCase(c); + if (i < (str.length() - 1)) { + nexteCharIsUpperCase = Character.isUpperCase(str.charAt(i + 1)); + } + if (preCharIsUpperCase && curreCharIsUpperCase && !nexteCharIsUpperCase) { + sb.append(SEPARATOR); + } else if ((i != 0 && !preCharIsUpperCase) && curreCharIsUpperCase) { + sb.append(SEPARATOR); + } + sb.append(Character.toLowerCase(c)); + } + + return sb.toString(); + } + + /** + * 是否包含字符串 + * + * @param str 验证字符串 + * @param strs 字符串组 + * @return 包含返回true + */ + public static boolean inStringIgnoreCase(String str, String... strs) { + if (str != null && strs != null) { + for (String s : strs) { + if (str.equalsIgnoreCase(trim(s))) { + return true; + } + } + } + return false; + } + + /** + * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld + * + * @param name 转换前的下划线大写方式命名的字符串 + * @return 转换后的驼峰式命名的字符串 + */ + public static String convertToCamelCase(String name) { + StringBuilder result = new StringBuilder(); + // 快速检查 + if (name == null || name.isEmpty()) { + // 没必要转换 + return ""; + } else if (!name.contains("_")) { + // 不含下划线,仅将首字母大写 + return name.substring(0, 1).toUpperCase() + name.substring(1); + } + // 用下划线将原始字符串分割 + String[] camels = name.split("_"); + for (String camel : camels) { + // 跳过原始字符串中开头、结尾的下换线或双重下划线 + if (camel.isEmpty()) { + continue; + } + // 首字母大写 + result.append(camel.substring(0, 1).toUpperCase()); + result.append(camel.substring(1).toLowerCase()); + } + return result.toString(); + } + + /** + * 驼峰式命名法 例如:user_name->userName + */ + public static String toCamelCase(String s) { + if (s == null) { + return null; + } + s = s.toLowerCase(); + StringBuilder sb = new StringBuilder(s.length()); + boolean upperCase = false; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + + if (c == SEPARATOR) { + upperCase = true; + } else if (upperCase) { + sb.append(Character.toUpperCase(c)); + upperCase = false; + } else { + sb.append(c); + } + } + return sb.toString(); + } + + /** + * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串 + * + * @param str 指定字符串 + * @param strs 需要检查的字符串数组 + * @return 是否匹配 + */ + public static boolean matches(String str, List strs) { + if (isEmpty(str) || isEmpty(strs)) { + return false; + } + for (String pattern : strs) { + if (isMatch(pattern, str)) { + return true; + } + } + return false; + } + + /** + * 判断url是否与规则配置: + * ? 表示单个字符; + * * 表示一层路径内的任意字符串,不可跨层级; + * ** 表示任意层路径; + * + * @param pattern 匹配规则 + * @param url 需要匹配的url + * @return + */ + public static boolean isMatch(String pattern, String url) { + AntPathMatcher matcher = new AntPathMatcher(); + return matcher.match(pattern, url); + } + + @SuppressWarnings("unchecked") + public static T cast(Object obj) { + return (T) obj; + } + + /** + * 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。 + * + * @param num 数字对象 + * @param size 字符串指定长度 + * @return 返回数字的字符串格式,该字符串为指定长度。 + */ + public static String padl(final Number num, final int size) { + return padl(num.toString(), size, '0'); + } + + /** + * 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。 + * + * @param s 原始字符串 + * @param size 字符串指定长度 + * @param c 用于补齐的字符 + * @return 返回指定长度的字符串,由原字符串左补齐或截取得到。 + */ + public static String padl(final String s, final int size, final char c) { + final StringBuilder sb = new StringBuilder(size); + if (s != null) { + final int len = s.length(); + if (s.length() <= size) { + for (int i = size - len; i > 0; i--) { + sb.append(c); + } + sb.append(s); + } else { + return s.substring(len - size, len); + } + } else { + for (int i = size; i > 0; i--) { + sb.append(c); + } + } + return sb.toString(); + } + + /** + * 16进制表示的字符串转换为字节数组 + * + * @param s 16进制表示的字符串 + * @return byte[] 字节数组 + */ + public static int[] hexStringToIntArray(String s) { + int len = s.length(); + int[] b = new int[len / 2]; + for (int i = 0; i < len; i += 2) { + // 两位一组,表示一个字节,把这样表示的16进制字符串,还原成一个字节 + b[i / 2] = (int) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16)); + } + return b; + } + + /** + * int转换成16进制字符串 + * + * @param b 需要转换的int值 + * @return 16进制的String + */ + public static String toHexString(int b) { + String hex = Integer.toHexString(b & 0xFF); + if (hex.length() == 1) { + hex = '0' + hex; + } + return "0x" + hex.toUpperCase(); + } + + /** + * 字节数组转换成String,指定长度转换长度 + * + * @param arrBytes + * @param count 转换长度 + * @param blank 要不要空格(每个byte字节,最是否用一个“ ”隔开) + * @return "" | arrBytes换成的字符串(不存在null) + */ + public static String byteArray2HexString(byte[] arrBytes, int count, boolean blank) { + String ret = ""; + if (arrBytes == null || arrBytes.length < 1) + return ret; + if (count > arrBytes.length) + count = arrBytes.length; + StringBuilder builder = new StringBuilder(); + + for (int i = 0; i < count; i++) { + ret = Integer.toHexString(arrBytes[i] & 0xFF).toUpperCase(); + if (ret.length() == 1) + builder.append("0").append(ret); + else + builder.append(ret); + if (blank) + builder.append(" "); + } + return builder.toString(); + } + + /** + * 将字节数组转换成16进制字符串 + * + * @param array 需要转换的字符串(字节间没有分隔符) + * @return 转换完成的字符串 + */ + public static String byteArrayToHexString(byte[] array) { + return byteArray2HexString(array, Integer.MAX_VALUE, false); + } + + /** + * 将两个ASCII字符合成一个字节; 如:"EF"--> 0xEF + * + * @param src0 byte + * @param src1 byte + * @return byte + */ + public static byte uniteBytes(byte src0, byte src1) { + byte _b0 = Byte.decode("0x" + new String(new byte[]{src0})); + _b0 = (byte) (_b0 << 4); + byte _b1 = Byte.decode("0x" + new String(new byte[]{src1})); + byte ret = (byte) (_b0 ^ _b1); + return ret; + } + +} \ No newline at end of file diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/Threads.java b/jsowell-common/src/main/java/com/jsowell/common/util/Threads.java new file mode 100644 index 000000000..5300165d4 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/Threads.java @@ -0,0 +1,77 @@ +package com.jsowell.common.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +/** + * 线程相关工具类. + * + * @author jsowell + */ +public class Threads { + private static final Logger logger = LoggerFactory.getLogger(Threads.class); + + /** + * sleep等待,单位为毫秒 + */ + public static void sleep(long milliseconds) { + try { + Thread.sleep(milliseconds); + } catch (InterruptedException e) { + return; + } + } + + /** + * 停止线程池 + * 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务. + * 如果超时, 则调用shutdownNow, 取消在workQueue中Pending的任务,并中断所有阻塞函数. + * 如果仍然超時,則強制退出. + * 另对在shutdown时线程本身被调用中断做了处理. + */ + public static void shutdownAndAwaitTermination(ExecutorService pool) { + if (pool != null && !pool.isShutdown()) { + pool.shutdown(); + try { + if (!pool.awaitTermination(120, TimeUnit.SECONDS)) { + pool.shutdownNow(); + if (!pool.awaitTermination(120, TimeUnit.SECONDS)) { + logger.info("Pool did not terminate"); + } + } + } catch (InterruptedException ie) { + pool.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } + + /** + * 打印线程异常信息 + */ + public static void printException(Runnable r, Throwable t) { + if (t == null && r instanceof Future) { + try { + Future future = (Future) r; + if (future.isDone()) { + future.get(); + } + } catch (CancellationException ce) { + t = ce; + } catch (ExecutionException ee) { + t = ee.getCause(); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + } + if (t != null) { + logger.error(t.getMessage(), t); + } + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/YKCUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/YKCUtils.java new file mode 100644 index 000000000..d2bf30db6 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/YKCUtils.java @@ -0,0 +1,136 @@ +package com.jsowell.common.util; + +import com.google.common.primitives.Bytes; +import lombok.extern.slf4j.Slf4j; + +import javax.xml.bind.DatatypeConverter; +import java.math.BigDecimal; +import java.util.Arrays; + +@Slf4j +public class YKCUtils { + + private static final BigDecimal multiple = new BigDecimal("100000"); + + /** + * 校验云快充请求数据格式 + * + * @param msg 请求报文 + * @return + */ + public static boolean checkMsg(byte[] msg) { + // 起始标志 + byte[] head = BytesUtil.copyBytes(msg, 0, 1); + // 数据长度 + byte[] length = BytesUtil.copyBytes(msg, 1, 1); + // 序列号域 + byte[] serialNumber = BytesUtil.copyBytes(msg, 2, 2); + // 加密标志 + byte[] encryptFlag = BytesUtil.copyBytes(msg, 4, 1); + // 帧类型标志 + byte[] frameType = BytesUtil.copyBytes(msg, 5, 1); + // 消息体 + byte[] msgBody = BytesUtil.copyBytes(msg, 6, msg.length - 8); + // 帧校验域 + byte[] crcByte = new byte[]{msg[msg.length - 2], msg[msg.length - 1]}; + + //起始位必须是0x68 + if (0x68 != head[0]) { + log.error("起始位必须是0x68"); + return false; + } + // 序列号域+加密标志+帧类型标志+消息体 + byte[] data = Bytes.concat(serialNumber, encryptFlag, frameType, msgBody); + // 校验长度 + if (data.length != BytesUtil.bytesToIntLittle(length)) { + log.error("数据长度不正确"); + return false; + } + // CRC校验 source target + String sourceCRC = String.format("%04x", BytesUtil.bytesToInt(crcByte, 0)); + String targetCRC = String.format("%04x", CRC16Util.calcCrc16(data)); + String oldTargetCRC = String.format("%04x", CRC16Util.calcCrc16Old(data)); + if (!sourceCRC.equalsIgnoreCase(targetCRC)) { + log.error("CRC校验不通过, 源crc:{}, 计算得出crc:{}, 老crc计算:{}, 帧类型:{}, 报文:{}, msg:{}" + , sourceCRC, targetCRC, oldTargetCRC, YKCUtils.frameType2Str(frameType), BytesUtil.binary(msg, 16), Arrays.toString(msg)); + return false; + } + return true; + } + + /** + * 转换电压电流以及起始soc + * 精确到小数点后一位;待机置零 + * + * @param bytes + * @return + */ + public static String convertVoltageCurrent(byte[] bytes) { + // 转换为int 最后一位是小数位 + int i = BytesUtil.bytesToIntLittle(bytes); + // 使用BigDecimal + BigDecimal bigDecimal = new BigDecimal(i); + BigDecimal divide = bigDecimal.divide(new BigDecimal(10), 1, BigDecimal.ROUND_UP); + return divide.toString(); + } + + /** + * 转换小数点 + * + * @param bytes + * @param scale 小数位 + * @return + */ + public static String convertDecimalPoint(byte[] bytes, int scale) { + // 转换为int + int i = BytesUtil.bytesToIntLittle(bytes); + // 使用BigDecimal + BigDecimal bigDecimal = new BigDecimal(i); + BigDecimal divide = bigDecimal.divide(BigDecimal.valueOf(Math.pow(10d, scale)), scale, BigDecimal.ROUND_UP); + return divide.toString(); + } + + /** + * 获取价格byte数组 + * 价格转换为byte数组 + * @param price 实际价格,单位元 + * @param scale 保留小数位 + * @return + */ + public static byte[] getPriceByte(String price, int scale) { + // 保留小数位 例:保留5位小数,需要乘以10的5次方 + BigDecimal value = BigDecimal.valueOf(Math.pow(10d, scale)); + int i = new BigDecimal(price).multiply(value).intValue(); + return BytesUtil.intToBytesLittle(i, 4); + } + + /** + * byte转帧类型字符串 + * @param bytes + * @return + */ + public static String frameType2Str(byte[] bytes) { + String s = BytesUtil.bin2HexStr(bytes); + return "0x" + s; + } + + public static void main(String[] args) { + // String hexString = "681E0000003388000000000027012302081602434533880000000000270101008361"; + // byte[] byteArray = new byte[hexString.length() / 2]; + // for (int i = 0; i < byteArray.length; i++) { + // int index = i * 2; + // int j = Integer.parseInt(hexString.substring(index, index + 2), 16); + // byteArray[i] = (byte) j; + // } + // System.out.println(byteArray); + // String binary = BytesUtil.binary(byteArray, 16); + // String aaa = DatatypeConverter.printHexBinary(byteArray); + // System.out.println(binary); + // System.out.println(aaa); + + String hexString = "680d3c4000038800000000002701001568"; + byte[] bytes = BytesUtil.hexStringToByteArray(hexString); + System.out.println(checkMsg(bytes)); + + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/bean/BeanUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/bean/BeanUtils.java new file mode 100644 index 000000000..8a45b2f7c --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/bean/BeanUtils.java @@ -0,0 +1,104 @@ +package com.jsowell.common.util.bean; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Bean 工具类 + * + * @author jsowell + */ +public class BeanUtils extends org.springframework.beans.BeanUtils { + /** + * Bean方法名中属性名开始的下标 + */ + private static final int BEAN_METHOD_PROP_INDEX = 3; + + /** + * 匹配getter方法的正则表达式 + */ + private static final Pattern GET_PATTERN = Pattern.compile("get(\\p{javaUpperCase}\\w*)"); + + /** + * 匹配setter方法的正则表达式 + */ + private static final Pattern SET_PATTERN = Pattern.compile("set(\\p{javaUpperCase}\\w*)"); + + /** + * Bean属性复制工具方法。 + * + * @param dest 目标对象 + * @param src 源对象 + */ + public static void copyBeanProp(Object dest, Object src) { + try { + copyProperties(src, dest); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 获取对象的setter方法。 + * + * @param obj 对象 + * @return 对象的setter方法列表 + */ + public static List getSetterMethods(Object obj) { + // setter方法列表 + List setterMethods = new ArrayList(); + + // 获取所有方法 + Method[] methods = obj.getClass().getMethods(); + + // 查找setter方法 + + for (Method method : methods) { + Matcher m = SET_PATTERN.matcher(method.getName()); + if (m.matches() && (method.getParameterTypes().length == 1)) { + setterMethods.add(method); + } + } + // 返回setter方法列表 + return setterMethods; + } + + /** + * 获取对象的getter方法。 + * + * @param obj 对象 + * @return 对象的getter方法列表 + */ + + public static List getGetterMethods(Object obj) { + // getter方法列表 + List getterMethods = new ArrayList(); + // 获取所有方法 + Method[] methods = obj.getClass().getMethods(); + // 查找getter方法 + for (Method method : methods) { + Matcher m = GET_PATTERN.matcher(method.getName()); + if (m.matches() && (method.getParameterTypes().length == 0)) { + getterMethods.add(method); + } + } + // 返回getter方法列表 + return getterMethods; + } + + /** + * 检查Bean方法名中的属性名是否相等。
+ * 如getName()和setName()属性名一样,getName()和setAge()属性名不一样。 + * + * @param m1 方法名1 + * @param m2 方法名2 + * @return 属性名一样返回true,否则返回false + */ + + public static boolean isMethodPropEquals(String m1, String m2) { + return m1.substring(BEAN_METHOD_PROP_INDEX).equals(m2.substring(BEAN_METHOD_PROP_INDEX)); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/bean/BeanValidators.java b/jsowell-common/src/main/java/com/jsowell/common/util/bean/BeanValidators.java new file mode 100644 index 000000000..5f425abfd --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/bean/BeanValidators.java @@ -0,0 +1,21 @@ +package com.jsowell.common.util.bean; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Validator; +import java.util.Set; + +/** + * bean对象属性验证 + * + * @author jsowell + */ +public class BeanValidators { + public static void validateWithException(Validator validator, Object object, Class... groups) + throws ConstraintViolationException { + Set> constraintViolations = validator.validate(object, groups); + if (!constraintViolations.isEmpty()) { + throw new ConstraintViolationException(constraintViolations); + } + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/file/FileTypeUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/file/FileTypeUtils.java new file mode 100644 index 000000000..17d5f0a61 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/file/FileTypeUtils.java @@ -0,0 +1,64 @@ +package com.jsowell.common.util.file; + +import org.apache.commons.lang3.StringUtils; + +import java.io.File; + +/** + * 文件类型工具类 + * + * @author jsowell + */ +public class FileTypeUtils { + /** + * 获取文件类型 + *

+ * 例如: jsowell.txt, 返回: txt + * + * @param file 文件名 + * @return 后缀(不含".") + */ + public static String getFileType(File file) { + if (null == file) { + return StringUtils.EMPTY; + } + return getFileType(file.getName()); + } + + /** + * 获取文件类型 + *

+ * 例如: jsowell.txt, 返回: txt + * + * @param fileName 文件名 + * @return 后缀(不含".") + */ + public static String getFileType(String fileName) { + int separatorIndex = fileName.lastIndexOf("."); + if (separatorIndex < 0) { + return ""; + } + return fileName.substring(separatorIndex + 1).toLowerCase(); + } + + /** + * 获取文件类型 + * + * @param photoByte 文件字节码 + * @return 后缀(不含".") + */ + public static String getFileExtendName(byte[] photoByte) { + String strFileExtendName = "JPG"; + if ((photoByte[0] == 71) && (photoByte[1] == 73) && (photoByte[2] == 70) && (photoByte[3] == 56) + && ((photoByte[4] == 55) || (photoByte[4] == 57)) && (photoByte[5] == 97)) { + strFileExtendName = "GIF"; + } else if ((photoByte[6] == 74) && (photoByte[7] == 70) && (photoByte[8] == 73) && (photoByte[9] == 70)) { + strFileExtendName = "JPG"; + } else if ((photoByte[0] == 66) && (photoByte[1] == 77)) { + strFileExtendName = "BMP"; + } else if ((photoByte[1] == 80) && (photoByte[2] == 78) && (photoByte[3] == 71)) { + strFileExtendName = "PNG"; + } + return strFileExtendName; + } +} \ No newline at end of file diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/file/FileUploadUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/file/FileUploadUtils.java new file mode 100644 index 000000000..b5c961e9a --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/file/FileUploadUtils.java @@ -0,0 +1,198 @@ +package com.jsowell.common.util.file; + +import com.jsowell.common.config.JsowellConfig; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.exception.file.FileNameLengthLimitExceededException; +import com.jsowell.common.exception.file.FileSizeLimitExceededException; +import com.jsowell.common.exception.file.InvalidExtensionException; +import com.jsowell.common.util.DateUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.id.Seq; +import org.apache.commons.io.FilenameUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.Objects; + +/** + * 文件上传工具类 + * + * @author jsowell + */ +public class FileUploadUtils { + /** + * 默认大小 50M + */ + public static final long DEFAULT_MAX_SIZE = 50 * 1024 * 1024; + + /** + * 默认的文件名最大长度 100 + */ + public static final int DEFAULT_FILE_NAME_LENGTH = 100; + + /** + * 默认上传的地址 + */ + private static String defaultBaseDir = JsowellConfig.getProfile(); + + public static void setDefaultBaseDir(String defaultBaseDir) { + FileUploadUtils.defaultBaseDir = defaultBaseDir; + } + + public static String getDefaultBaseDir() { + return defaultBaseDir; + } + + /** + * 以默认配置进行文件上传 + * + * @param file 上传的文件 + * @return 文件名称 + * @throws Exception + */ + public static final String upload(MultipartFile file) throws IOException { + try { + return upload(getDefaultBaseDir(), file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); + } catch (Exception e) { + throw new IOException(e.getMessage(), e); + } + } + + /** + * 根据文件路径上传 + * + * @param baseDir 相对应用的基目录 + * @param file 上传的文件 + * @return 文件名称 + * @throws IOException + */ + public static final String upload(String baseDir, MultipartFile file) throws IOException { + try { + return upload(baseDir, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); + } catch (Exception e) { + throw new IOException(e.getMessage(), e); + } + } + + /** + * 文件上传 + * + * @param baseDir 相对应用的基目录 + * @param file 上传的文件 + * @param allowedExtension 上传文件类型 + * @return 返回上传成功的文件名 + * @throws FileSizeLimitExceededException 如果超出最大大小 + * @throws FileNameLengthLimitExceededException 文件名太长 + * @throws IOException 比如读写文件出错时 + * @throws InvalidExtensionException 文件校验异常 + */ + public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension) + throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException, + InvalidExtensionException { + int fileNamelength = Objects.requireNonNull(file.getOriginalFilename()).length(); + if (fileNamelength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH) { + throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH); + } + + assertAllowed(file, allowedExtension); + + String fileName = extractFilename(file); + + String absPath = getAbsoluteFile(baseDir, fileName).getAbsolutePath(); + file.transferTo(Paths.get(absPath)); + return getPathFileName(baseDir, fileName); + } + + /** + * 编码文件名 + */ + public static final String extractFilename(MultipartFile file) { + return StringUtils.format("{}/{}_{}.{}", DateUtils.datePath(), + FilenameUtils.getBaseName(file.getOriginalFilename()), Seq.getId(Seq.uploadSeqType), getExtension(file)); + } + + public static final File getAbsoluteFile(String uploadDir, String fileName) throws IOException { + File desc = new File(uploadDir + File.separator + fileName); + + if (!desc.exists()) { + if (!desc.getParentFile().exists()) { + desc.getParentFile().mkdirs(); + } + } + return desc; + } + + public static final String getPathFileName(String uploadDir, String fileName) throws IOException { + int dirLastIndex = JsowellConfig.getProfile().length() + 1; + String currentDir = StringUtils.substring(uploadDir, dirLastIndex); + return Constants.RESOURCE_PREFIX + "/" + currentDir + "/" + fileName; + } + + /** + * 文件大小校验 + * + * @param file 上传的文件 + * @return + * @throws FileSizeLimitExceededException 如果超出最大大小 + * @throws InvalidExtensionException + */ + public static final void assertAllowed(MultipartFile file, String[] allowedExtension) + throws FileSizeLimitExceededException, InvalidExtensionException { + long size = file.getSize(); + if (size > DEFAULT_MAX_SIZE) { + throw new FileSizeLimitExceededException(DEFAULT_MAX_SIZE / 1024 / 1024); + } + + String fileName = file.getOriginalFilename(); + String extension = getExtension(file); + if (allowedExtension != null && !isAllowedExtension(extension, allowedExtension)) { + if (allowedExtension == MimeTypeUtils.IMAGE_EXTENSION) { + throw new InvalidExtensionException.InvalidImageExtensionException(allowedExtension, extension, + fileName); + } else if (allowedExtension == MimeTypeUtils.FLASH_EXTENSION) { + throw new InvalidExtensionException.InvalidFlashExtensionException(allowedExtension, extension, + fileName); + } else if (allowedExtension == MimeTypeUtils.MEDIA_EXTENSION) { + throw new InvalidExtensionException.InvalidMediaExtensionException(allowedExtension, extension, + fileName); + } else if (allowedExtension == MimeTypeUtils.VIDEO_EXTENSION) { + throw new InvalidExtensionException.InvalidVideoExtensionException(allowedExtension, extension, + fileName); + } else { + throw new InvalidExtensionException(allowedExtension, extension, fileName); + } + } + } + + /** + * 判断MIME类型是否是允许的MIME类型 + * + * @param extension + * @param allowedExtension + * @return + */ + public static final boolean isAllowedExtension(String extension, String[] allowedExtension) { + for (String str : allowedExtension) { + if (str.equalsIgnoreCase(extension)) { + return true; + } + } + return false; + } + + /** + * 获取文件名的后缀 + * + * @param file 表单文件 + * @return 后缀名 + */ + public static final String getExtension(MultipartFile file) { + String extension = FilenameUtils.getExtension(file.getOriginalFilename()); + if (StringUtils.isEmpty(extension)) { + extension = MimeTypeUtils.getExtension(Objects.requireNonNull(file.getContentType())); + } + return extension; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/file/FileUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/file/FileUtils.java new file mode 100644 index 000000000..b8eaf04af --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/file/FileUtils.java @@ -0,0 +1,252 @@ +package com.jsowell.common.util.file; + +import com.jsowell.common.config.JsowellConfig; +import com.jsowell.common.util.DateUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.id.IdUtils; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.ArrayUtils; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +/** + * 文件处理工具类 + * + * @author jsowell + */ +public class FileUtils { + public static String FILENAME_PATTERN = "[a-zA-Z0-9_\\-\\|\\.\\u4e00-\\u9fa5]+"; + + /** + * 输出指定文件的byte数组 + * + * @param filePath 文件路径 + * @param os 输出流 + * @return + */ + public static void writeBytes(String filePath, OutputStream os) throws IOException { + FileInputStream fis = null; + try { + File file = new File(filePath); + if (!file.exists()) { + throw new FileNotFoundException(filePath); + } + fis = new FileInputStream(file); + byte[] b = new byte[1024]; + int length; + while ((length = fis.read(b)) > 0) { + os.write(b, 0, length); + } + } catch (IOException e) { + throw e; + } finally { + IOUtils.close(os); + IOUtils.close(fis); + } + } + + /** + * 写数据到文件中 + * + * @param data 数据 + * @return 目标文件 + * @throws IOException IO异常 + */ + public static String writeImportBytes(byte[] data) throws IOException { + return writeBytes(data, JsowellConfig.getImportPath()); + } + + /** + * 写数据到文件中 + * + * @param data 数据 + * @param uploadDir 目标文件 + * @return 目标文件 + * @throws IOException IO异常 + */ + public static String writeBytes(byte[] data, String uploadDir) throws IOException { + FileOutputStream fos = null; + String pathName = ""; + try { + String extension = getFileExtendName(data); + pathName = DateUtils.datePath() + "/" + IdUtils.fastUUID() + "." + extension; + File file = FileUploadUtils.getAbsoluteFile(uploadDir, pathName); + fos = new FileOutputStream(file); + fos.write(data); + } finally { + IOUtils.close(fos); + } + return FileUploadUtils.getPathFileName(uploadDir, pathName); + } + + /** + * 删除文件 + * + * @param filePath 文件 + * @return + */ + public static boolean deleteFile(String filePath) { + boolean flag = false; + File file = new File(filePath); + // 路径为文件且不为空则进行删除 + if (file.isFile() && file.exists()) { + file.delete(); + flag = true; + } + return flag; + } + + /** + * 文件名称验证 + * + * @param filename 文件名称 + * @return true 正常 false 非法 + */ + public static boolean isValidFilename(String filename) { + return filename.matches(FILENAME_PATTERN); + } + + /** + * 检查文件是否可下载 + * + * @param resource 需要下载的文件 + * @return true 正常 false 非法 + */ + public static boolean checkAllowDownload(String resource) { + // 禁止目录上跳级别 + if (StringUtils.contains(resource, "..")) { + return false; + } + + // 检查允许下载的文件规则 + if (ArrayUtils.contains(MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION, FileTypeUtils.getFileType(resource))) { + return true; + } + + // 不在允许下载的文件规则 + return false; + } + + /** + * 下载文件名重新编码 + * + * @param request 请求对象 + * @param fileName 文件名 + * @return 编码后的文件名 + */ + public static String setFileDownloadHeader(HttpServletRequest request, String fileName) throws UnsupportedEncodingException { + final String agent = request.getHeader("USER-AGENT"); + String filename = fileName; + if (agent.contains("MSIE")) { + // IE浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + filename = filename.replace("+", " "); + } else if (agent.contains("Firefox")) { + // 火狐浏览器 + filename = new String(fileName.getBytes(), "ISO8859-1"); + } else if (agent.contains("Chrome")) { + // google浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + } else { + // 其它浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + } + return filename; + } + + /** + * 下载文件名重新编码 + * + * @param response 响应对象 + * @param realFileName 真实文件名 + */ + public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) throws UnsupportedEncodingException { + String percentEncodedFileName = percentEncode(realFileName); + + StringBuilder contentDispositionValue = new StringBuilder(); + contentDispositionValue.append("attachment; filename=") + .append(percentEncodedFileName) + .append(";") + .append("filename*=") + .append("utf-8''") + .append(percentEncodedFileName); + + response.addHeader("Access-Control-Expose-Headers", "Content-Disposition,download-filename"); + response.setHeader("Content-disposition", contentDispositionValue.toString()); + response.setHeader("download-filename", percentEncodedFileName); + } + + /** + * 百分号编码工具方法 + * + * @param s 需要百分号编码的字符串 + * @return 百分号编码后的字符串 + */ + public static String percentEncode(String s) throws UnsupportedEncodingException { + String encode = URLEncoder.encode(s, StandardCharsets.UTF_8.toString()); + return encode.replaceAll("\\+", "%20"); + } + + /** + * 获取图像后缀 + * + * @param photoByte 图像数据 + * @return 后缀名 + */ + public static String getFileExtendName(byte[] photoByte) { + String strFileExtendName = "jpg"; + if ((photoByte[0] == 71) && (photoByte[1] == 73) && (photoByte[2] == 70) && (photoByte[3] == 56) + && ((photoByte[4] == 55) || (photoByte[4] == 57)) && (photoByte[5] == 97)) { + strFileExtendName = "gif"; + } else if ((photoByte[6] == 74) && (photoByte[7] == 70) && (photoByte[8] == 73) && (photoByte[9] == 70)) { + strFileExtendName = "jpg"; + } else if ((photoByte[0] == 66) && (photoByte[1] == 77)) { + strFileExtendName = "bmp"; + } else if ((photoByte[1] == 80) && (photoByte[2] == 78) && (photoByte[3] == 71)) { + strFileExtendName = "png"; + } + return strFileExtendName; + } + + /** + * 获取文件名称 /profile/upload/2022/04/16/jsowell.png -- jsowell.png + * + * @param fileName 路径名称 + * @return 没有文件路径的名称 + */ + public static String getName(String fileName) { + if (fileName == null) { + return null; + } + int lastUnixPos = fileName.lastIndexOf('/'); + int lastWindowsPos = fileName.lastIndexOf('\\'); + int index = Math.max(lastUnixPos, lastWindowsPos); + return fileName.substring(index + 1); + } + + /** + * 获取不带后缀文件名称 /profile/upload/2022/04/16/jsowell.png -- jsowell + * + * @param fileName 路径名称 + * @return 没有文件路径和后缀的名称 + */ + public static String getNameNotSuffix(String fileName) { + if (fileName == null) { + return null; + } + String baseName = FilenameUtils.getBaseName(fileName); + return baseName; + } + +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/file/ImageUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/file/ImageUtils.java new file mode 100644 index 000000000..d48583bb4 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/file/ImageUtils.java @@ -0,0 +1,79 @@ +package com.jsowell.common.util.file; + +import com.jsowell.common.config.JsowellConfig; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.util.StringUtils; +import org.apache.poi.util.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.Arrays; + +/** + * 图片处理工具类 + * + * @author jsowell + */ +public class ImageUtils { + private static final Logger log = LoggerFactory.getLogger(ImageUtils.class); + + public static byte[] getImage(String imagePath) { + InputStream is = getFile(imagePath); + try { + return IOUtils.toByteArray(is); + } catch (Exception e) { + log.error("图片加载异常 {}", e); + return null; + } finally { + IOUtils.closeQuietly(is); + } + } + + public static InputStream getFile(String imagePath) { + try { + byte[] result = readFile(imagePath); + result = Arrays.copyOf(result, result.length); + return new ByteArrayInputStream(result); + } catch (Exception e) { + log.error("获取图片异常 {}", e); + } + return null; + } + + /** + * 读取文件为字节数据 + * + * @param url 地址 + * @return 字节数据 + */ + public static byte[] readFile(String url) { + InputStream in = null; + try { + if (url.startsWith("http")) { + // 网络地址 + URL urlObj = new URL(url); + URLConnection urlConnection = urlObj.openConnection(); + urlConnection.setConnectTimeout(30 * 1000); + urlConnection.setReadTimeout(60 * 1000); + urlConnection.setDoInput(true); + in = urlConnection.getInputStream(); + } else { + // 本机地址 + String localPath = JsowellConfig.getProfile(); + String downloadPath = localPath + StringUtils.substringAfter(url, Constants.RESOURCE_PREFIX); + in = new FileInputStream(downloadPath); + } + return IOUtils.toByteArray(in); + } catch (Exception e) { + log.error("获取文件路径异常 {}", e); + return null; + } finally { + IOUtils.closeQuietly(in); + } + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/file/MimeTypeUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/file/MimeTypeUtils.java new file mode 100644 index 000000000..cea243d2a --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/file/MimeTypeUtils.java @@ -0,0 +1,56 @@ +package com.jsowell.common.util.file; + +/** + * 媒体类型工具类 + * + * @author jsowell + */ +public class MimeTypeUtils { + public static final String IMAGE_PNG = "image/png"; + + public static final String IMAGE_JPG = "image/jpg"; + + public static final String IMAGE_JPEG = "image/jpeg"; + + public static final String IMAGE_BMP = "image/bmp"; + + public static final String IMAGE_GIF = "image/gif"; + + public static final String[] IMAGE_EXTENSION = {"bmp", "gif", "jpg", "jpeg", "png"}; + + public static final String[] FLASH_EXTENSION = {"swf", "flv"}; + + public static final String[] MEDIA_EXTENSION = {"swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg", + "asf", "rm", "rmvb"}; + + public static final String[] VIDEO_EXTENSION = {"mp4", "avi", "rmvb"}; + + public static final String[] DEFAULT_ALLOWED_EXTENSION = { + // 图片 + "bmp", "gif", "jpg", "jpeg", "png", + // word excel powerpoint + "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt", + // 压缩文件 + "rar", "zip", "gz", "bz2", + // 视频格式 + "mp4", "avi", "rmvb", + // pdf + "pdf"}; + + public static String getExtension(String prefix) { + switch (prefix) { + case IMAGE_PNG: + return "png"; + case IMAGE_JPG: + return "jpg"; + case IMAGE_JPEG: + return "jpeg"; + case IMAGE_BMP: + return "bmp"; + case IMAGE_GIF: + return "gif"; + default: + return ""; + } + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/html/EscapeUtil.java b/jsowell-common/src/main/java/com/jsowell/common/util/html/EscapeUtil.java new file mode 100644 index 000000000..b2a2c6a68 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/html/EscapeUtil.java @@ -0,0 +1,140 @@ +package com.jsowell.common.util.html; + +import com.jsowell.common.util.StringUtils; + +/** + * 转义和反转义工具类 + * + * @author jsowell + */ +public class EscapeUtil { + public static final String RE_HTML_MARK = "(<[^<]*?>)|(<[\\s]*?/[^<]*?>)|(<[^<]*?/[\\s]*?>)"; + + private static final char[][] TEXT = new char[64][]; + + static { + for (int i = 0; i < 64; i++) { + TEXT[i] = new char[]{(char) i}; + } + + // special HTML characters + TEXT['\''] = "'".toCharArray(); // 单引号 + TEXT['"'] = """.toCharArray(); // 双引号 + TEXT['&'] = "&".toCharArray(); // &符 + TEXT['<'] = "<".toCharArray(); // 小于号 + TEXT['>'] = ">".toCharArray(); // 大于号 + } + + /** + * 转义文本中的HTML字符为安全的字符 + * + * @param text 被转义的文本 + * @return 转义后的文本 + */ + public static String escape(String text) { + return encode(text); + } + + /** + * 还原被转义的HTML特殊字符 + * + * @param content 包含转义符的HTML内容 + * @return 转换后的字符串 + */ + public static String unescape(String content) { + return decode(content); + } + + /** + * 清除所有HTML标签,但是不删除标签内的内容 + * + * @param content 文本 + * @return 清除标签后的文本 + */ + public static String clean(String content) { + return new HTMLFilter().filter(content); + } + + /** + * Escape编码 + * + * @param text 被编码的文本 + * @return 编码后的字符 + */ + private static String encode(String text) { + if (StringUtils.isEmpty(text)) { + return StringUtils.EMPTY; + } + + final StringBuilder tmp = new StringBuilder(text.length() * 6); + char c; + for (int i = 0; i < text.length(); i++) { + c = text.charAt(i); + if (c < 256) { + tmp.append("%"); + if (c < 16) { + tmp.append("0"); + } + tmp.append(Integer.toString(c, 16)); + } else { + tmp.append("%u"); + if (c <= 0xfff) { + // issue#I49JU8@Gitee + tmp.append("0"); + } + tmp.append(Integer.toString(c, 16)); + } + } + return tmp.toString(); + } + + /** + * Escape解码 + * + * @param content 被转义的内容 + * @return 解码后的字符串 + */ + public static String decode(String content) { + if (StringUtils.isEmpty(content)) { + return content; + } + + StringBuilder tmp = new StringBuilder(content.length()); + int lastPos = 0, pos = 0; + char ch; + while (lastPos < content.length()) { + pos = content.indexOf("%", lastPos); + if (pos == lastPos) { + if (content.charAt(pos + 1) == 'u') { + ch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16); + tmp.append(ch); + lastPos = pos + 6; + } else { + ch = (char) Integer.parseInt(content.substring(pos + 1, pos + 3), 16); + tmp.append(ch); + lastPos = pos + 3; + } + } else { + if (pos == -1) { + tmp.append(content.substring(lastPos)); + lastPos = content.length(); + } else { + tmp.append(content.substring(lastPos, pos)); + lastPos = pos; + } + } + } + return tmp.toString(); + } + + public static void main(String[] args) { + String html = ""; + String escape = EscapeUtil.escape(html); + // String html = "ipt>alert(\"XSS\")ipt>"; + // String html = "<123"; + // String html = "123>"; + System.out.println("clean: " + EscapeUtil.clean(html)); + System.out.println("escape: " + escape); + System.out.println("unescape: " + EscapeUtil.unescape(escape)); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/html/HTMLFilter.java b/jsowell-common/src/main/java/com/jsowell/common/util/html/HTMLFilter.java new file mode 100644 index 000000000..9cfc903a8 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/html/HTMLFilter.java @@ -0,0 +1,501 @@ +package com.jsowell.common.util.html; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * HTML过滤器,用于去除XSS漏洞隐患。 + * + * @author jsowell + */ +public final class HTMLFilter { + /** + * regex flag union representing /si modifiers in php + **/ + private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL; + private static final Pattern P_COMMENTS = Pattern.compile("", Pattern.DOTALL); + private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI); + private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL); + private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI); + private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI); + private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI); + private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI); + private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI); + private static final Pattern P_ENTITY = Pattern.compile("&#(\\d+);?"); + private static final Pattern P_ENTITY_UNICODE = Pattern.compile("&#x([0-9a-f]+);?"); + private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?"); + private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))"); + private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL); + private static final Pattern P_END_ARROW = Pattern.compile("^>"); + private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_AMP = Pattern.compile("&"); + private static final Pattern P_QUOTE = Pattern.compile("\""); + private static final Pattern P_LEFT_ARROW = Pattern.compile("<"); + private static final Pattern P_RIGHT_ARROW = Pattern.compile(">"); + private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>"); + + // @xxx could grow large... maybe use sesat's ReferenceMap + private static final ConcurrentMap P_REMOVE_PAIR_BLANKS = new ConcurrentHashMap<>(); + private static final ConcurrentMap P_REMOVE_SELF_BLANKS = new ConcurrentHashMap<>(); + + /** + * set of allowed html elements, along with allowed attributes for each element + **/ + private final Map> vAllowed; + /** + * counts of open tags for each (allowable) html element + **/ + private final Map vTagCounts = new HashMap<>(); + + /** + * html elements which must always be self-closing (e.g. "") + **/ + private final String[] vSelfClosingTags; + /** + * html elements which must always have separate opening and closing tags (e.g. "") + **/ + private final String[] vNeedClosingTags; + /** + * set of disallowed html elements + **/ + private final String[] vDisallowed; + /** + * attributes which should be checked for valid protocols + **/ + private final String[] vProtocolAtts; + /** + * allowed protocols + **/ + private final String[] vAllowedProtocols; + /** + * tags which should be removed if they contain no content (e.g. "" or "") + **/ + private final String[] vRemoveBlanks; + /** + * entities allowed within html markup + **/ + private final String[] vAllowedEntities; + /** + * flag determining whether comments are allowed in input String. + */ + private final boolean stripComment; + private final boolean encodeQuotes; + /** + * flag determining whether to try to make tags when presented with "unbalanced" angle brackets (e.g. "" + * becomes " text "). If set to false, unbalanced angle brackets will be html escaped. + */ + private final boolean alwaysMakeTags; + + /** + * Default constructor. + */ + public HTMLFilter() { + vAllowed = new HashMap<>(); + + final ArrayList a_atts = new ArrayList<>(); + a_atts.add("href"); + a_atts.add("target"); + vAllowed.put("a", a_atts); + + final ArrayList img_atts = new ArrayList<>(); + img_atts.add("src"); + img_atts.add("width"); + img_atts.add("height"); + img_atts.add("alt"); + vAllowed.put("img", img_atts); + + final ArrayList no_atts = new ArrayList<>(); + vAllowed.put("b", no_atts); + vAllowed.put("strong", no_atts); + vAllowed.put("i", no_atts); + vAllowed.put("em", no_atts); + + vSelfClosingTags = new String[]{"img"}; + vNeedClosingTags = new String[]{"a", "b", "strong", "i", "em"}; + vDisallowed = new String[]{}; + vAllowedProtocols = new String[]{"http", "mailto", "https"}; // no ftp. + vProtocolAtts = new String[]{"src", "href"}; + vRemoveBlanks = new String[]{"a", "b", "strong", "i", "em"}; + vAllowedEntities = new String[]{"amp", "gt", "lt", "quot"}; + stripComment = true; + encodeQuotes = true; + alwaysMakeTags = false; + } + + /** + * Map-parameter configurable constructor. + * + * @param conf map containing configuration. keys match field names. + */ + @SuppressWarnings("unchecked") + public HTMLFilter(final Map conf) { + + assert conf.containsKey("vAllowed") : "configuration requires vAllowed"; + assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags"; + assert conf.containsKey("vNeedClosingTags") : "configuration requires vNeedClosingTags"; + assert conf.containsKey("vDisallowed") : "configuration requires vDisallowed"; + assert conf.containsKey("vAllowedProtocols") : "configuration requires vAllowedProtocols"; + assert conf.containsKey("vProtocolAtts") : "configuration requires vProtocolAtts"; + assert conf.containsKey("vRemoveBlanks") : "configuration requires vRemoveBlanks"; + assert conf.containsKey("vAllowedEntities") : "configuration requires vAllowedEntities"; + + vAllowed = Collections.unmodifiableMap((HashMap>) conf.get("vAllowed")); + vSelfClosingTags = (String[]) conf.get("vSelfClosingTags"); + vNeedClosingTags = (String[]) conf.get("vNeedClosingTags"); + vDisallowed = (String[]) conf.get("vDisallowed"); + vAllowedProtocols = (String[]) conf.get("vAllowedProtocols"); + vProtocolAtts = (String[]) conf.get("vProtocolAtts"); + vRemoveBlanks = (String[]) conf.get("vRemoveBlanks"); + vAllowedEntities = (String[]) conf.get("vAllowedEntities"); + stripComment = conf.containsKey("stripComment") ? (Boolean) conf.get("stripComment") : true; + encodeQuotes = conf.containsKey("encodeQuotes") ? (Boolean) conf.get("encodeQuotes") : true; + alwaysMakeTags = conf.containsKey("alwaysMakeTags") ? (Boolean) conf.get("alwaysMakeTags") : true; + } + + private void reset() { + vTagCounts.clear(); + } + + // --------------------------------------------------------------- + // my versions of some PHP library functions + public static String chr(final int decimal) { + return String.valueOf((char) decimal); + } + + public static String htmlSpecialChars(final String s) { + String result = s; + result = regexReplace(P_AMP, "&", result); + result = regexReplace(P_QUOTE, """, result); + result = regexReplace(P_LEFT_ARROW, "<", result); + result = regexReplace(P_RIGHT_ARROW, ">", result); + return result; + } + + // --------------------------------------------------------------- + + /** + * given a user submitted input String, filter out any invalid or restricted html. + * + * @param input text (i.e. submitted by a user) than may contain html + * @return "clean" version of input, with only valid, whitelisted html elements allowed + */ + public String filter(final String input) { + reset(); + String s = input; + + s = escapeComments(s); + + s = balanceHTML(s); + + s = checkTags(s); + + s = processRemoveBlanks(s); + + // s = validateEntities(s); + + return s; + } + + public boolean isAlwaysMakeTags() { + return alwaysMakeTags; + } + + public boolean isStripComments() { + return stripComment; + } + + private String escapeComments(final String s) { + final Matcher m = P_COMMENTS.matcher(s); + final StringBuffer buf = new StringBuffer(); + if (m.find()) { + final String match = m.group(1); // (.*?) + m.appendReplacement(buf, Matcher.quoteReplacement("")); + } + m.appendTail(buf); + + return buf.toString(); + } + + private String balanceHTML(String s) { + if (alwaysMakeTags) { + // + // try and form html + // + s = regexReplace(P_END_ARROW, "", s); + // 不追加结束标签 + s = regexReplace(P_BODY_TO_END, "<$1>", s); + s = regexReplace(P_XML_CONTENT, "$1<$2", s); + + } else { + // + // escape stray brackets + // + s = regexReplace(P_STRAY_LEFT_ARROW, "<$1", s); + s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2><", s); + + // + // the last regexp causes '<>' entities to appear + // (we need to do a lookahead assertion so that the last bracket can + // be used in the next pass of the regexp) + // + s = regexReplace(P_BOTH_ARROWS, "", s); + } + + return s; + } + + private String checkTags(String s) { + Matcher m = P_TAGS.matcher(s); + + final StringBuffer buf = new StringBuffer(); + while (m.find()) { + String replaceStr = m.group(1); + replaceStr = processTag(replaceStr); + m.appendReplacement(buf, Matcher.quoteReplacement(replaceStr)); + } + m.appendTail(buf); + + // these get tallied in processTag + // (remember to reset before subsequent calls to filter method) + final StringBuilder sBuilder = new StringBuilder(buf.toString()); + for (String key : vTagCounts.keySet()) { + for (int ii = 0; ii < vTagCounts.get(key); ii++) { + sBuilder.append(""); + } + } + s = sBuilder.toString(); + + return s; + } + + private String processRemoveBlanks(final String s) { + String result = s; + for (String tag : vRemoveBlanks) { + if (!P_REMOVE_PAIR_BLANKS.containsKey(tag)) { + P_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?>")); + } + result = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), "", result); + if (!P_REMOVE_SELF_BLANKS.containsKey(tag)) { + P_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?/>")); + } + result = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), "", result); + } + + return result; + } + + private static String regexReplace(final Pattern regex_pattern, final String replacement, final String s) { + Matcher m = regex_pattern.matcher(s); + return m.replaceAll(replacement); + } + + private String processTag(final String s) { + // ending tags + Matcher m = P_END_TAG.matcher(s); + if (m.find()) { + final String name = m.group(1).toLowerCase(); + if (allowed(name)) { + if (!inArray(name, vSelfClosingTags)) { + if (vTagCounts.containsKey(name)) { + vTagCounts.put(name, vTagCounts.get(name) - 1); + return ""; + } + } + } + } + + // starting tags + m = P_START_TAG.matcher(s); + if (m.find()) { + final String name = m.group(1).toLowerCase(); + final String body = m.group(2); + String ending = m.group(3); + + // debug( "in a starting tag, name='" + name + "'; body='" + body + "'; ending='" + ending + "'" ); + if (allowed(name)) { + final StringBuilder params = new StringBuilder(); + + final Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body); + final Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body); + final List paramNames = new ArrayList<>(); + final List paramValues = new ArrayList<>(); + while (m2.find()) { + paramNames.add(m2.group(1)); // ([a-z0-9]+) + paramValues.add(m2.group(3)); // (.*?) + } + while (m3.find()) { + paramNames.add(m3.group(1)); // ([a-z0-9]+) + paramValues.add(m3.group(3)); // ([^\"\\s']+) + } + + String paramName, paramValue; + for (int ii = 0; ii < paramNames.size(); ii++) { + paramName = paramNames.get(ii).toLowerCase(); + paramValue = paramValues.get(ii); + + // debug( "paramName='" + paramName + "'" ); + // debug( "paramValue='" + paramValue + "'" ); + // debug( "allowed? " + vAllowed.get( name ).contains( paramName ) ); + + if (allowedAttribute(name, paramName)) { + if (inArray(paramName, vProtocolAtts)) { + paramValue = processParamProtocol(paramValue); + } + params.append(' ').append(paramName).append("=\\\"").append(paramValue).append("\\\""); + } + } + + if (inArray(name, vSelfClosingTags)) { + ending = " /"; + } + + if (inArray(name, vNeedClosingTags)) { + ending = ""; + } + + if (ending == null || ending.length() < 1) { + if (vTagCounts.containsKey(name)) { + vTagCounts.put(name, vTagCounts.get(name) + 1); + } else { + vTagCounts.put(name, 1); + } + } else { + ending = " /"; + } + return "<" + name + params + ending + ">"; + } else { + return ""; + } + } + + // comments + m = P_COMMENT.matcher(s); + if (!stripComment && m.find()) { + return "<" + m.group() + ">"; + } + + return ""; + } + + private String processParamProtocol(String s) { + s = decodeEntities(s); + final Matcher m = P_PROTOCOL.matcher(s); + if (m.find()) { + final String protocol = m.group(1); + if (!inArray(protocol, vAllowedProtocols)) { + // bad protocol, turn into local anchor link instead + s = "#" + s.substring(protocol.length() + 1); + if (s.startsWith("#//")) { + s = "#" + s.substring(3); + } + } + } + + return s; + } + + private String decodeEntities(String s) { + StringBuffer buf = new StringBuffer(); + + Matcher m = P_ENTITY.matcher(s); + while (m.find()) { + final String match = m.group(1); + final int decimal = Integer.decode(match).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENTITY_UNICODE.matcher(s); + while (m.find()) { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENCODE.matcher(s); + while (m.find()) { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + s = validateEntities(s); + return s; + } + + private String validateEntities(final String s) { + StringBuffer buf = new StringBuffer(); + + // validate entities throughout the string + Matcher m = P_VALID_ENTITIES.matcher(s); + while (m.find()) { + final String one = m.group(1); // ([^&;]*) + final String two = m.group(2); // (?=(;|&|$)) + m.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two))); + } + m.appendTail(buf); + + return encodeQuotes(buf.toString()); + } + + private String encodeQuotes(final String s) { + if (encodeQuotes) { + StringBuffer buf = new StringBuffer(); + Matcher m = P_VALID_QUOTES.matcher(s); + while (m.find()) { + final String one = m.group(1); // (>|^) + final String two = m.group(2); // ([^<]+?) + final String three = m.group(3); // (<|$) + // 不替换双引号为",防止json格式无效 regexReplace(P_QUOTE, """, two) + m.appendReplacement(buf, Matcher.quoteReplacement(one + two + three)); + } + m.appendTail(buf); + return buf.toString(); + } else { + return s; + } + } + + private String checkEntity(final String preamble, final String term) { + + return ";".equals(term) && isValidEntity(preamble) ? '&' + preamble : "&" + preamble; + } + + private boolean isValidEntity(final String entity) { + return inArray(entity, vAllowedEntities); + } + + private static boolean inArray(final String s, final String[] array) { + for (String item : array) { + if (item != null && item.equals(s)) { + return true; + } + } + return false; + } + + private boolean allowed(final String name) { + return (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed); + } + + private boolean allowedAttribute(final String name, final String paramName) { + return allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName)); + } +} \ No newline at end of file diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/http/HttpHelper.java b/jsowell-common/src/main/java/com/jsowell/common/util/http/HttpHelper.java new file mode 100644 index 000000000..7a23ea8b1 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/http/HttpHelper.java @@ -0,0 +1,44 @@ +package com.jsowell.common.util.http; + +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.ServletRequest; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + +/** + * 通用http工具封装 + * + * @author jsowell + */ +public class HttpHelper { + private static final Logger LOGGER = LoggerFactory.getLogger(HttpHelper.class); + + public static String getBodyString(ServletRequest request) { + StringBuilder sb = new StringBuilder(); + BufferedReader reader = null; + try (InputStream inputStream = request.getInputStream()) { + reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + String line = ""; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + } catch (IOException e) { + LOGGER.warn("getBodyString出现问题!"); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + LOGGER.error(ExceptionUtils.getMessage(e)); + } + } + } + return sb.toString(); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/http/HttpUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/http/HttpUtils.java new file mode 100644 index 000000000..279afdb69 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/http/HttpUtils.java @@ -0,0 +1,277 @@ +package com.jsowell.common.util.http; + +import com.jsowell.common.constant.Constants; +import com.jsowell.common.util.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.ConnectException; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; + +/** + * 通用http发送方法 + * + * @author jsowell + */ +public class HttpUtils { + private static final Logger log = LoggerFactory.getLogger(HttpUtils.class); + + /** + * 向指定 URL 发送GET方法的请求 + * + * @param url 发送请求的 URL + * @return 所代表远程资源的响应结果 + */ + public static String sendGet(String url) { + return sendGet(url, StringUtils.EMPTY); + } + + /** + * 向指定 URL 发送GET方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 + * @return 所代表远程资源的响应结果 + */ + public static String sendGet(String url, String param) { + return sendGet(url, param, Constants.UTF8); + } + + /** + * 向指定 URL 发送GET方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 + * @param contentType 编码类型 + * @return 所代表远程资源的响应结果 + */ + public static String sendGet(String url, String param, String contentType) { + StringBuilder result = new StringBuilder(); + BufferedReader in = null; + try { + String urlNameString = StringUtils.isNotBlank(param) ? url + "?" + param : url; + log.info("sendGet - {}", urlNameString); + URL realUrl = new URL(urlNameString); + URLConnection connection = realUrl.openConnection(); + connection.setRequestProperty("accept", "*/*"); + connection.setRequestProperty("connection", "Keep-Alive"); + connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); + connection.connect(); + in = new BufferedReader(new InputStreamReader(connection.getInputStream(), contentType)); + String line; + while ((line = in.readLine()) != null) { + result.append(line); + } + log.info("recv - {}", result); + } catch (ConnectException e) { + log.error("调用HttpUtils.sendGet ConnectException, url=" + url + ",param=" + param, e); + } catch (SocketTimeoutException e) { + log.error("调用HttpUtils.sendGet SocketTimeoutException, url=" + url + ",param=" + param, e); + } catch (IOException e) { + log.error("调用HttpUtils.sendGet IOException, url=" + url + ",param=" + param, e); + } catch (Exception e) { + log.error("调用HttpsUtil.sendGet Exception, url=" + url + ",param=" + param, e); + } finally { + try { + if (in != null) { + in.close(); + } + } catch (Exception ex) { + log.error("调用in.close Exception, url=" + url + ",param=" + param, ex); + } + } + return result.toString(); + } + + /** + * 向指定 URL 发送POST方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 + * @return 所代表远程资源的响应结果 + */ + public static String sendPost(String url, String param) { + PrintWriter out = null; + BufferedReader in = null; + StringBuilder result = new StringBuilder(); + try { + log.info("sendPost - {}", url); + URL realUrl = new URL(url); + URLConnection conn = realUrl.openConnection(); + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("connection", "Keep-Alive"); + conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); + conn.setRequestProperty("Accept-Charset", "utf-8"); + conn.setRequestProperty("contentType", "utf-8"); + conn.setDoOutput(true); + conn.setDoInput(true); + out = new PrintWriter(conn.getOutputStream()); + out.print(param); + out.flush(); + in = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)); + String line; + while ((line = in.readLine()) != null) { + result.append(line); + } + log.info("recv - {}", result); + } catch (ConnectException e) { + log.error("调用HttpUtils.sendPost ConnectException, url=" + url + ",param=" + param, e); + } catch (SocketTimeoutException e) { + log.error("调用HttpUtils.sendPost SocketTimeoutException, url=" + url + ",param=" + param, e); + } catch (IOException e) { + log.error("调用HttpUtils.sendPost IOException, url=" + url + ",param=" + param, e); + } catch (Exception e) { + log.error("调用HttpsUtil.sendPost Exception, url=" + url + ",param=" + param, e); + } finally { + try { + if (out != null) { + out.close(); + } + if (in != null) { + in.close(); + } + } catch (IOException ex) { + log.error("调用in.close Exception, url=" + url + ",param=" + param, ex); + } + } + return result.toString(); + } + + /** + * 向指定 URL 发送POST方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 + * @param contentType header中的contentType + * @return 所代表远程资源的响应结果 + */ + public static String sendPostContentType(String url, String param, String contentType) { + PrintWriter out = null; + BufferedReader in = null; + StringBuilder result = new StringBuilder(); + try { + log.info("sendPost - {}", url); + URL realUrl = new URL(url); + URLConnection conn = realUrl.openConnection(); + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("connection", "Keep-Alive"); + conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); + conn.setRequestProperty("Accept-Charset", "utf-8"); + conn.setRequestProperty("Content-Type", contentType); + conn.setDoOutput(true); + conn.setDoInput(true); + out = new PrintWriter(conn.getOutputStream()); + out.print(param); + out.flush(); + in = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)); + String line; + while ((line = in.readLine()) != null) { + result.append(line); + } + log.info("recv - {}", result); + } catch (ConnectException e) { + log.error("调用HttpUtils.sendPost ConnectException, url=" + url + ",param=" + param, e); + } catch (SocketTimeoutException e) { + log.error("调用HttpUtils.sendPost SocketTimeoutException, url=" + url + ",param=" + param, e); + } catch (IOException e) { + log.error("调用HttpUtils.sendPost IOException, url=" + url + ",param=" + param, e); + } catch (Exception e) { + log.error("调用HttpsUtil.sendPost Exception, url=" + url + ",param=" + param, e); + } finally { + try { + if (out != null) { + out.close(); + } + if (in != null) { + in.close(); + } + } catch (IOException ex) { + log.error("调用in.close Exception, url=" + url + ",param=" + param, ex); + } + } + return result.toString(); + } + + + + + public static String sendSSLPost(String url, String param) { + StringBuilder result = new StringBuilder(); + String urlNameString = url + "?" + param; + try { + log.info("sendSSLPost - {}", urlNameString); + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, new TrustManager[]{new TrustAnyTrustManager()}, new java.security.SecureRandom()); + URL console = new URL(urlNameString); + HttpsURLConnection conn = (HttpsURLConnection) console.openConnection(); + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("connection", "Keep-Alive"); + conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); + conn.setRequestProperty("Accept-Charset", "utf-8"); + conn.setRequestProperty("contentType", "utf-8"); + conn.setDoOutput(true); + conn.setDoInput(true); + + conn.setSSLSocketFactory(sc.getSocketFactory()); + conn.setHostnameVerifier(new TrustAnyHostnameVerifier()); + conn.connect(); + InputStream is = conn.getInputStream(); + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + String ret = ""; + while ((ret = br.readLine()) != null) { + if (ret != null && !"".equals(ret.trim())) { + result.append(new String(ret.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8)); + } + } + log.info("recv - {}", result); + conn.disconnect(); + br.close(); + } catch (ConnectException e) { + log.error("调用HttpUtils.sendSSLPost ConnectException, url=" + url + ",param=" + param, e); + } catch (SocketTimeoutException e) { + log.error("调用HttpUtils.sendSSLPost SocketTimeoutException, url=" + url + ",param=" + param, e); + } catch (IOException e) { + log.error("调用HttpUtils.sendSSLPost IOException, url=" + url + ",param=" + param, e); + } catch (Exception e) { + log.error("调用HttpsUtil.sendSSLPost Exception, url=" + url + ",param=" + param, e); + } + return result.toString(); + } + + private static class TrustAnyTrustManager implements X509TrustManager { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[]{}; + } + } + + private static class TrustAnyHostnameVerifier implements HostnameVerifier { + @Override + public boolean verify(String hostname, SSLSession session) { + return true; + } + } +} \ No newline at end of file diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/id/IdUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/id/IdUtils.java new file mode 100644 index 000000000..2f432e749 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/id/IdUtils.java @@ -0,0 +1,101 @@ +package com.jsowell.common.util.id; + +import com.google.common.collect.Sets; +import com.jsowell.common.util.DateUtils; +import com.jsowell.common.util.RandomUtil; + +import java.util.Set; + +/** + * ID生成器工具类 + * + * @author jsowell + */ +public class IdUtils { + /** + * 获取随机UUID + * + * @return 随机UUID + */ + public static String randomUUID() { + return UUID.randomUUID().toString(); + } + + /** + * 简化的UUID,去掉了横线 + * + * @return 简化的UUID,去掉了横线 + */ + public static String simpleUUID() { + return UUID.randomUUID().toString(true); + } + + /** + * 获取随机UUID,使用性能更好的ThreadLocalRandom生成UUID + * + * @return 随机UUID + */ + public static String fastUUID() { + return UUID.fastUUID().toString(); + } + + /** + * 简化的UUID,去掉了横线,使用性能更好的ThreadLocalRandom生成UUID + * + * @return 简化的UUID,去掉了横线 + */ + public static String fastSimpleUUID() { + return UUID.fastUUID().toString(true); + } + + /** + * 生成交易流水号 + * + * @param pileSn 桩编号 例如:32010600019236 + * @param connectorCode 枪口号 例如:01 + */ + public static String generateOrderCode(String pileSn, String connectorCode) { + return generateOrderCode(pileSn + connectorCode); + } + + /** + * 生成交易流水号 + * 生成规则为 格式桩号(7bytes) +枪号(1byte) +年月日时分秒(6bytes) +自 增序号(2bytes); + * @param pileConnectorCode 为已经拼好的充电桩枪口号 例如:3201060001923601 + * @return 交易流水号 例如:32010600019236012001061803423060 + */ + public static String generateOrderCode(String pileConnectorCode) { + String timeNow = DateUtils.dateTimeNow(DateUtils.YYMMDDHHMMSS); + //随机生成一个四位整数 + String randomNumber = RandomUtil.getRandomNumber(4); + return pileConnectorCode + timeNow + randomNumber; + } + + public static void main(String[] args) { + Set set = Sets.newHashSet(); + for (int i = 0; i < 1000; i++) { + // String s = System.currentTimeMillis() + RandomUtil.getRandomNumber(6); + String id = SnowflakeIdWorker.getSnowflakeId(); + set.add(id); + System.out.println(id); + } + System.out.println("set size = " + set.size()); + } + + /** + * 生成八位会员id + */ + public static String getMemberId() { + long id = Long.parseLong(SnowflakeIdWorker.getSnowflakeId()); + StringBuilder sb = new StringBuilder(id + ""); + StringBuilder reverse = sb.reverse();//将id翻转:我们发现id很长,且高位很长部分是一样的数 + id = new Long(reverse.toString()) / 1000;//切去部分长度 + while (id > 100000000) { + id /= 10; + } + Integer num = Integer.parseInt(id + ""); + return String.valueOf(num); + } + + +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/id/Seq.java b/jsowell-common/src/main/java/com/jsowell/common/util/id/Seq.java new file mode 100644 index 000000000..573c2652c --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/id/Seq.java @@ -0,0 +1,80 @@ +package com.jsowell.common.util.id; + +import com.jsowell.common.util.DateUtils; +import com.jsowell.common.util.StringUtils; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author jsowell 序列生成类 + */ +public class Seq { + // 通用序列类型 + public static final String commSeqType = "COMMON"; + + // 上传序列类型 + public static final String uploadSeqType = "UPLOAD"; + + // 通用接口序列数 + private static AtomicInteger commSeq = new AtomicInteger(1); + + // 上传接口序列数 + private static AtomicInteger uploadSeq = new AtomicInteger(1); + + // 机器标识 + private static String machineCode = "A"; + + /** + * 获取通用序列号 + * + * @return 序列值 + */ + public static String getId() { + return getId(commSeqType); + } + + /** + * 默认16位序列号 yyMMddHHmmss + 一位机器标识 + 3长度循环递增字符串 + * + * @return 序列值 + */ + public static String getId(String type) { + AtomicInteger atomicInt = commSeq; + if (uploadSeqType.equals(type)) { + atomicInt = uploadSeq; + } + return getId(atomicInt, 3); + } + + /** + * 通用接口序列号 yyMMddHHmmss + 一位机器标识 + length长度循环递增字符串 + * + * @param atomicInt 序列数 + * @param length 数值长度 + * @return 序列值 + */ + public static String getId(AtomicInteger atomicInt, int length) { + String result = DateUtils.dateTimeNow(); + result += machineCode; + result += getSeq(atomicInt, length); + return result; + } + + /** + * 序列循环递增字符串[1, 10 的 (length)幂次方), 用0左补齐length位数 + * + * @return 序列值 + */ + private synchronized static String getSeq(AtomicInteger atomicInt, int length) { + // 先取值再+1 + int value = atomicInt.getAndIncrement(); + + // 如果更新后值>=10 的 (length)幂次方则重置为1 + int maxSeq = (int) Math.pow(10, length); + if (atomicInt.get() >= maxSeq) { + atomicInt.set(1); + } + // 转字符串,用0左补齐 + return StringUtils.padl(value, length); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/id/SnUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/id/SnUtils.java new file mode 100644 index 000000000..eb072840b --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/id/SnUtils.java @@ -0,0 +1,77 @@ +package com.jsowell.common.util.id; + +import com.google.common.collect.Lists; +import com.jsowell.common.constant.CacheConstants; +import com.jsowell.common.core.redis.RedisCache; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.util.List; + +/** + * 生成sn号Util + */ +@Component +public class SnUtils { + + private static Logger logger = LoggerFactory.getLogger(SnUtils.class); + + @Autowired + public RedisCache redisCache; + + private String prefix = "88"; + + /** + * 获取递增的序列号 + * 充电桩编号定义为14位: 固定位88 + 年份23 + 10位自增数字不足补0 + * @param prefix 生成序列号的前缀 + * @return + */ + private String getPileSn(String prefix) { + //序列号前缀加特定标识,如系统模块名之类的 防止重复 + String key = CacheConstants.PILE_SN_GENERATE_KEY + prefix; + String increResult = null; + try { + Long increNum = redisCache.increment(key, 1); + // 年份 + int year = LocalDate.now().getYear() - 2000; + //不足补位 + increResult = prefix + year + String.format("%1$010d", increNum); + } catch (Exception e) { + logger.error("获取序列号失败", e); + } + return increResult; + } + + + + /** + * 生成sn号 + * + * @return + */ + public List generateSN() { + return generateSN(1); + } + + /** + * 批量生成sn号 + * + * @param size + * @return + */ + public List generateSN(int size) { + List resultList = Lists.newArrayList(); + if (size <= 0) { + return resultList; + } + for (int i = 0; i < size; i++) { + resultList.add(getPileSn(prefix)); + } + return resultList; + } + +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/id/SnowflakeIdWorker.java b/jsowell-common/src/main/java/com/jsowell/common/util/id/SnowflakeIdWorker.java new file mode 100644 index 000000000..1ef5140c7 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/id/SnowflakeIdWorker.java @@ -0,0 +1,237 @@ +package com.jsowell.common.util.id; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.SystemUtils; + +import java.net.Inet4Address; +import java.net.UnknownHostException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Twitter_Snowflake
+ * SnowFlake的结构如下(每部分用-分开):
+ * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
+ * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0
+ * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截) + * 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69
+ * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId
+ * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号
+ * 加起来刚好64位,为一个Long型。
+ * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。 + */ +@Slf4j +public class SnowflakeIdWorker { + + // ==============================Fields=========================================== + + /** + * 机器id所占的位数 + */ + private final long workerIdBits = 5L; + + /** + * 数据标识id所占的位数 + */ + private final long dataCenterIdBits = 5L; + + /** + * 工作机器ID(0~31) + */ + private final long workerId; + + /** + * 数据中心ID(0~31) + */ + private final long dataCenterId; + + /** + * 毫秒内序列(0~4095) + */ + private long sequence = 0L; + + /** + * 上次生成ID的时间截 + */ + private long lastTimestamp = -1L; + + private static final SnowflakeIdWorker idWorker; + + static { + idWorker = new SnowflakeIdWorker(getWorkId(), getDataCenterId()); + } + + //==============================Constructors===================================== + + /** + * 构造函数 + * + * @param workerId 工作ID (0~31) + * @param dataCenterId 数据中心ID (0~31) + */ + public SnowflakeIdWorker(long workerId, long dataCenterId) { + /* + * 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) + */ + long maxWorkerId = ~(-1L << workerIdBits); + if (workerId > maxWorkerId || workerId < 0) { + throw new IllegalArgumentException(String.format("workerId can't be greater than %d or less than 0", maxWorkerId)); + } + /* + * 支持的最大数据标识id,结果是31 + */ + long maxDataCenterId = ~(-1L << dataCenterIdBits); + if (dataCenterId > maxDataCenterId || dataCenterId < 0) { + throw new IllegalArgumentException(String.format("dataCenterId can't be greater than %d or less than 0", maxDataCenterId)); + } + this.workerId = workerId; + this.dataCenterId = dataCenterId; + } + + // ==============================Methods========================================== + + /** + * 获得下一个ID (该方法是线程安全的) + * + * @return SnowflakeId + */ + public synchronized long nextId() { + long timestamp = timeGen(); + + //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常 + if (timestamp < lastTimestamp) { + throw new RuntimeException( + String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); + } + + //如果是同一时间生成的,则进行毫秒内序列 + /* + * 序列在id中占的位数 + */ + long sequenceBits = 12L; + if (lastTimestamp == timestamp) { + /* + * 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) + */ + long sequenceMask = ~(-1L << sequenceBits); + sequence = (sequence + 1) & sequenceMask; + //毫秒内序列溢出 + if (sequence == 0) { + //阻塞到下一个毫秒,获得新的时间戳 + timestamp = tilNextMillis(lastTimestamp); + } + } else { + //时间戳改变,毫秒内序列重置 + sequence = 0L; + } + + //上次生成ID的时间截 + lastTimestamp = timestamp; + + //移位并通过或运算拼到一起组成64位的ID + /* + * 时间截向左移22位(5+5+12) + */ + long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits; + /* + * 数据标识id向左移17位(12+5) + */ + long dataCenterIdShift = sequenceBits + workerIdBits; + /* + * 机器ID向左移12位 + */ + /* + * 开始时间截 (2015-01-01) + */ + long twepoch = 1489111610226L; + return ((timestamp - twepoch) << timestampLeftShift) + | (dataCenterId << dataCenterIdShift) + | (workerId << sequenceBits) + | sequence; + } + + /** + * 阻塞到下一个毫秒,直到获得新的时间戳 + * + * @param lastTimestamp 上次生成ID的时间截 + * @return 当前时间戳 + */ + protected long tilNextMillis(long lastTimestamp) { + long timestamp = timeGen(); + while (timestamp <= lastTimestamp) { + timestamp = timeGen(); + } + return timestamp; + } + + /** + * 返回以毫秒为单位的当前时间 + * + * @return 当前时间(毫秒) + */ + protected long timeGen() { + return System.currentTimeMillis(); + } + + private static Long getWorkId() { + try { + String hostAddress = Inet4Address.getLocalHost().getHostAddress(); + int[] ints = StringUtils.toCodePoints(hostAddress); + int sums = 0; + for (int b : ints) { + sums += b; + } + return (long) (sums % 32); + } catch (UnknownHostException e) { + // 如果获取失败,则使用随机数备用 + return RandomUtils.nextLong(0, 31); + } + } + + private static Long getDataCenterId() { + int[] ints = StringUtils.toCodePoints(SystemUtils.getHostName()); + int sums = 0; + for (int i : ints) { + sums += i; + } + return (long) (sums % 32); + } + + + /** + * 静态工具类 + * 生成18位id + */ + public static String getSnowflakeId() { + return String.valueOf(idWorker.nextId()); + } + + //==============================Test============================================= + + /** + * 测试 + */ + public static void main(String[] args) throws InterruptedException { + //判断生成的记录是否有重复记录 + long beginTime = System.currentTimeMillis(); + Map map = new ConcurrentHashMap<>(); + for (int i = 0; i < 100; i++) { + new Thread(() -> { + for (int s = 0; s < 20000; s++) { + String snowFlakeId = SnowflakeIdWorker.getSnowflakeId(); + if (map.containsKey(snowFlakeId)) { + log.error("主键重复:" + snowFlakeId); + } else { + map.put(snowFlakeId, snowFlakeId); + } + } + }).start(); + } + Thread.sleep(3000); + log.info("map.size():{}", map.size()); + log.info("耗时:" + (System.currentTimeMillis() - beginTime)); + } +} + diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/id/UUID.java b/jsowell-common/src/main/java/com/jsowell/common/util/id/UUID.java new file mode 100644 index 000000000..af53381b2 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/id/UUID.java @@ -0,0 +1,441 @@ +package com.jsowell.common.util.id; + +import com.jsowell.common.exception.UtilException; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +/** + * 提供通用唯一识别码(universally unique identifier)(UUID)实现 + * + * @author jsowell + */ +public final class UUID implements java.io.Serializable, Comparable { + private static final long serialVersionUID = -1185015143654744140L; + + /** + * SecureRandom 的单例 + */ + private static class Holder { + static final SecureRandom numberGenerator = getSecureRandom(); + } + + /** + * 此UUID的最高64有效位 + */ + private final long mostSigBits; + + /** + * 此UUID的最低64有效位 + */ + private final long leastSigBits; + + /** + * 私有构造 + * + * @param data 数据 + */ + private UUID(byte[] data) { + long msb = 0; + long lsb = 0; + assert data.length == 16 : "data must be 16 bytes in length"; + for (int i = 0; i < 8; i++) { + msb = (msb << 8) | (data[i] & 0xff); + } + for (int i = 8; i < 16; i++) { + lsb = (lsb << 8) | (data[i] & 0xff); + } + this.mostSigBits = msb; + this.leastSigBits = lsb; + } + + /** + * 使用指定的数据构造新的 UUID。 + * + * @param mostSigBits 用于 {@code UUID} 的最高有效 64 位 + * @param leastSigBits 用于 {@code UUID} 的最低有效 64 位 + */ + public UUID(long mostSigBits, long leastSigBits) { + this.mostSigBits = mostSigBits; + this.leastSigBits = leastSigBits; + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的本地线程伪随机数生成器生成该 UUID。 + * + * @return 随机生成的 {@code UUID} + */ + public static UUID fastUUID() { + return randomUUID(false); + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。 + * + * @return 随机生成的 {@code UUID} + */ + public static UUID randomUUID() { + return randomUUID(true); + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。 + * + * @param isSecure 是否使用{@link SecureRandom}如果是可以获得更安全的随机码,否则可以得到更好的性能 + * @return 随机生成的 {@code UUID} + */ + public static UUID randomUUID(boolean isSecure) { + final Random ng = isSecure ? Holder.numberGenerator : getRandom(); + + byte[] randomBytes = new byte[16]; + ng.nextBytes(randomBytes); + randomBytes[6] &= 0x0f; /* clear version */ + randomBytes[6] |= 0x40; /* set to version 4 */ + randomBytes[8] &= 0x3f; /* clear variant */ + randomBytes[8] |= 0x80; /* set to IETF variant */ + return new UUID(randomBytes); + } + + /** + * 根据指定的字节数组获取类型 3(基于名称的)UUID 的静态工厂。 + * + * @param name 用于构造 UUID 的字节数组。 + * @return 根据指定数组生成的 {@code UUID} + */ + public static UUID nameUUIDFromBytes(byte[] name) { + MessageDigest md; + try { + md = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException nsae) { + throw new InternalError("MD5 not supported"); + } + byte[] md5Bytes = md.digest(name); + md5Bytes[6] &= 0x0f; /* clear version */ + md5Bytes[6] |= 0x30; /* set to version 3 */ + md5Bytes[8] &= 0x3f; /* clear variant */ + md5Bytes[8] |= 0x80; /* set to IETF variant */ + return new UUID(md5Bytes); + } + + /** + * 根据 {@link #toString()} 方法中描述的字符串标准表示形式创建{@code UUID}。 + * + * @param name 指定 {@code UUID} 字符串 + * @return 具有指定值的 {@code UUID} + * @throws IllegalArgumentException 如果 name 与 {@link #toString} 中描述的字符串表示形式不符抛出此异常 + */ + public static UUID fromString(String name) { + String[] components = name.split("-"); + if (components.length != 5) { + throw new IllegalArgumentException("Invalid UUID string: " + name); + } + for (int i = 0; i < 5; i++) { + components[i] = "0x" + components[i]; + } + + long mostSigBits = Long.decode(components[0]).longValue(); + mostSigBits <<= 16; + mostSigBits |= Long.decode(components[1]).longValue(); + mostSigBits <<= 16; + mostSigBits |= Long.decode(components[2]).longValue(); + + long leastSigBits = Long.decode(components[3]).longValue(); + leastSigBits <<= 48; + leastSigBits |= Long.decode(components[4]).longValue(); + + return new UUID(mostSigBits, leastSigBits); + } + + /** + * 返回此 UUID 的 128 位值中的最低有效 64 位。 + * + * @return 此 UUID 的 128 位值中的最低有效 64 位。 + */ + public long getLeastSignificantBits() { + return leastSigBits; + } + + /** + * 返回此 UUID 的 128 位值中的最高有效 64 位。 + * + * @return 此 UUID 的 128 位值中最高有效 64 位。 + */ + public long getMostSignificantBits() { + return mostSigBits; + } + + /** + * 与此 {@code UUID} 相关联的版本号. 版本号描述此 {@code UUID} 是如何生成的。 + *

+ * 版本号具有以下含意: + *

    + *
  • 1 基于时间的 UUID + *
  • 2 DCE 安全 UUID + *
  • 3 基于名称的 UUID + *
  • 4 随机生成的 UUID + *
+ * + * @return 此 {@code UUID} 的版本号 + */ + public int version() { + // Version is bits masked by 0x000000000000F000 in MS long + return (int) ((mostSigBits >> 12) & 0x0f); + } + + /** + * 与此 {@code UUID} 相关联的变体号。变体号描述 {@code UUID} 的布局。 + *

+ * 变体号具有以下含意: + *

    + *
  • 0 为 NCS 向后兼容保留 + *
  • 2 IETF RFC 4122(Leach-Salz), 用于此类 + *
  • 6 保留,微软向后兼容 + *
  • 7 保留供以后定义使用 + *
+ * + * @return 此 {@code UUID} 相关联的变体号 + */ + public int variant() { + // This field is composed of a varying number of bits. + // 0 - - Reserved for NCS backward compatibility + // 1 0 - The IETF aka Leach-Salz variant (used by this class) + // 1 1 0 Reserved, Microsoft backward compatibility + // 1 1 1 Reserved for future definition. + return (int) ((leastSigBits >>> (64 - (leastSigBits >>> 62))) & (leastSigBits >> 63)); + } + + /** + * 与此 UUID 相关联的时间戳值。 + * + *

+ * 60 位的时间戳值根据此 {@code UUID} 的 time_low、time_mid 和 time_hi 字段构造。
+ * 所得到的时间戳以 100 毫微秒为单位,从 UTC(通用协调时间) 1582 年 10 月 15 日零时开始。 + * + *

+ * 时间戳值仅在在基于时间的 UUID(其 version 类型为 1)中才有意义。
+ * 如果此 {@code UUID} 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。 + * + * @throws UnsupportedOperationException 如果此 {@code UUID} 不是 version 为 1 的 UUID。 + */ + public long timestamp() throws UnsupportedOperationException { + checkTimeBase(); + return (mostSigBits & 0x0FFFL) << 48// + | ((mostSigBits >> 16) & 0x0FFFFL) << 32// + | mostSigBits >>> 32; + } + + /** + * 与此 UUID 相关联的时钟序列值。 + * + *

+ * 14 位的时钟序列值根据此 UUID 的 clock_seq 字段构造。clock_seq 字段用于保证在基于时间的 UUID 中的时间唯一性。 + *

+ * {@code clockSequence} 值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。 如果此 UUID 不是基于时间的 UUID,则此方法抛出 + * UnsupportedOperationException。 + * + * @return 此 {@code UUID} 的时钟序列 + * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1 + */ + public int clockSequence() throws UnsupportedOperationException { + checkTimeBase(); + return (int) ((leastSigBits & 0x3FFF000000000000L) >>> 48); + } + + /** + * 与此 UUID 相关的节点值。 + * + *

+ * 48 位的节点值根据此 UUID 的 node 字段构造。此字段旨在用于保存机器的 IEEE 802 地址,该地址用于生成此 UUID 以保证空间唯一性。 + *

+ * 节点值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。
+ * 如果此 UUID 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。 + * + * @return 此 {@code UUID} 的节点值 + * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1 + */ + public long node() throws UnsupportedOperationException { + checkTimeBase(); + return leastSigBits & 0x0000FFFFFFFFFFFFL; + } + + /** + * 返回此{@code UUID} 的字符串表现形式。 + * + *

+ * UUID 的字符串表示形式由此 BNF 描述: + * + *

+	 * {@code
+	 * UUID                   = ----
+	 * time_low               = 4*
+	 * time_mid               = 2*
+	 * time_high_and_version  = 2*
+	 * variant_and_sequence   = 2*
+	 * node                   = 6*
+	 * hexOctet               = 
+	 * hexDigit               = [0-9a-fA-F]
+	 * }
+	 * 
+ * + * + * + * @return 此{@code UUID} 的字符串表现形式 + * @see #toString(boolean) + */ + @Override + public String toString() { + return toString(false); + } + + /** + * 返回此{@code UUID} 的字符串表现形式。 + * + *

+ * UUID 的字符串表示形式由此 BNF 描述: + * + *

+	 * {@code
+	 * UUID                   = ----
+	 * time_low               = 4*
+	 * time_mid               = 2*
+	 * time_high_and_version  = 2*
+	 * variant_and_sequence   = 2*
+	 * node                   = 6*
+	 * hexOctet               = 
+	 * hexDigit               = [0-9a-fA-F]
+	 * }
+	 * 
+ * + * + * + * @param isSimple 是否简单模式,简单模式为不带'-'的UUID字符串 + * @return 此{@code UUID} 的字符串表现形式 + */ + public String toString(boolean isSimple) { + final StringBuilder builder = new StringBuilder(isSimple ? 32 : 36); + // time_low + builder.append(digits(mostSigBits >> 32, 8)); + if (!isSimple) { + builder.append('-'); + } + // time_mid + builder.append(digits(mostSigBits >> 16, 4)); + if (!isSimple) { + builder.append('-'); + } + // time_high_and_version + builder.append(digits(mostSigBits, 4)); + if (!isSimple) { + builder.append('-'); + } + // variant_and_sequence + builder.append(digits(leastSigBits >> 48, 4)); + if (!isSimple) { + builder.append('-'); + } + // node + builder.append(digits(leastSigBits, 12)); + + return builder.toString(); + } + + /** + * 返回此 UUID 的哈希码。 + * + * @return UUID 的哈希码值。 + */ + @Override + public int hashCode() { + long hilo = mostSigBits ^ leastSigBits; + return ((int) (hilo >> 32)) ^ (int) hilo; + } + + /** + * 将此对象与指定对象比较。 + *

+ * 当且仅当参数不为 {@code null}、而是一个 UUID 对象、具有与此 UUID 相同的 varriant、包含相同的值(每一位均相同)时,结果才为 {@code true}。 + * + * @param obj 要与之比较的对象 + * @return 如果对象相同,则返回 {@code true};否则返回 {@code false} + */ + @Override + public boolean equals(Object obj) { + if ((null == obj) || (obj.getClass() != UUID.class)) { + return false; + } + UUID id = (UUID) obj; + return (mostSigBits == id.mostSigBits && leastSigBits == id.leastSigBits); + } + + // Comparison Operations + + /** + * 将此 UUID 与指定的 UUID 比较。 + * + *

+ * 如果两个 UUID 不同,且第一个 UUID 的最高有效字段大于第二个 UUID 的对应字段,则第一个 UUID 大于第二个 UUID。 + * + * @param val 与此 UUID 比较的 UUID + * @return 在此 UUID 小于、等于或大于 val 时,分别返回 -1、0 或 1。 + */ + @Override + public int compareTo(UUID val) { + // The ordering is intentionally set up so that the UUIDs + // can simply be numerically compared as two numbers + return (this.mostSigBits < val.mostSigBits ? -1 : // + (this.mostSigBits > val.mostSigBits ? 1 : // + (this.leastSigBits < val.leastSigBits ? -1 : // + (this.leastSigBits > val.leastSigBits ? 1 : // + 0)))); + } + + // ------------------------------------------------------------------------------------------------------------------- + // Private method start + + /** + * 返回指定数字对应的hex值 + * + * @param val 值 + * @param digits 位 + * @return 值 + */ + private static String digits(long val, int digits) { + long hi = 1L << (digits * 4); + return Long.toHexString(hi | (val & (hi - 1))).substring(1); + } + + /** + * 检查是否为time-based版本UUID + */ + private void checkTimeBase() { + if (version() != 1) { + throw new UnsupportedOperationException("Not a time-based UUID"); + } + } + + /** + * 获取{@link SecureRandom},类提供加密的强随机数生成器 (RNG) + * + * @return {@link SecureRandom} + */ + public static SecureRandom getSecureRandom() { + try { + return SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + throw new UtilException(e); + } + } + + /** + * 获取随机数生成器对象
+ * ThreadLocalRandom是JDK 7之后提供并发产生随机数,能够解决多个线程发生的竞争争夺。 + * + * @return {@link ThreadLocalRandom} + */ + public static ThreadLocalRandom getRandom() { + return ThreadLocalRandom.current(); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/ip/AddressUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/ip/AddressUtils.java new file mode 100644 index 000000000..d35b3cb4b --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/ip/AddressUtils.java @@ -0,0 +1,93 @@ +package com.jsowell.common.util.ip; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.google.common.collect.Maps; +import com.jsowell.common.config.JsowellConfig; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.http.HttpUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; + +/** + * 获取地址类 + * + * @author jsowell + */ +public class AddressUtils { + private static final Logger log = LoggerFactory.getLogger(AddressUtils.class); + + // IP地址查询 + public static final String IP_URL = "http://whois.pconline.com.cn/ipJson.jsp"; + + // 高德查询地址api + public static final String AMAP_GET_LOCATION_URL = "https://restapi.amap.com/v3/geocode/geo"; + + // 高德应用key + private static final String AMAP_GET_LOCATION_KEY = "51e155ceb3ab2d4f3b6bd81ebaab20b3"; + + // 未知地址 + public static final String UNKNOWN = "XX XX"; + + public static String getRealAddressByIP(String ip) { + // 内网不查询 + if (IpUtils.internalIp(ip)) { + return "内网IP"; + } + if (JsowellConfig.isAddressEnabled()) { + try { + String rspStr = HttpUtils.sendGet(IP_URL, "ip=" + ip + "&json=true", Constants.GBK); + if (StringUtils.isEmpty(rspStr)) { + log.error("获取地理位置异常 {}", ip); + return UNKNOWN; + } + JSONObject obj = JSON.parseObject(rspStr); + String region = obj.getString("pro"); + String city = obj.getString("city"); + return String.format("%s %s", region, city); + } catch (Exception e) { + log.error("获取地理位置异常 {}", ip); + } + } + return UNKNOWN; + } + + /** + * 地址转换经纬度 + * areaCode和address必传参数,任意一个为空则返回null + * 高德api地址 https://lbs.amap.com/api/webservice/guide/api/georegeo + * + * @param areaCode 前端地址选择器获取到的区域编码 例如:"320000,320500,320583" + * @param address 详细地址 + * @return 逗号分割的经纬度字符串 + */ + public static Map getLongitudeAndLatitude(String areaCode, String address) { + if (StringUtils.isBlank(areaCode) || StringUtils.isBlank(address)) { + return null; + } + String[] split = StringUtils.split(areaCode, ","); + String param = "key=" + AMAP_GET_LOCATION_KEY + "&city=" + split[2] + "&address=" + address; + String resultStr = HttpUtils.sendGet(AMAP_GET_LOCATION_URL, param, Constants.UTF8); + if (StringUtils.isNotBlank(resultStr)) { + JSONObject resultObj = JSON.parseObject(resultStr); + // 状态为1 查询成功 + if (StringUtils.equals(resultObj.getString("status"), Constants.ONE)) { + JSONArray geocodes = resultObj.getJSONArray("geocodes"); + if (geocodes != null && geocodes.size() > 0) { + Map map = Maps.newHashMap(); + JSONObject jsonObject = geocodes.getJSONObject(0); + String location = jsonObject.getString("location"); + String[] strArr = StringUtils.split(location, ","); + map.put("lng", strArr[0]); + map.put("lat", strArr[1]); + return map; + } + } + } + return null; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/ip/IpUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/ip/IpUtils.java new file mode 100644 index 000000000..b3fcdb8c8 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/ip/IpUtils.java @@ -0,0 +1,224 @@ +package com.jsowell.common.util.ip; + +import com.jsowell.common.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * 获取IP方法 + * + * @author jsowell + */ +public class IpUtils { + /** + * 获取客户端IP + * + * @param request 请求对象 + * @return IP地址 + */ + public static String getIpAddr(HttpServletRequest request) { + if (request == null) { + return "unknown"; + } + String ip = request.getHeader("x-forwarded-for"); + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("X-Forwarded-For"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("X-Real-IP"); + } + + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + + return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip); + } + + /** + * 检查是否为内部IP地址 + * + * @param ip IP地址 + * @return 结果 + */ + public static boolean internalIp(String ip) { + byte[] addr = textToNumericFormatV4(ip); + return internalIp(addr) || "127.0.0.1".equals(ip); + } + + /** + * 检查是否为内部IP地址 + * + * @param addr byte地址 + * @return 结果 + */ + private static boolean internalIp(byte[] addr) { + if (StringUtils.isNull(addr) || addr.length < 2) { + return true; + } + final byte b0 = addr[0]; + final byte b1 = addr[1]; + // 10.x.x.x/8 + final byte SECTION_1 = 0x0A; + // 172.16.x.x/12 + final byte SECTION_2 = (byte) 0xAC; + final byte SECTION_3 = (byte) 0x10; + final byte SECTION_4 = (byte) 0x1F; + // 192.168.x.x/16 + final byte SECTION_5 = (byte) 0xC0; + final byte SECTION_6 = (byte) 0xA8; + switch (b0) { + case SECTION_1: + return true; + case SECTION_2: + if (b1 >= SECTION_3 && b1 <= SECTION_4) { + return true; + } + case SECTION_5: + switch (b1) { + case SECTION_6: + return true; + } + default: + return false; + } + } + + /** + * 将IPv4地址转换成字节 + * + * @param text IPv4地址 + * @return byte 字节 + */ + public static byte[] textToNumericFormatV4(String text) { + if (text.length() == 0) { + return null; + } + + byte[] bytes = new byte[4]; + String[] elements = text.split("\\.", -1); + try { + long l; + int i; + switch (elements.length) { + case 1: + l = Long.parseLong(elements[0]); + if ((l < 0L) || (l > 4294967295L)) { + return null; + } + bytes[0] = (byte) (int) (l >> 24 & 0xFF); + bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF); + bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 2: + l = Integer.parseInt(elements[0]); + if ((l < 0L) || (l > 255L)) { + return null; + } + bytes[0] = (byte) (int) (l & 0xFF); + l = Integer.parseInt(elements[1]); + if ((l < 0L) || (l > 16777215L)) { + return null; + } + bytes[1] = (byte) (int) (l >> 16 & 0xFF); + bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 3: + for (i = 0; i < 2; ++i) { + l = Integer.parseInt(elements[i]); + if ((l < 0L) || (l > 255L)) { + return null; + } + bytes[i] = (byte) (int) (l & 0xFF); + } + l = Integer.parseInt(elements[2]); + if ((l < 0L) || (l > 65535L)) { + return null; + } + bytes[2] = (byte) (int) (l >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 4: + for (i = 0; i < 4; ++i) { + l = Integer.parseInt(elements[i]); + if ((l < 0L) || (l > 255L)) { + return null; + } + bytes[i] = (byte) (int) (l & 0xFF); + } + break; + default: + return null; + } + } catch (NumberFormatException e) { + return null; + } + return bytes; + } + + /** + * 获取IP地址 + * + * @return 本地IP地址 + */ + public static String getHostIp() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + } + return "127.0.0.1"; + } + + /** + * 获取主机名 + * + * @return 本地主机名 + */ + public static String getHostName() { + try { + return InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + } + return "未知"; + } + + /** + * 从多级反向代理中获得第一个非unknown IP地址 + * + * @param ip 获得的IP地址 + * @return 第一个非unknown IP地址 + */ + public static String getMultistageReverseProxyIp(String ip) { + // 多级反向代理检测 + if (ip != null && ip.indexOf(",") > 0) { + final String[] ips = ip.trim().split(","); + for (String subIp : ips) { + if (false == isUnknown(subIp)) { + ip = subIp; + break; + } + } + } + return ip; + } + + /** + * 检测给定字符串是否为未知,多用于检测HTTP请求相关 + * + * @param checkString 被检测的字符串 + * @return 是否未知 + */ + public static boolean isUnknown(String checkString) { + return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString); + } +} \ No newline at end of file diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/poi/ExcelHandlerAdapter.java b/jsowell-common/src/main/java/com/jsowell/common/util/poi/ExcelHandlerAdapter.java new file mode 100644 index 000000000..e43bec4d0 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/poi/ExcelHandlerAdapter.java @@ -0,0 +1,17 @@ +package com.jsowell.common.util.poi; + +/** + * Excel数据格式处理适配器 + * + * @author jsowell + */ +public interface ExcelHandlerAdapter { + /** + * 格式化 + * + * @param value 单元格数据值 + * @param args excel注解args参数组 + * @return 处理后的值 + */ + Object format(Object value, String[] args); +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/poi/ExcelUtil.java b/jsowell-common/src/main/java/com/jsowell/common/util/poi/ExcelUtil.java new file mode 100644 index 000000000..f1d63f2fe --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/poi/ExcelUtil.java @@ -0,0 +1,1355 @@ +package com.jsowell.common.util.poi; + +import com.jsowell.common.annotation.Excel; +import com.jsowell.common.annotation.Excel.ColumnType; +import com.jsowell.common.annotation.Excel.Type; +import com.jsowell.common.annotation.Excels; +import com.jsowell.common.config.JsowellConfig; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.text.Convert; +import com.jsowell.common.exception.UtilException; +import com.jsowell.common.util.DateUtils; +import com.jsowell.common.util.DictUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.file.FileTypeUtils; +import com.jsowell.common.util.file.FileUtils; +import com.jsowell.common.util.file.ImageUtils; +import com.jsowell.common.util.reflect.ReflectUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.RegExUtils; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.poi.hssf.usermodel.*; +import org.apache.poi.ooxml.POIXMLDocumentPart; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.ss.util.CellRangeAddressList; +import org.apache.poi.util.IOUtils; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; +import org.apache.poi.xssf.usermodel.*; +import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTMarker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Excel相关处理 + * + * @author jsowell + */ +public class ExcelUtil { + private static final Logger log = LoggerFactory.getLogger(ExcelUtil.class); + + public static final String FORMULA_REGEX_STR = "=|-|\\+|@"; + + public static final String[] FORMULA_STR = {"=", "-", "+", "@"}; + + /** + * Excel sheet最大行数,默认65536 + */ + public static final int sheetSize = 65536; + + /** + * 工作表名称 + */ + private String sheetName; + + /** + * 导出类型(EXPORT:导出数据;IMPORT:导入模板) + */ + private Type type; + + /** + * 工作薄对象 + */ + private Workbook wb; + + /** + * 工作表对象 + */ + private Sheet sheet; + + /** + * 样式列表 + */ + private Map styles; + + /** + * 导入导出数据列表 + */ + private List list; + + /** + * 注解列表 + */ + private List fields; + + /** + * 当前行号 + */ + private int rownum; + + /** + * 标题 + */ + private String title; + + /** + * 最大高度 + */ + private short maxHeight; + + /** + * 合并后最后行数 + */ + private int subMergedLastRowNum = 0; + + /** + * 合并后开始行数 + */ + private int subMergedFirstRowNum = 1; + + /** + * 对象的子列表方法 + */ + private Method subMethod; + + /** + * 对象的子列表属性 + */ + private List subFields; + + /** + * 统计列表 + */ + private Map statistics = new HashMap(); + + /** + * 数字格式 + */ + private static final DecimalFormat DOUBLE_FORMAT = new DecimalFormat("######0.00"); + + /** + * 实体对象 + */ + public Class clazz; + + /** + * 需要排除列属性 + */ + public String[] excludeFields; + + public ExcelUtil(Class clazz) { + this.clazz = clazz; + } + + /** + * 隐藏Excel中列属性 + * + * @param fields 列属性名 示例[单个"name"/多个"id","name"] + * @throws Exception + */ + public void hideColumn(String... fields) { + this.excludeFields = fields; + } + + public void init(List list, String sheetName, String title, Type type) { + if (list == null) { + list = new ArrayList(); + } + this.list = list; + this.sheetName = sheetName; + this.type = type; + this.title = title; + createExcelField(); + createWorkbook(); + createTitle(); + createSubHead(); + } + + /** + * 创建excel第一行标题 + */ + public void createTitle() { + if (StringUtils.isNotEmpty(title)) { + subMergedFirstRowNum++; + subMergedLastRowNum++; + int titleLastCol = this.fields.size() - 1; + if (isSubList()) { + titleLastCol = titleLastCol + subFields.size() - 1; + } + Row titleRow = sheet.createRow(rownum == 0 ? rownum++ : 0); + titleRow.setHeightInPoints(30); + Cell titleCell = titleRow.createCell(0); + titleCell.setCellStyle(styles.get("title")); + titleCell.setCellValue(title); + sheet.addMergedRegion(new CellRangeAddress(titleRow.getRowNum(), titleRow.getRowNum(), titleRow.getRowNum(), titleLastCol)); + } + } + + /** + * 创建对象的子列表名称 + */ + public void createSubHead() { + if (isSubList()) { + subMergedFirstRowNum++; + subMergedLastRowNum++; + Row subRow = sheet.createRow(rownum); + int excelNum = 0; + for (Object[] objects : fields) { + Excel attr = (Excel) objects[1]; + Cell headCell1 = subRow.createCell(excelNum); + headCell1.setCellValue(attr.name()); + headCell1.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor()))); + excelNum++; + } + int headFirstRow = excelNum - 1; + int headLastRow = headFirstRow + subFields.size() - 1; + if (headLastRow > headFirstRow) { + sheet.addMergedRegion(new CellRangeAddress(rownum, rownum, headFirstRow, headLastRow)); + } + rownum++; + } + } + + /** + * 对excel表单默认第一个索引名转换成list + * + * @param is 输入流 + * @return 转换后集合 + */ + public List importExcel(InputStream is) throws Exception { + return importExcel(is, 0); + } + + /** + * 对excel表单默认第一个索引名转换成list + * + * @param is 输入流 + * @param titleNum 标题占用行数 + * @return 转换后集合 + */ + public List importExcel(InputStream is, int titleNum) throws Exception { + return importExcel(StringUtils.EMPTY, is, titleNum); + } + + /** + * 对excel表单指定表格索引名转换成list + * + * @param sheetName 表格索引名 + * @param titleNum 标题占用行数 + * @param is 输入流 + * @return 转换后集合 + */ + public List importExcel(String sheetName, InputStream is, int titleNum) throws Exception { + this.type = Type.IMPORT; + this.wb = WorkbookFactory.create(is); + List list = new ArrayList(); + // 如果指定sheet名,则取指定sheet中的内容 否则默认指向第1个sheet + Sheet sheet = StringUtils.isNotEmpty(sheetName) ? wb.getSheet(sheetName) : wb.getSheetAt(0); + if (sheet == null) { + throw new IOException("文件sheet不存在"); + } + boolean isXSSFWorkbook = !(wb instanceof HSSFWorkbook); + Map pictures; + if (isXSSFWorkbook) { + pictures = getSheetPictures07((XSSFSheet) sheet, (XSSFWorkbook) wb); + } else { + pictures = getSheetPictures03((HSSFSheet) sheet, (HSSFWorkbook) wb); + } + // 获取最后一个非空行的行下标,比如总行数为n,则返回的为n-1 + int rows = sheet.getLastRowNum(); + + if (rows > 0) { + // 定义一个map用于存放excel列的序号和field. + Map cellMap = new HashMap(); + // 获取表头 + Row heard = sheet.getRow(titleNum); + for (int i = 0; i < heard.getPhysicalNumberOfCells(); i++) { + Cell cell = heard.getCell(i); + if (StringUtils.isNotNull(cell)) { + String value = this.getCellValue(heard, i).toString(); + cellMap.put(value, i); + } else { + cellMap.put(null, i); + } + } + // 有数据时才处理 得到类的所有field. + List fields = this.getFields(); + Map fieldsMap = new HashMap(); + for (Object[] objects : fields) { + Excel attr = (Excel) objects[1]; + Integer column = cellMap.get(attr.name()); + if (column != null) { + fieldsMap.put(column, objects); + } + } + for (int i = titleNum + 1; i <= rows; i++) { + // 从第2行开始取数据,默认第一行是表头. + Row row = sheet.getRow(i); + // 判断当前行是否是空行 + if (isRowEmpty(row)) { + continue; + } + T entity = null; + for (Map.Entry entry : fieldsMap.entrySet()) { + Object val = this.getCellValue(row, entry.getKey()); + + // 如果不存在实例则新建. + entity = (entity == null ? clazz.newInstance() : entity); + // 从map中得到对应列的field. + Field field = (Field) entry.getValue()[0]; + Excel attr = (Excel) entry.getValue()[1]; + // 取得类型,并根据对象类型设置值. + Class fieldType = field.getType(); + if (String.class == fieldType) { + String s = Convert.toStr(val); + if (StringUtils.endsWith(s, ".0")) { + val = StringUtils.substringBefore(s, ".0"); + } else { + String dateFormat = field.getAnnotation(Excel.class).dateFormat(); + if (StringUtils.isNotEmpty(dateFormat)) { + val = parseDateToStr(dateFormat, val); + } else { + val = Convert.toStr(val); + } + } + } else if ((Integer.TYPE == fieldType || Integer.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val))) { + val = Convert.toInt(val); + } else if ((Long.TYPE == fieldType || Long.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val))) { + val = Convert.toLong(val); + } else if (Double.TYPE == fieldType || Double.class == fieldType) { + val = Convert.toDouble(val); + } else if (Float.TYPE == fieldType || Float.class == fieldType) { + val = Convert.toFloat(val); + } else if (BigDecimal.class == fieldType) { + val = Convert.toBigDecimal(val); + } else if (Date.class == fieldType) { + if (val instanceof String) { + val = DateUtils.parseDate(val); + } else if (val instanceof Double) { + val = DateUtil.getJavaDate((Double) val); + } + } else if (Boolean.TYPE == fieldType || Boolean.class == fieldType) { + val = Convert.toBool(val, false); + } + if (StringUtils.isNotNull(fieldType)) { + String propertyName = field.getName(); + if (StringUtils.isNotEmpty(attr.targetAttr())) { + propertyName = field.getName() + "." + attr.targetAttr(); + } else if (StringUtils.isNotEmpty(attr.readConverterExp())) { + val = reverseByExp(Convert.toStr(val), attr.readConverterExp(), attr.separator()); + } else if (StringUtils.isNotEmpty(attr.dictType())) { + val = reverseDictByExp(Convert.toStr(val), attr.dictType(), attr.separator()); + } else if (!attr.handler().equals(ExcelHandlerAdapter.class)) { + val = dataFormatHandlerAdapter(val, attr); + } else if (ColumnType.IMAGE == attr.cellType() && StringUtils.isNotEmpty(pictures)) { + PictureData image = pictures.get(row.getRowNum() + "_" + entry.getKey()); + if (image == null) { + val = ""; + } else { + byte[] data = image.getData(); + val = FileUtils.writeImportBytes(data); + } + } + ReflectUtils.invokeSetter(entity, propertyName, val); + } + } + list.add(entity); + } + } + return list; + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @return 结果 + */ + public AjaxResult exportExcel(List list, String sheetName) { + return exportExcel(list, sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public AjaxResult exportExcel(List list, String sheetName, String title) { + this.init(list, sheetName, title, Type.EXPORT); + return exportExcel(); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param response 返回数据 + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @return 结果 + */ + public void exportExcel(HttpServletResponse response, List list, String sheetName) { + exportExcel(response, list, sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param response 返回数据 + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public void exportExcel(HttpServletResponse response, List list, String sheetName, String title) { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + this.init(list, sheetName, title, Type.EXPORT); + exportExcel(response); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @return 结果 + */ + public AjaxResult importTemplateExcel(String sheetName) { + return importTemplateExcel(sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public AjaxResult importTemplateExcel(String sheetName, String title) { + this.init(null, sheetName, title, Type.IMPORT); + return exportExcel(); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @return 结果 + */ + public void importTemplateExcel(HttpServletResponse response, String sheetName) { + importTemplateExcel(response, sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public void importTemplateExcel(HttpServletResponse response, String sheetName, String title) { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + this.init(null, sheetName, title, Type.IMPORT); + exportExcel(response); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @return 结果 + */ + public void exportExcel(HttpServletResponse response) { + try { + writeSheet(); + wb.write(response.getOutputStream()); + } catch (Exception e) { + log.error("导出Excel异常{}", e.getMessage()); + } finally { + IOUtils.closeQuietly(wb); + } + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @return 结果 + */ + public AjaxResult exportExcel() { + OutputStream out = null; + try { + writeSheet(); + String filename = encodingFilename(sheetName); + out = new FileOutputStream(getAbsoluteFile(filename)); + wb.write(out); + return AjaxResult.success(filename); + } catch (Exception e) { + log.error("导出Excel异常{}", e.getMessage()); + throw new UtilException("导出Excel失败,请联系网站管理员!"); + } finally { + IOUtils.closeQuietly(wb); + IOUtils.closeQuietly(out); + } + } + + /** + * 创建写入数据到Sheet + */ + public void writeSheet() { + // 取出一共有多少个sheet. + int sheetNo = Math.max(1, (int) Math.ceil(list.size() * 1.0 / sheetSize)); + for (int index = 0; index < sheetNo; index++) { + createSheet(sheetNo, index); + + // 产生一行 + Row row = sheet.createRow(rownum); + int column = 0; + // 写入各个字段的列头名称 + for (Object[] os : fields) { + Field field = (Field) os[0]; + Excel excel = (Excel) os[1]; + if (Collection.class.isAssignableFrom(field.getType())) { + for (Field subField : subFields) { + Excel subExcel = subField.getAnnotation(Excel.class); + this.createHeadCell(subExcel, row, column++); + } + } else { + this.createHeadCell(excel, row, column++); + } + } + if (Type.EXPORT.equals(type)) { + fillExcelData(index, row); + addStatisticsRow(); + } + } + } + + /** + * 填充excel数据 + * + * @param index 序号 + * @param row 单元格行 + */ + @SuppressWarnings("unchecked") + public void fillExcelData(int index, Row row) { + int startNo = index * sheetSize; + int endNo = Math.min(startNo + sheetSize, list.size()); + int rowNo = (1 + rownum) - startNo; + for (int i = startNo; i < endNo; i++) { + rowNo = i > 1 ? rowNo + 1 : rowNo + i; + row = sheet.createRow(rowNo); + // 得到导出对象. + T vo = (T) list.get(i); + Collection subList = null; + if (isSubListValue(vo)) { + subList = getListCellValue(vo); + subMergedLastRowNum = subMergedLastRowNum + subList.size(); + } + + int column = 0; + for (Object[] os : fields) { + Field field = (Field) os[0]; + Excel excel = (Excel) os[1]; + if (Collection.class.isAssignableFrom(field.getType()) && StringUtils.isNotNull(subList)) { + boolean subFirst = false; + for (Object obj : subList) { + if (subFirst) { + rowNo++; + row = sheet.createRow(rowNo); + } + List subFields = FieldUtils.getFieldsListWithAnnotation(obj.getClass(), Excel.class); + int subIndex = 0; + for (Field subField : subFields) { + if (subField.isAnnotationPresent(Excel.class)) { + subField.setAccessible(true); + Excel attr = subField.getAnnotation(Excel.class); + this.addCell(attr, row, (T) obj, subField, column + subIndex); + } + subIndex++; + } + subFirst = true; + } + this.subMergedFirstRowNum = this.subMergedFirstRowNum + subList.size(); + } else { + this.addCell(excel, row, vo, field, column++); + } + } + } + } + + /** + * 创建表格样式 + * + * @param wb 工作薄对象 + * @return 样式列表 + */ + private Map createStyles(Workbook wb) { + // 写入各条记录,每条记录对应excel表中的一行 + Map styles = new HashMap(); + CellStyle style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + Font titleFont = wb.createFont(); + titleFont.setFontName("Arial"); + titleFont.setFontHeightInPoints((short) 16); + titleFont.setBold(true); + style.setFont(titleFont); + styles.put("title", style); + + style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setBorderRight(BorderStyle.THIN); + style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderLeft(BorderStyle.THIN); + style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderTop(BorderStyle.THIN); + style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderBottom(BorderStyle.THIN); + style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + Font dataFont = wb.createFont(); + dataFont.setFontName("Arial"); + dataFont.setFontHeightInPoints((short) 10); + style.setFont(dataFont); + styles.put("data", style); + + style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + Font totalFont = wb.createFont(); + totalFont.setFontName("Arial"); + totalFont.setFontHeightInPoints((short) 10); + style.setFont(totalFont); + styles.put("total", style); + + styles.putAll(annotationHeaderStyles(wb, styles)); + + styles.putAll(annotationDataStyles(wb)); + + return styles; + } + + /** + * 根据Excel注解创建表格头样式 + * + * @param wb 工作薄对象 + * @return 自定义样式列表 + */ + private Map annotationHeaderStyles(Workbook wb, Map styles) { + Map headerStyles = new HashMap(); + for (Object[] os : fields) { + Excel excel = (Excel) os[1]; + String key = StringUtils.format("header_{}_{}", excel.headerColor(), excel.headerBackgroundColor()); + if (!headerStyles.containsKey(key)) { + CellStyle style = wb.createCellStyle(); + style = wb.createCellStyle(); + style.cloneStyleFrom(styles.get("data")); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setFillForegroundColor(excel.headerBackgroundColor().index); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + Font headerFont = wb.createFont(); + headerFont.setFontName("Arial"); + headerFont.setFontHeightInPoints((short) 10); + headerFont.setBold(true); + headerFont.setColor(excel.headerColor().index); + style.setFont(headerFont); + headerStyles.put(key, style); + } + } + return headerStyles; + } + + /** + * 根据Excel注解创建表格列样式 + * + * @param wb 工作薄对象 + * @return 自定义样式列表 + */ + private Map annotationDataStyles(Workbook wb) { + Map styles = new HashMap(); + for (Object[] os : fields) { + Excel excel = (Excel) os[1]; + String key = StringUtils.format("data_{}_{}_{}", excel.align(), excel.color(), excel.backgroundColor()); + if (!styles.containsKey(key)) { + CellStyle style = wb.createCellStyle(); + style = wb.createCellStyle(); + style.setAlignment(excel.align()); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setBorderRight(BorderStyle.THIN); + style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderLeft(BorderStyle.THIN); + style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderTop(BorderStyle.THIN); + style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderBottom(BorderStyle.THIN); + style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + style.setFillForegroundColor(excel.backgroundColor().getIndex()); + Font dataFont = wb.createFont(); + dataFont.setFontName("Arial"); + dataFont.setFontHeightInPoints((short) 10); + dataFont.setColor(excel.color().index); + style.setFont(dataFont); + styles.put(key, style); + } + } + return styles; + } + + /** + * 创建单元格 + */ + public Cell createHeadCell(Excel attr, Row row, int column) { + // 创建列 + Cell cell = row.createCell(column); + // 写入列信息 + cell.setCellValue(attr.name()); + setDataValidation(attr, row, column); + cell.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor()))); + if (isSubList()) { + // 填充默认样式,防止合并单元格样式失效 + sheet.setDefaultColumnStyle(column, styles.get(StringUtils.format("data_{}_{}_{}", attr.align(), attr.color(), attr.backgroundColor()))); + if (attr.needMerge()) { + sheet.addMergedRegion(new CellRangeAddress(rownum - 1, rownum, column, column)); + } + } + return cell; + } + + /** + * 设置单元格信息 + * + * @param value 单元格值 + * @param attr 注解相关 + * @param cell 单元格信息 + */ + public void setCellVo(Object value, Excel attr, Cell cell) { + if (ColumnType.STRING == attr.cellType()) { + String cellValue = Convert.toStr(value); + // 对于任何以表达式触发字符 =-+@开头的单元格,直接使用tab字符作为前缀,防止CSV注入。 + if (StringUtils.startsWithAny(cellValue, FORMULA_STR)) { + cellValue = RegExUtils.replaceFirst(cellValue, FORMULA_REGEX_STR, "\t$0"); + } + cell.setCellValue(StringUtils.isNull(cellValue) ? attr.defaultValue() : cellValue + attr.suffix()); + } else if (ColumnType.NUMERIC == attr.cellType()) { + if (StringUtils.isNotNull(value)) { + cell.setCellValue(StringUtils.contains(Convert.toStr(value), ".") ? Convert.toDouble(value) : Convert.toInt(value)); + } + } else if (ColumnType.IMAGE == attr.cellType()) { + ClientAnchor anchor = new XSSFClientAnchor(0, 0, 0, 0, (short) cell.getColumnIndex(), cell.getRow().getRowNum(), (short) (cell.getColumnIndex() + 1), cell.getRow().getRowNum() + 1); + String imagePath = Convert.toStr(value); + if (StringUtils.isNotEmpty(imagePath)) { + byte[] data = ImageUtils.getImage(imagePath); + getDrawingPatriarch(cell.getSheet()).createPicture(anchor, + cell.getSheet().getWorkbook().addPicture(data, getImageType(data))); + } + } + } + + /** + * 获取画布 + */ + public static Drawing getDrawingPatriarch(Sheet sheet) { + if (sheet.getDrawingPatriarch() == null) { + sheet.createDrawingPatriarch(); + } + return sheet.getDrawingPatriarch(); + } + + /** + * 获取图片类型,设置图片插入类型 + */ + public int getImageType(byte[] value) { + String type = FileTypeUtils.getFileExtendName(value); + if ("JPG".equalsIgnoreCase(type)) { + return Workbook.PICTURE_TYPE_JPEG; + } else if ("PNG".equalsIgnoreCase(type)) { + return Workbook.PICTURE_TYPE_PNG; + } + return Workbook.PICTURE_TYPE_JPEG; + } + + /** + * 创建表格样式 + */ + public void setDataValidation(Excel attr, Row row, int column) { + if (attr.name().indexOf("注:") >= 0) { + sheet.setColumnWidth(column, 6000); + } else { + // 设置列宽 + sheet.setColumnWidth(column, (int) ((attr.width() + 0.72) * 256)); + } + if (StringUtils.isNotEmpty(attr.prompt()) || attr.combo().length > 0) { + // 提示信息或只能选择不能输入的列内容. + setPromptOrValidation(sheet, attr.combo(), attr.prompt(), 1, 100, column, column); + } + } + + /** + * 添加单元格 + */ + public Cell addCell(Excel attr, Row row, T vo, Field field, int column) { + Cell cell = null; + try { + // 设置行高 + row.setHeight(maxHeight); + // 根据Excel中设置情况决定是否导出,有些情况需要保持为空,希望用户填写这一列. + if (attr.isExport()) { + // 创建cell + cell = row.createCell(column); + if (isSubListValue(vo) && getListCellValue(vo).size() > 1 && attr.needMerge()) { + CellRangeAddress cellAddress = new CellRangeAddress(subMergedFirstRowNum, subMergedLastRowNum, column, column); + sheet.addMergedRegion(cellAddress); + } + cell.setCellStyle(styles.get(StringUtils.format("data_{}_{}_{}", attr.align(), attr.color(), attr.backgroundColor()))); + + // 用于读取对象中的属性 + Object value = getTargetValue(vo, field, attr); + String dateFormat = attr.dateFormat(); + String readConverterExp = attr.readConverterExp(); + String separator = attr.separator(); + String dictType = attr.dictType(); + if (StringUtils.isNotEmpty(dateFormat) && StringUtils.isNotNull(value)) { + cell.setCellValue(parseDateToStr(dateFormat, value)); + } else if (StringUtils.isNotEmpty(readConverterExp) && StringUtils.isNotNull(value)) { + cell.setCellValue(convertByExp(Convert.toStr(value), readConverterExp, separator)); + } else if (StringUtils.isNotEmpty(dictType) && StringUtils.isNotNull(value)) { + cell.setCellValue(convertDictByExp(Convert.toStr(value), dictType, separator)); + } else if (value instanceof BigDecimal && -1 != attr.scale()) { + cell.setCellValue((((BigDecimal) value).setScale(attr.scale(), attr.roundingMode())).doubleValue()); + } else if (!attr.handler().equals(ExcelHandlerAdapter.class)) { + cell.setCellValue(dataFormatHandlerAdapter(value, attr)); + } else { + // 设置列类型 + setCellVo(value, attr, cell); + } + addStatisticsData(column, Convert.toStr(value), attr); + } + } catch (Exception e) { + log.error("导出Excel失败{}", e); + } + return cell; + } + + /** + * 设置 POI XSSFSheet 单元格提示或选择框 + * + * @param sheet 表单 + * @param textlist 下拉框显示的内容 + * @param promptContent 提示内容 + * @param firstRow 开始行 + * @param endRow 结束行 + * @param firstCol 开始列 + * @param endCol 结束列 + */ + public void setPromptOrValidation(Sheet sheet, String[] textlist, String promptContent, int firstRow, int endRow, + int firstCol, int endCol) { + DataValidationHelper helper = sheet.getDataValidationHelper(); + DataValidationConstraint constraint = textlist.length > 0 ? helper.createExplicitListConstraint(textlist) : helper.createCustomConstraint("DD1"); + CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol); + DataValidation dataValidation = helper.createValidation(constraint, regions); + if (StringUtils.isNotEmpty(promptContent)) { + // 如果设置了提示信息则鼠标放上去提示 + dataValidation.createPromptBox("", promptContent); + dataValidation.setShowPromptBox(true); + } + // 处理Excel兼容性问题 + if (dataValidation instanceof XSSFDataValidation) { + dataValidation.setSuppressDropDownArrow(true); + dataValidation.setShowErrorBox(true); + } else { + dataValidation.setSuppressDropDownArrow(false); + } + sheet.addValidationData(dataValidation); + } + + /** + * 解析导出值 0=男,1=女,2=未知 + * + * @param propertyValue 参数值 + * @param converterExp 翻译注解 + * @param separator 分隔符 + * @return 解析后值 + */ + public static String convertByExp(String propertyValue, String converterExp, String separator) { + StringBuilder propertyString = new StringBuilder(); + String[] convertSource = converterExp.split(","); + for (String item : convertSource) { + String[] itemArray = item.split("="); + if (StringUtils.containsAny(propertyValue, separator)) { + for (String value : propertyValue.split(separator)) { + if (itemArray[0].equals(value)) { + propertyString.append(itemArray[1] + separator); + break; + } + } + } else { + if (itemArray[0].equals(propertyValue)) { + return itemArray[1]; + } + } + } + return StringUtils.stripEnd(propertyString.toString(), separator); + } + + /** + * 反向解析值 男=0,女=1,未知=2 + * + * @param propertyValue 参数值 + * @param converterExp 翻译注解 + * @param separator 分隔符 + * @return 解析后值 + */ + public static String reverseByExp(String propertyValue, String converterExp, String separator) { + StringBuilder propertyString = new StringBuilder(); + String[] convertSource = converterExp.split(","); + for (String item : convertSource) { + String[] itemArray = item.split("="); + if (StringUtils.containsAny(propertyValue, separator)) { + for (String value : propertyValue.split(separator)) { + if (itemArray[1].equals(value)) { + propertyString.append(itemArray[0] + separator); + break; + } + } + } else { + if (itemArray[1].equals(propertyValue)) { + return itemArray[0]; + } + } + } + return StringUtils.stripEnd(propertyString.toString(), separator); + } + + /** + * 解析字典值 + * + * @param dictValue 字典值 + * @param dictType 字典类型 + * @param separator 分隔符 + * @return 字典标签 + */ + public static String convertDictByExp(String dictValue, String dictType, String separator) { + return DictUtils.getDictLabel(dictType, dictValue, separator); + } + + /** + * 反向解析值字典值 + * + * @param dictLabel 字典标签 + * @param dictType 字典类型 + * @param separator 分隔符 + * @return 字典值 + */ + public static String reverseDictByExp(String dictLabel, String dictType, String separator) { + return DictUtils.getDictValue(dictType, dictLabel, separator); + } + + /** + * 数据处理器 + * + * @param value 数据值 + * @param excel 数据注解 + * @return + */ + public String dataFormatHandlerAdapter(Object value, Excel excel) { + try { + Object instance = excel.handler().newInstance(); + Method formatMethod = excel.handler().getMethod("format", new Class[]{Object.class, String[].class}); + value = formatMethod.invoke(instance, value, excel.args()); + } catch (Exception e) { + log.error("不能格式化数据 " + excel.handler(), e.getMessage()); + } + return Convert.toStr(value); + } + + /** + * 合计统计信息 + */ + private void addStatisticsData(Integer index, String text, Excel entity) { + if (entity != null && entity.isStatistics()) { + Double temp = 0D; + if (!statistics.containsKey(index)) { + statistics.put(index, temp); + } + try { + temp = Double.valueOf(text); + } catch (NumberFormatException e) { + } + statistics.put(index, statistics.get(index) + temp); + } + } + + /** + * 创建统计行 + */ + public void addStatisticsRow() { + if (statistics.size() > 0) { + Row row = sheet.createRow(sheet.getLastRowNum() + 1); + Set keys = statistics.keySet(); + Cell cell = row.createCell(0); + cell.setCellStyle(styles.get("total")); + cell.setCellValue("合计"); + + for (Integer key : keys) { + cell = row.createCell(key); + cell.setCellStyle(styles.get("total")); + cell.setCellValue(DOUBLE_FORMAT.format(statistics.get(key))); + } + statistics.clear(); + } + } + + /** + * 编码文件名 + */ + public String encodingFilename(String filename) { + filename = UUID.randomUUID().toString() + "_" + filename + ".xlsx"; + return filename; + } + + /** + * 获取下载路径 + * + * @param filename 文件名称 + */ + public String getAbsoluteFile(String filename) { + String downloadPath = JsowellConfig.getDownloadPath() + filename; + File desc = new File(downloadPath); + if (!desc.getParentFile().exists()) { + desc.getParentFile().mkdirs(); + } + return downloadPath; + } + + /** + * 获取bean中的属性值 + * + * @param vo 实体对象 + * @param field 字段 + * @param excel 注解 + * @return 最终的属性值 + * @throws Exception + */ + private Object getTargetValue(T vo, Field field, Excel excel) throws Exception { + Object o = field.get(vo); + if (StringUtils.isNotEmpty(excel.targetAttr())) { + String target = excel.targetAttr(); + if (target.contains(".")) { + String[] targets = target.split("[.]"); + for (String name : targets) { + o = getValue(o, name); + } + } else { + o = getValue(o, target); + } + } + return o; + } + + /** + * 以类的属性的get方法方法形式获取值 + * + * @param o + * @param name + * @return value + * @throws Exception + */ + private Object getValue(Object o, String name) throws Exception { + if (StringUtils.isNotNull(o) && StringUtils.isNotEmpty(name)) { + Class clazz = o.getClass(); + Field field = clazz.getDeclaredField(name); + field.setAccessible(true); + o = field.get(o); + } + return o; + } + + /** + * 得到所有定义字段 + */ + private void createExcelField() { + this.fields = getFields(); + this.fields = this.fields.stream().sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())).collect(Collectors.toList()); + this.maxHeight = getRowHeight(); + } + + /** + * 获取字段注解信息 + */ + public List getFields() { + List fields = new ArrayList(); + List tempFields = new ArrayList<>(); + tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields())); + tempFields.addAll(Arrays.asList(clazz.getDeclaredFields())); + for (Field field : tempFields) { + if (!ArrayUtils.contains(this.excludeFields, field.getName())) { + // 单注解 + if (field.isAnnotationPresent(Excel.class)) { + Excel attr = field.getAnnotation(Excel.class); + if (attr != null && (attr.type() == Type.ALL || attr.type() == type)) { + field.setAccessible(true); + fields.add(new Object[]{field, attr}); + } + if (Collection.class.isAssignableFrom(field.getType())) { + subMethod = getSubMethod(field.getName(), clazz); + ParameterizedType pt = (ParameterizedType) field.getGenericType(); + Class subClass = (Class) pt.getActualTypeArguments()[0]; + this.subFields = FieldUtils.getFieldsListWithAnnotation(subClass, Excel.class); + } + } + + // 多注解 + if (field.isAnnotationPresent(Excels.class)) { + Excels attrs = field.getAnnotation(Excels.class); + Excel[] excels = attrs.value(); + for (Excel attr : excels) { + if (attr != null && (attr.type() == Type.ALL || attr.type() == type)) { + field.setAccessible(true); + fields.add(new Object[]{field, attr}); + } + } + } + } + } + return fields; + } + + /** + * 根据注解获取最大行高 + */ + public short getRowHeight() { + double maxHeight = 0; + for (Object[] os : this.fields) { + Excel excel = (Excel) os[1]; + maxHeight = Math.max(maxHeight, excel.height()); + } + return (short) (maxHeight * 20); + } + + /** + * 创建一个工作簿 + */ + public void createWorkbook() { + this.wb = new SXSSFWorkbook(500); + this.sheet = wb.createSheet(); + wb.setSheetName(0, sheetName); + this.styles = createStyles(wb); + } + + /** + * 创建工作表 + * + * @param sheetNo sheet数量 + * @param index 序号 + */ + public void createSheet(int sheetNo, int index) { + // 设置工作表的名称. + if (sheetNo > 1 && index > 0) { + this.sheet = wb.createSheet(); + this.createTitle(); + wb.setSheetName(index, sheetName + index); + } + } + + /** + * 获取单元格值 + * + * @param row 获取的行 + * @param column 获取单元格列号 + * @return 单元格值 + */ + public Object getCellValue(Row row, int column) { + if (row == null) { + return row; + } + Object val = ""; + try { + Cell cell = row.getCell(column); + if (StringUtils.isNotNull(cell)) { + if (cell.getCellType() == CellType.NUMERIC || cell.getCellType() == CellType.FORMULA) { + val = cell.getNumericCellValue(); + if (DateUtil.isCellDateFormatted(cell)) { + val = DateUtil.getJavaDate((Double) val); // POI Excel 日期格式转换 + } else { + if ((Double) val % 1 != 0) { + val = new BigDecimal(val.toString()); + } else { + val = new DecimalFormat("0").format(val); + } + } + } else if (cell.getCellType() == CellType.STRING) { + val = cell.getStringCellValue(); + } else if (cell.getCellType() == CellType.BOOLEAN) { + val = cell.getBooleanCellValue(); + } else if (cell.getCellType() == CellType.ERROR) { + val = cell.getErrorCellValue(); + } + + } + } catch (Exception e) { + return val; + } + return val; + } + + /** + * 判断是否是空行 + * + * @param row 判断的行 + * @return + */ + private boolean isRowEmpty(Row row) { + if (row == null) { + return true; + } + for (int i = row.getFirstCellNum(); i < row.getLastCellNum(); i++) { + Cell cell = row.getCell(i); + if (cell != null && cell.getCellType() != CellType.BLANK) { + return false; + } + } + return true; + } + + /** + * 获取Excel2003图片 + * + * @param sheet 当前sheet对象 + * @param workbook 工作簿对象 + * @return Map key:图片单元格索引(1_1)String,value:图片流PictureData + */ + public static Map getSheetPictures03(HSSFSheet sheet, HSSFWorkbook workbook) { + Map sheetIndexPicMap = new HashMap(); + List pictures = workbook.getAllPictures(); + if (!pictures.isEmpty()) { + for (HSSFShape shape : sheet.getDrawingPatriarch().getChildren()) { + HSSFClientAnchor anchor = (HSSFClientAnchor) shape.getAnchor(); + if (shape instanceof HSSFPicture) { + HSSFPicture pic = (HSSFPicture) shape; + int pictureIndex = pic.getPictureIndex() - 1; + HSSFPictureData picData = pictures.get(pictureIndex); + String picIndex = String.valueOf(anchor.getRow1()) + "_" + String.valueOf(anchor.getCol1()); + sheetIndexPicMap.put(picIndex, picData); + } + } + return sheetIndexPicMap; + } else { + return sheetIndexPicMap; + } + } + + /** + * 获取Excel2007图片 + * + * @param sheet 当前sheet对象 + * @param workbook 工作簿对象 + * @return Map key:图片单元格索引(1_1)String,value:图片流PictureData + */ + public static Map getSheetPictures07(XSSFSheet sheet, XSSFWorkbook workbook) { + Map sheetIndexPicMap = new HashMap(); + for (POIXMLDocumentPart dr : sheet.getRelations()) { + if (dr instanceof XSSFDrawing) { + XSSFDrawing drawing = (XSSFDrawing) dr; + List shapes = drawing.getShapes(); + for (XSSFShape shape : shapes) { + if (shape instanceof XSSFPicture) { + XSSFPicture pic = (XSSFPicture) shape; + XSSFClientAnchor anchor = pic.getPreferredSize(); + CTMarker ctMarker = anchor.getFrom(); + String picIndex = ctMarker.getRow() + "_" + ctMarker.getCol(); + sheetIndexPicMap.put(picIndex, pic.getPictureData()); + } + } + } + } + return sheetIndexPicMap; + } + + /** + * 格式化不同类型的日期对象 + * + * @param dateFormat 日期格式 + * @param val 被格式化的日期对象 + * @return 格式化后的日期字符 + */ + public String parseDateToStr(String dateFormat, Object val) { + if (val == null) { + return ""; + } + String str; + if (val instanceof Date) { + str = DateUtils.parseDateToStr(dateFormat, (Date) val); + } else if (val instanceof LocalDateTime) { + str = DateUtils.parseDateToStr(dateFormat, DateUtils.localDateTime2Date((LocalDateTime) val)); + } else if (val instanceof LocalDate) { + str = DateUtils.parseDateToStr(dateFormat, DateUtils.localDate2Date((LocalDate) val)); + } else { + str = val.toString(); + } + return str; + } + + /** + * 是否有对象的子列表 + */ + public boolean isSubList() { + return StringUtils.isNotNull(subFields) && subFields.size() > 0; + } + + /** + * 是否有对象的子列表,集合不为空 + */ + public boolean isSubListValue(T vo) { + return StringUtils.isNotNull(subFields) && subFields.size() > 0 && StringUtils.isNotNull(getListCellValue(vo)) && getListCellValue(vo).size() > 0; + } + + /** + * 获取集合的值 + */ + public Collection getListCellValue(Object obj) { + Object value; + try { + value = subMethod.invoke(obj, new Object[]{}); + } catch (Exception e) { + return new ArrayList(); + } + return (Collection) value; + } + + /** + * 获取对象的子列表方法 + * + * @param name 名称 + * @param pojoClass 类对象 + * @return 子列表方法 + */ + public Method getSubMethod(String name, Class pojoClass) { + StringBuffer getMethodName = new StringBuffer("get"); + getMethodName.append(name.substring(0, 1).toUpperCase()); + getMethodName.append(name.substring(1)); + Method method = null; + try { + method = pojoClass.getMethod(getMethodName.toString(), new Class[]{}); + } catch (Exception e) { + log.error("获取对象异常{}", e.getMessage()); + } + return method; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/reflect/ReflectUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/reflect/ReflectUtils.java new file mode 100644 index 000000000..1d9ff8910 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/reflect/ReflectUtils.java @@ -0,0 +1,329 @@ +package com.jsowell.common.util.reflect; + +import com.jsowell.common.core.text.Convert; +import com.jsowell.common.util.DateUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.poi.ss.usermodel.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Date; + +/** + * 反射工具类. 提供调用getter/setter方法, 访问私有变量, 调用私有方法, 获取泛型类型Class, 被AOP过的真实类等工具函数. + * + * @author jsowell + */ +@SuppressWarnings("rawtypes") +public class ReflectUtils { + private static final String SETTER_PREFIX = "set"; + + private static final String GETTER_PREFIX = "get"; + + private static final String CGLIB_CLASS_SEPARATOR = "$$"; + + private static Logger logger = LoggerFactory.getLogger(ReflectUtils.class); + + /** + * 调用Getter方法. + * 支持多级,如:对象名.对象名.方法 + */ + @SuppressWarnings("unchecked") + public static E invokeGetter(Object obj, String propertyName) { + Object object = obj; + for (String name : StringUtils.split(propertyName, ".")) { + String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(name); + object = invokeMethod(object, getterMethodName, new Class[]{}, new Object[]{}); + } + return (E) object; + } + + /** + * 调用Setter方法, 仅匹配方法名。 + * 支持多级,如:对象名.对象名.方法 + */ + public static void invokeSetter(Object obj, String propertyName, E value) { + Object object = obj; + String[] names = StringUtils.split(propertyName, "."); + for (int i = 0; i < names.length; i++) { + if (i < names.length - 1) { + String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(names[i]); + object = invokeMethod(object, getterMethodName, new Class[]{}, new Object[]{}); + } else { + String setterMethodName = SETTER_PREFIX + StringUtils.capitalize(names[i]); + invokeMethodByName(object, setterMethodName, new Object[]{value}); + } + } + } + + /** + * 直接读取对象属性值, 无视private/protected修饰符, 不经过getter函数. + */ + @SuppressWarnings("unchecked") + public static E getFieldValue(final Object obj, final String fieldName) { + Field field = getAccessibleField(obj, fieldName); + if (field == null) { + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 "); + return null; + } + E result = null; + try { + result = (E) field.get(obj); + } catch (IllegalAccessException e) { + logger.error("不可能抛出的异常{}", e.getMessage()); + } + return result; + } + + /** + * 直接设置对象属性值, 无视private/protected修饰符, 不经过setter函数. + */ + public static void setFieldValue(final Object obj, final String fieldName, final E value) { + Field field = getAccessibleField(obj, fieldName); + if (field == null) { + // throw new IllegalArgumentException("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 "); + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 "); + return; + } + try { + field.set(obj, value); + } catch (IllegalAccessException e) { + logger.error("不可能抛出的异常: {}", e.getMessage()); + } + } + + /** + * 直接调用对象方法, 无视private/protected修饰符. + * 用于一次性调用的情况,否则应使用getAccessibleMethod()函数获得Method后反复调用. + * 同时匹配方法名+参数类型, + */ + @SuppressWarnings("unchecked") + public static E invokeMethod(final Object obj, final String methodName, final Class[] parameterTypes, + final Object[] args) { + if (obj == null || methodName == null) { + return null; + } + Method method = getAccessibleMethod(obj, methodName, parameterTypes); + if (method == null) { + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + methodName + "] 方法 "); + return null; + } + try { + return (E) method.invoke(obj, args); + } catch (Exception e) { + String msg = "method: " + method + ", obj: " + obj + ", args: " + args + ""; + throw convertReflectionExceptionToUnchecked(msg, e); + } + } + + /** + * 直接调用对象方法, 无视private/protected修饰符, + * 用于一次性调用的情况,否则应使用getAccessibleMethodByName()函数获得Method后反复调用. + * 只匹配函数名,如果有多个同名函数调用第一个。 + */ + @SuppressWarnings("unchecked") + public static E invokeMethodByName(final Object obj, final String methodName, final Object[] args) { + Method method = getAccessibleMethodByName(obj, methodName, args.length); + if (method == null) { + // 如果为空不报错,直接返回空。 + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + methodName + "] 方法 "); + return null; + } + try { + // 类型转换(将参数数据类型转换为目标方法参数类型) + Class[] cs = method.getParameterTypes(); + for (int i = 0; i < cs.length; i++) { + if (args[i] != null && !args[i].getClass().equals(cs[i])) { + if (cs[i] == String.class) { + args[i] = Convert.toStr(args[i]); + if (StringUtils.endsWith((String) args[i], ".0")) { + args[i] = StringUtils.substringBefore((String) args[i], ".0"); + } + } else if (cs[i] == Integer.class) { + args[i] = Convert.toInt(args[i]); + } else if (cs[i] == Long.class) { + args[i] = Convert.toLong(args[i]); + } else if (cs[i] == Double.class) { + args[i] = Convert.toDouble(args[i]); + } else if (cs[i] == Float.class) { + args[i] = Convert.toFloat(args[i]); + } else if (cs[i] == Date.class) { + if (args[i] instanceof String) { + args[i] = DateUtils.parseDate(args[i]); + } else { + args[i] = DateUtil.getJavaDate((Double) args[i]); + } + } else if (cs[i] == boolean.class || cs[i] == Boolean.class) { + args[i] = Convert.toBool(args[i]); + } + } + } + return (E) method.invoke(obj, args); + } catch (Exception e) { + String msg = "method: " + method + ", obj: " + obj + ", args: " + args + ""; + throw convertReflectionExceptionToUnchecked(msg, e); + } + } + + /** + * 循环向上转型, 获取对象的DeclaredField, 并强制设置为可访问. + * 如向上转型到Object仍无法找到, 返回null. + */ + public static Field getAccessibleField(final Object obj, final String fieldName) { + // 为空不报错。直接返回 null + if (obj == null) { + return null; + } + Validate.notBlank(fieldName, "fieldName can't be blank"); + for (Class superClass = obj.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) { + try { + Field field = superClass.getDeclaredField(fieldName); + makeAccessible(field); + return field; + } catch (NoSuchFieldException e) { + continue; + } + } + return null; + } + + /** + * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问. + * 如向上转型到Object仍无法找到, 返回null. + * 匹配函数名+参数类型。 + * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args) + */ + public static Method getAccessibleMethod(final Object obj, final String methodName, + final Class... parameterTypes) { + // 为空不报错。直接返回 null + if (obj == null) { + return null; + } + Validate.notBlank(methodName, "methodName can't be blank"); + for (Class searchType = obj.getClass(); searchType != Object.class; searchType = searchType.getSuperclass()) { + try { + Method method = searchType.getDeclaredMethod(methodName, parameterTypes); + makeAccessible(method); + return method; + } catch (NoSuchMethodException e) { + continue; + } + } + return null; + } + + /** + * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问. + * 如向上转型到Object仍无法找到, 返回null. + * 只匹配函数名。 + * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args) + */ + public static Method getAccessibleMethodByName(final Object obj, final String methodName, int argsNum) { + // 为空不报错。直接返回 null + if (obj == null) { + return null; + } + Validate.notBlank(methodName, "methodName can't be blank"); + for (Class searchType = obj.getClass(); searchType != Object.class; searchType = searchType.getSuperclass()) { + Method[] methods = searchType.getDeclaredMethods(); + for (Method method : methods) { + if (method.getName().equals(methodName) && method.getParameterTypes().length == argsNum) { + makeAccessible(method); + return method; + } + } + } + return null; + } + + /** + * 改变private/protected的方法为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。 + */ + public static void makeAccessible(Method method) { + if ((!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers())) + && !method.isAccessible()) { + method.setAccessible(true); + } + } + + /** + * 改变private/protected的成员变量为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。 + */ + public static void makeAccessible(Field field) { + if ((!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers()) + || Modifier.isFinal(field.getModifiers())) && !field.isAccessible()) { + field.setAccessible(true); + } + } + + /** + * 通过反射, 获得Class定义中声明的泛型参数的类型, 注意泛型必须定义在父类处 + * 如无法找到, 返回Object.class. + */ + @SuppressWarnings("unchecked") + public static Class getClassGenricType(final Class clazz) { + return getClassGenricType(clazz, 0); + } + + /** + * 通过反射, 获得Class定义中声明的父类的泛型参数的类型. + * 如无法找到, 返回Object.class. + */ + public static Class getClassGenricType(final Class clazz, final int index) { + Type genType = clazz.getGenericSuperclass(); + + if (!(genType instanceof ParameterizedType)) { + logger.debug(clazz.getSimpleName() + "'s superclass not ParameterizedType"); + return Object.class; + } + + Type[] params = ((ParameterizedType) genType).getActualTypeArguments(); + + if (index >= params.length || index < 0) { + logger.debug("Index: " + index + ", Size of " + clazz.getSimpleName() + "'s Parameterized Type: " + + params.length); + return Object.class; + } + if (!(params[index] instanceof Class)) { + logger.debug(clazz.getSimpleName() + " not set the actual class on superclass generic parameter"); + return Object.class; + } + + return (Class) params[index]; + } + + public static Class getUserClass(Object instance) { + if (instance == null) { + throw new RuntimeException("Instance must not be null"); + } + Class clazz = instance.getClass(); + if (clazz != null && clazz.getName().contains(CGLIB_CLASS_SEPARATOR)) { + Class superClass = clazz.getSuperclass(); + if (superClass != null && !Object.class.equals(superClass)) { + return superClass; + } + } + return clazz; + + } + + /** + * 将反射时的checked exception转换为unchecked exception. + */ + public static RuntimeException convertReflectionExceptionToUnchecked(String msg, Exception e) { + if (e instanceof IllegalAccessException || e instanceof IllegalArgumentException + || e instanceof NoSuchMethodException) { + return new IllegalArgumentException(msg, e); + } else if (e instanceof InvocationTargetException) { + return new RuntimeException(msg, ((InvocationTargetException) e).getTargetException()); + } + return new RuntimeException(msg, e); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/sign/Base64.java b/jsowell-common/src/main/java/com/jsowell/common/util/sign/Base64.java new file mode 100644 index 000000000..f4da0faa7 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/sign/Base64.java @@ -0,0 +1,253 @@ +package com.jsowell.common.util.sign; + +/** + * Base64工具类 + * + * @author jsowell + */ +public final class Base64 { + static private final int BASELENGTH = 128; + static private final int LOOKUPLENGTH = 64; + static private final int TWENTYFOURBITGROUP = 24; + static private final int EIGHTBIT = 8; + static private final int SIXTEENBIT = 16; + static private final int FOURBYTE = 4; + static private final int SIGN = -128; + static private final char PAD = '='; + static final private byte[] base64Alphabet = new byte[BASELENGTH]; + static final private char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH]; + + static { + for (int i = 0; i < BASELENGTH; ++i) { + base64Alphabet[i] = -1; + } + for (int i = 'Z'; i >= 'A'; i--) { + base64Alphabet[i] = (byte) (i - 'A'); + } + for (int i = 'z'; i >= 'a'; i--) { + base64Alphabet[i] = (byte) (i - 'a' + 26); + } + + for (int i = '9'; i >= '0'; i--) { + base64Alphabet[i] = (byte) (i - '0' + 52); + } + + base64Alphabet['+'] = 62; + base64Alphabet['/'] = 63; + + for (int i = 0; i <= 25; i++) { + lookUpBase64Alphabet[i] = (char) ('A' + i); + } + + for (int i = 26, j = 0; i <= 51; i++, j++) { + lookUpBase64Alphabet[i] = (char) ('a' + j); + } + + for (int i = 52, j = 0; i <= 61; i++, j++) { + lookUpBase64Alphabet[i] = (char) ('0' + j); + } + lookUpBase64Alphabet[62] = (char) '+'; + lookUpBase64Alphabet[63] = (char) '/'; + } + + private static boolean isWhiteSpace(char octect) { + return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9); + } + + private static boolean isPad(char octect) { + return (octect == PAD); + } + + private static boolean isData(char octect) { + return (octect < BASELENGTH && base64Alphabet[octect] != -1); + } + + /** + * Encodes hex octects into Base64 + * + * @param binaryData Array containing binaryData + * @return Encoded Base64 array + */ + public static String encode(byte[] binaryData) { + if (binaryData == null) { + return null; + } + + int lengthDataBits = binaryData.length * EIGHTBIT; + if (lengthDataBits == 0) { + return ""; + } + + int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP; + int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP; + int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 : numberTriplets; + char encodedData[] = null; + + encodedData = new char[numberQuartet * 4]; + + byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0; + + int encodedIndex = 0; + int dataIndex = 0; + + for (int i = 0; i < numberTriplets; i++) { + b1 = binaryData[dataIndex++]; + b2 = binaryData[dataIndex++]; + b3 = binaryData[dataIndex++]; + + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) : (byte) ((b3) >> 6 ^ 0xfc); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f]; + } + + // form integral number of 6-bit groups + if (fewerThan24bits == EIGHTBIT) { + b1 = binaryData[dataIndex]; + k = (byte) (b1 & 0x03); + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4]; + encodedData[encodedIndex++] = PAD; + encodedData[encodedIndex++] = PAD; + } else if (fewerThan24bits == SIXTEENBIT) { + b1 = binaryData[dataIndex]; + b2 = binaryData[dataIndex + 1]; + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2]; + encodedData[encodedIndex++] = PAD; + } + return new String(encodedData); + } + + /** + * Decodes Base64 data into octects + * + * @param encoded string containing Base64 data + * @return Array containind decoded data. + */ + public static byte[] decode(String encoded) { + if (encoded == null) { + return null; + } + + char[] base64Data = encoded.toCharArray(); + // remove white spaces + int len = removeWhiteSpace(base64Data); + + if (len % FOURBYTE != 0) { + return null;// should be divisible by four + } + + int numberQuadruple = (len / FOURBYTE); + + if (numberQuadruple == 0) { + return new byte[0]; + } + + byte decodedData[] = null; + byte b1 = 0, b2 = 0, b3 = 0, b4 = 0; + char d1 = 0, d2 = 0, d3 = 0, d4 = 0; + + int i = 0; + int encodedIndex = 0; + int dataIndex = 0; + decodedData = new byte[(numberQuadruple) * 3]; + + for (; i < numberQuadruple - 1; i++) { + + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++])) + || !isData((d3 = base64Data[dataIndex++])) || !isData((d4 = base64Data[dataIndex++]))) { + return null; + } // if found "no data" just return null + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + } + + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))) { + return null;// if found "no data" just return null + } + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + + d3 = base64Data[dataIndex++]; + d4 = base64Data[dataIndex++]; + if (!isData((d3)) || !isData((d4))) {// Check if they are PAD characters + if (isPad(d3) && isPad(d4)) { + if ((b2 & 0xf) != 0)// last 4 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 1]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4); + return tmp; + } else if (!isPad(d3) && isPad(d4)) { + b3 = base64Alphabet[d3]; + if ((b3 & 0x3) != 0)// last 2 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 2]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + return tmp; + } else { + return null; + } + } else { // No PAD e.g 3cQl + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + + } + return decodedData; + } + + /** + * remove WhiteSpace from MIME containing encoded Base64 data. + * + * @param data the byte array of base64 data (with WS) + * @return the new length + */ + private static int removeWhiteSpace(char[] data) { + if (data == null) { + return 0; + } + + // count characters that's not whitespace + int newSize = 0; + int len = data.length; + for (int i = 0; i < len; i++) { + if (!isWhiteSpace(data[i])) { + data[newSize++] = data[i]; + } + } + return newSize; + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/sign/MD5Util.java b/jsowell-common/src/main/java/com/jsowell/common/util/sign/MD5Util.java new file mode 100644 index 000000000..debed9993 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/sign/MD5Util.java @@ -0,0 +1,107 @@ +package com.jsowell.common.util.sign; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; + +public class MD5Util { + + private static final Logger log = LoggerFactory.getLogger(MD5Util.class); + + private static final String hexDigits[] = {"0", "1", "2", "3", "4", "5", + "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"}; + + private static String byteArrayToHexString(byte b[]) { + StringBuffer resultSb = new StringBuffer(); + for (int i = 0; i < b.length; i++) + resultSb.append(byteToHexString(b[i])); + + return resultSb.toString(); + } + + private static String byteToHexString(byte b) { + int n = b; + if (n < 0) + n += 256; + int d1 = n / 16; + int d2 = n % 16; + return hexDigits[d1] + hexDigits[d2]; + } + + public static String MD5Encode(String origin, String charsetname) { + String resultString = null; + try { + resultString = new String(origin); + MessageDigest md = MessageDigest.getInstance("MD5"); + if (charsetname == null || "".equals(charsetname)) + resultString = byteArrayToHexString(md.digest(resultString + .getBytes())); + else + resultString = byteArrayToHexString(md.digest(resultString + .getBytes(charsetname))); + } catch (Exception exception) { + } + return resultString; + } + + /** + * MD5编码 + * + * @param origin 原始字符串 + * @return 经过MD5加密之后的结果 + */ + public static String MD5Encode(String origin) { + String resultString = null; + try { + resultString = origin; + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(resultString.getBytes("UTF-8")); + resultString = byteArrayToHexString(md.digest()); + } catch (Exception e) { + e.printStackTrace(); + } + return resultString; + } + + private static byte[] md5(String s) { + MessageDigest algorithm; + try { + algorithm = MessageDigest.getInstance("MD5"); + algorithm.reset(); + algorithm.update(s.getBytes(StandardCharsets.UTF_8)); + byte[] messageDigest = algorithm.digest(); + return messageDigest; + } catch (Exception e) { + log.error("MD5 Error...", e); + } + return null; + } + + private static String toHex(byte[] hash) { + if (hash == null) { + return null; + } + StringBuilder buf = new StringBuilder(hash.length * 2); + int i; + + for (i = 0; i < hash.length; i++) { + if ((hash[i] & 0xff) < 0x10) { + buf.append("0"); + } + buf.append(Long.toString(hash[i] & 0xff, 16)); + } + return buf.toString(); + } + + public static String hash(String s) { + try { + return new String(toHex(md5(s)).getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); + } catch (Exception e) { + log.error("not supported charset...", e); + return s; + } + } + +} \ No newline at end of file diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/sim/SimCardUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/sim/SimCardUtils.java new file mode 100644 index 000000000..ebe762248 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/sim/SimCardUtils.java @@ -0,0 +1,43 @@ +package com.jsowell.common.util.sim; + +import java.math.BigDecimal; +import java.text.DecimalFormat; + +/** + * sim卡相关工具类 + * + * @author JS-ZZA + * @date 2022/12/7 10:25 + */ +public class SimCardUtils { + + /** + * 将KB转为对应的MB或GB + * + * @param size 需进行转化的值(KB) + * @return 转换后的值,自带单位 + */ + public static String kb2MbOrGb(int size) { + int GB = 1024 * 1024; // 定义GB的计算常量 + int MB = 1024; // 定义MB的计算常量 + DecimalFormat df = new DecimalFormat("0.00");//格式化小数 + String resultSize = ""; + if (size / GB >= 1) { + //如果当前Byte的值大于等于1GB + resultSize = df.format(size / (float) GB); + } else if (size / MB >= 1) { + //如果当前Byte的值大于等于1MB + resultSize = df.format(size / (float) MB); + } else { + resultSize = String.valueOf(size); + } + return resultSize; + } + + public static void main(String[] args) { + int a = 13320; + String str = "11874.95"; + BigDecimal bigDecimal = new BigDecimal(str); + System.out.println(kb2MbOrGb(bigDecimal.intValue())); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/sim/XunZhongSimUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/sim/XunZhongSimUtils.java new file mode 100644 index 000000000..1253ad191 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/sim/XunZhongSimUtils.java @@ -0,0 +1,125 @@ +package com.jsowell.common.util.sim; + +import cn.hutool.http.HttpRequest; +import org.apache.commons.codec.binary.Base64; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Hashtable; +import java.util.List; + +/** + * 讯众物联Sim卡商工具类 + * + * @author JS-ZZA + * @date 2022/12/6 15:47 + */ +@Component +public class XunZhongSimUtils { + + private static String API_SECRET; + + @Value("${xunzhong.apiSecret}") + public void setAPI_SECRET(String API_SECRET) { + XunZhongSimUtils.API_SECRET = API_SECRET; + } + + // Base 64 加密 + private static String encode(final byte[] bytes) { + return new String(Base64.encodeBase64(bytes)); + } + + // SHA 256 加密 + private static String SHA256(final String strText) { + return SHA(strText, "SHA-256"); + } + + /** + * SHA算法加密 + * @param strText + * @param strType + * @return + */ + private static String SHA(final String strText, final String strType) { + // 返回值 + String strResult = null; + // 是否是有效字符串 + if (strText != null && strText.length() > 0) { + try { + // SHA 加密开始 + MessageDigest messageDigest = MessageDigest.getInstance(strType); + // 传⼊要加密的字符串 + messageDigest.update(strText.getBytes()); + // 得到 byte 结果 + byte[] byteBuffer = messageDigest.digest(); + // 將 byte 转换 string + StringBuilder strHexString = new StringBuilder(); + // 遍历 byte buffer + for (byte b : byteBuffer) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + strHexString.append('0'); + } + strHexString.append(hex); + } + strResult = strHexString.toString(); + } + // 得到返回結果 + catch(NoSuchAlgorithmException e){ + e.printStackTrace(); + } + } + + return strResult; + } + + + /** + * 生成signStr + * @param params + * @return + */ + public static String getSignStr(Hashtable params) { + // 参数按 key 排序 + List keys = new ArrayList<>(params.keySet()); + Collections.sort(keys); + + StringBuilder sign = new StringBuilder(); + + for (String key : keys) { + Object obj = params.get(key); + if (!sign.toString().equals("")) { + sign.append("&"); + } + sign.append(key).append("=").append(obj); + } + // ⽣成 sign + sign.append(API_SECRET); + // 添加加密的 sign + String signStr = SHA256(encode(sign.toString().getBytes())); + + return signStr; + } + + /** + * 链式构建请求,引入hutool + * @param url + * @param params + * @return + */ + public static String sendPost(String url, Hashtable params) { + String postResult = HttpRequest.post(url) + .header("Content-type", "application/x-www-form-urlencoded;charset=utf-8") + .form(params) + .timeout(20000)//超时,毫秒 + .execute().body(); + + return postResult; + } + + +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/spring/SpringUtils.java b/jsowell-common/src/main/java/com/jsowell/common/util/spring/SpringUtils.java new file mode 100644 index 000000000..3ba1dbe56 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/spring/SpringUtils.java @@ -0,0 +1,141 @@ +package com.jsowell.common.util.spring; + +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; +import com.jsowell.common.util.StringUtils; + +/** + * spring工具类 方便在非spring管理环境中获取bean + * + * @author jsowell + */ +@Component +public final class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware { + /** + * Spring应用上下文环境 + */ + private static ConfigurableListableBeanFactory beanFactory; + + private static ApplicationContext applicationContext; + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + SpringUtils.beanFactory = beanFactory; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + SpringUtils.applicationContext = applicationContext; + } + + /** + * 获取对象 + * + * @param name + * @return Object 一个以所给名字注册的bean的实例 + * @throws org.springframework.beans.BeansException + */ + @SuppressWarnings("unchecked") + public static T getBean(String name) throws BeansException { + return (T) beanFactory.getBean(name); + } + + /** + * 获取类型为requiredType的对象 + * + * @param clz + * @return + * @throws org.springframework.beans.BeansException + */ + public static T getBean(Class clz) throws BeansException { + T result = (T) beanFactory.getBean(clz); + return result; + } + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + * + * @param name + * @return boolean + */ + public static boolean containsBean(String name) { + return beanFactory.containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + * + * @param name + * @return boolean + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException { + return beanFactory.isSingleton(name); + } + + /** + * @param name + * @return Class 注册对象的类型 + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + */ + public static Class getType(String name) throws NoSuchBeanDefinitionException { + return beanFactory.getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + * + * @param name + * @return + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException { + return beanFactory.getAliases(name); + } + + /** + * 获取aop代理对象 + * + * @param invoker + * @return + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) { + return (T) AopContext.currentProxy(); + } + + /** + * 获取当前的环境配置,无配置返回null + * + * @return 当前的环境配置 + */ + public static String[] getActiveProfiles() { + return applicationContext.getEnvironment().getActiveProfiles(); + } + + /** + * 获取当前的环境配置,当有多个环境配置时,只获取第一个 + * + * @return 当前的环境配置 + */ + public static String getActiveProfile() { + final String[] activeProfiles = getActiveProfiles(); + return StringUtils.isNotEmpty(activeProfiles) ? activeProfiles[0] : null; + } + + /** + * 获取配置文件中的值 + * + * @param key 配置文件的key + * @return 当前的配置文件的值 + */ + public static String getRequiredProperty(String key) { + return applicationContext.getEnvironment().getRequiredProperty(key); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/util/sql/SqlUtil.java b/jsowell-common/src/main/java/com/jsowell/common/util/sql/SqlUtil.java new file mode 100644 index 000000000..3f59a9092 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/util/sql/SqlUtil.java @@ -0,0 +1,53 @@ +package com.jsowell.common.util.sql; + +import com.jsowell.common.exception.UtilException; +import com.jsowell.common.util.StringUtils; + +/** + * sql操作工具类 + * + * @author jsowell + */ +public class SqlUtil { + /** + * 定义常用的 sql关键字 + */ + public static String SQL_REGEX = "select |insert |delete |update |drop |count |exec |chr |mid |master |truncate |char |and |declare "; + + /** + * 仅支持字母、数字、下划线、空格、逗号、小数点(支持多个字段排序) + */ + public static String SQL_PATTERN = "[a-zA-Z0-9_\\ \\,\\.]+"; + + /** + * 检查字符,防止注入绕过 + */ + public static String escapeOrderBySql(String value) { + if (StringUtils.isNotEmpty(value) && !isValidOrderBySql(value)) { + throw new UtilException("参数不符合规范,不能进行查询"); + } + return value; + } + + /** + * 验证 order by 语法是否符合规范 + */ + public static boolean isValidOrderBySql(String value) { + return value.matches(SQL_PATTERN); + } + + /** + * SQL关键字检查 + */ + public static void filterKeyword(String value) { + if (StringUtils.isEmpty(value)) { + return; + } + String[] sqlKeywords = StringUtils.split(SQL_REGEX, "\\|"); + for (String sqlKeyword : sqlKeywords) { + if (StringUtils.indexOfIgnoreCase(value, sqlKeyword) > -1) { + throw new UtilException("参数存在SQL注入风险"); + } + } + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/xss/Xss.java b/jsowell-common/src/main/java/com/jsowell/common/xss/Xss.java new file mode 100644 index 000000000..faee4ca4e --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/xss/Xss.java @@ -0,0 +1,26 @@ +package com.jsowell.common.xss; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 自定义xss校验注解 + * + * @author jsowell + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(value = { ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER }) +@Constraint(validatedBy = { XssValidator.class }) +public @interface Xss{ + String message() + + default "不允许任何脚本运行"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/xss/XssValidator.java b/jsowell-common/src/main/java/com/jsowell/common/xss/XssValidator.java new file mode 100644 index 000000000..aa05ae6d5 --- /dev/null +++ b/jsowell-common/src/main/java/com/jsowell/common/xss/XssValidator.java @@ -0,0 +1,31 @@ +package com.jsowell.common.xss; + +import com.jsowell.common.util.StringUtils; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 自定义xss校验注解实现 + * + * @author jsowell + */ +public class XssValidator implements ConstraintValidator { + private static final String HTML_PATTERN = "<(\\S*?)[^>]*>.*?|<.*? />"; + + @Override + public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) { + if (StringUtils.isBlank(value)) { + return true; + } + return !containsHtml(value); + } + + public static boolean containsHtml(String value) { + Pattern pattern = Pattern.compile(HTML_PATTERN); + Matcher matcher = pattern.matcher(value); + return matcher.matches(); + } +} \ No newline at end of file diff --git a/jsowell-framework/pom.xml b/jsowell-framework/pom.xml new file mode 100644 index 000000000..ba1d8ade9 --- /dev/null +++ b/jsowell-framework/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + com.jsowell + jsowell-charger-web + 1.0.0 + + + jsowell-framework + + + framework框架核心 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + com.alibaba + druid-spring-boot-starter + + + + + com.github.penggle + kaptcha + + + javax.servlet-api + javax.servlet + + + + + + + com.github.oshi + oshi-core + + + + + com.jsowell + jsowell-system + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.8 + 1.8 + + /src/test/** + + utf-8 + + + + + + + \ No newline at end of file diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/aspectj/DataScopeAspect.java b/jsowell-framework/src/main/java/com/jsowell/framework/aspectj/DataScopeAspect.java new file mode 100644 index 000000000..df6a4f442 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/aspectj/DataScopeAspect.java @@ -0,0 +1,135 @@ +package com.jsowell.framework.aspectj; + +import com.jsowell.common.annotation.DataScope; +import com.jsowell.common.core.domain.BaseEntity; +import com.jsowell.common.core.domain.entity.SysRole; +import com.jsowell.common.core.domain.entity.SysUser; +import com.jsowell.common.core.domain.model.LoginUser; +import com.jsowell.common.util.SecurityUtils; +import com.jsowell.common.util.StringUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * 数据过滤处理 + * + * @author jsowell + */ +@Aspect +@Component +public class DataScopeAspect { + /** + * 全部数据权限 + */ + public static final String DATA_SCOPE_ALL = "1"; + + /** + * 自定数据权限 + */ + public static final String DATA_SCOPE_CUSTOM = "2"; + + /** + * 部门数据权限 + */ + public static final String DATA_SCOPE_DEPT = "3"; + + /** + * 部门及以下数据权限 + */ + public static final String DATA_SCOPE_DEPT_AND_CHILD = "4"; + + /** + * 仅本人数据权限 + */ + public static final String DATA_SCOPE_SELF = "5"; + + /** + * 数据权限过滤关键字 + */ + public static final String DATA_SCOPE = "dataScope"; + + @Before("@annotation(controllerDataScope)") + public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable { + clearDataScope(point); + handleDataScope(point, controllerDataScope); + } + + protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope) { + // 获取当前的用户 + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNotNull(loginUser)) { + SysUser currentUser = loginUser.getUser(); + // 如果是超级管理员,则不过滤数据 + if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin()) { + dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(), + controllerDataScope.userAlias()); + } + } + } + + /** + * 数据范围过滤 + * + * @param joinPoint 切点 + * @param user 用户 + * @param deptAlias 部门别名 + * @param userAlias 用户别名 + */ + public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias) { + StringBuilder sqlString = new StringBuilder(); + List conditions = new ArrayList(); + + for (SysRole role : user.getRoles()) { + String dataScope = role.getDataScope(); + if (!DATA_SCOPE_CUSTOM.equals(dataScope) && conditions.contains(dataScope)) { + continue; + } + if (DATA_SCOPE_ALL.equals(dataScope)) { + sqlString = new StringBuilder(); + break; + } else if (DATA_SCOPE_CUSTOM.equals(dataScope)) { + sqlString.append(StringUtils.format( + " OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias, + role.getRoleId())); + } else if (DATA_SCOPE_DEPT.equals(dataScope)) { + sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId())); + } else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)) { + sqlString.append(StringUtils.format( + " OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )", + deptAlias, user.getDeptId(), user.getDeptId())); + } else if (DATA_SCOPE_SELF.equals(dataScope)) { + if (StringUtils.isNotBlank(userAlias)) { + sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId())); + } else { + // 数据权限为仅本人且没有userAlias别名不查询任何数据 + sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias)); + } + } + conditions.add(dataScope); + } + + if (StringUtils.isNotBlank(sqlString.toString())) { + Object params = joinPoint.getArgs()[0]; + if (StringUtils.isNotNull(params) && params instanceof BaseEntity) { + BaseEntity baseEntity = (BaseEntity) params; + baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")"); + } + } + } + + /** + * 拼接权限sql前先清空params.dataScope参数防止注入 + */ + private void clearDataScope(final JoinPoint joinPoint) { + Object params = joinPoint.getArgs()[0]; + if (StringUtils.isNotNull(params) && params instanceof BaseEntity) { + BaseEntity baseEntity = (BaseEntity) params; + baseEntity.getParams().put(DATA_SCOPE, ""); + } + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/aspectj/DataSourceAspect.java b/jsowell-framework/src/main/java/com/jsowell/framework/aspectj/DataSourceAspect.java new file mode 100644 index 000000000..1d580a979 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/aspectj/DataSourceAspect.java @@ -0,0 +1,64 @@ +package com.jsowell.framework.aspectj; + +import com.jsowell.common.annotation.DataSource; +import com.jsowell.common.util.StringUtils; +import com.jsowell.framework.datasource.DynamicDataSourceContextHolder; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.Objects; + +/** + * 多数据源处理 + * + * @author jsowell + */ +@Aspect +@Order(1) +@Component +public class DataSourceAspect { + protected Logger logger = LoggerFactory.getLogger(getClass()); + + @Pointcut("@annotation(com.jsowell.common.annotation.DataSource)" + + "|| @within(com.jsowell.common.annotation.DataSource)") + public void dsPointCut() { + + } + + @Around("dsPointCut()") + public Object around(ProceedingJoinPoint point) throws Throwable { + DataSource dataSource = getDataSource(point); + + if (StringUtils.isNotNull(dataSource)) { + DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name()); + } + + try { + return point.proceed(); + } finally { + // 销毁数据源 在执行方法之后 + DynamicDataSourceContextHolder.clearDataSourceType(); + } + } + + /** + * 获取需要切换的数据源 + */ + public DataSource getDataSource(ProceedingJoinPoint point) { + MethodSignature signature = (MethodSignature) point.getSignature(); + DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class); + if (Objects.nonNull(dataSource)) { + return dataSource; + } + + return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class); + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/aspectj/LogAspect.java b/jsowell-framework/src/main/java/com/jsowell/framework/aspectj/LogAspect.java new file mode 100644 index 000000000..8d9ed795b --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/aspectj/LogAspect.java @@ -0,0 +1,200 @@ +package com.jsowell.framework.aspectj; + +import com.alibaba.fastjson2.JSON; +import com.jsowell.common.annotation.Log; +import com.jsowell.common.core.domain.model.LoginUser; +import com.jsowell.common.enums.BusinessStatus; +import com.jsowell.common.enums.HttpMethod; +import com.jsowell.common.filter.PropertyPreExcludeFilter; +import com.jsowell.common.util.SecurityUtils; +import com.jsowell.common.util.ServletUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.ip.IpUtils; +import com.jsowell.framework.manager.AsyncManager; +import com.jsowell.framework.manager.factory.AsyncFactory; +import com.jsowell.system.domain.SysOperLog; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.validation.BindingResult; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.HandlerMapping; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Collection; +import java.util.Map; + +/** + * 操作日志记录处理 + * + * @author jsowell + */ +@Aspect +@Component +public class LogAspect { + private static final Logger log = LoggerFactory.getLogger(LogAspect.class); + + /** + * 排除敏感属性字段 + */ + public static final String[] EXCLUDE_PROPERTIES = {"password", "oldPassword", "newPassword", "confirmPassword"}; + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) { + handleLog(joinPoint, controllerLog, null, jsonResult); + } + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) { + handleLog(joinPoint, controllerLog, e, null); + } + + protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) { + try { + // 获取当前的用户 + LoginUser loginUser = SecurityUtils.getLoginUser(); + + // *========数据库日志=========*// + SysOperLog operLog = new SysOperLog(); + operLog.setStatus(BusinessStatus.SUCCESS.ordinal()); + // 请求的地址 + String ip = IpUtils.getIpAddr(ServletUtils.getRequest()); + operLog.setOperIp(ip); + operLog.setOperUrl(ServletUtils.getRequest().getRequestURI()); + if (loginUser != null) { + operLog.setOperName(loginUser.getUsername()); + } + + if (e != null) { + operLog.setStatus(BusinessStatus.FAIL.ordinal()); + operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000)); + } + // 设置方法名称 + String className = joinPoint.getTarget().getClass().getName(); + String methodName = joinPoint.getSignature().getName(); + operLog.setMethod(className + "." + methodName + "()"); + // 设置请求方式 + operLog.setRequestMethod(ServletUtils.getRequest().getMethod()); + // 处理设置注解上的参数 + getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult); + // 保存数据库 + AsyncManager.me().execute(AsyncFactory.recordOper(operLog)); + } catch (Exception exp) { + // 记录本地异常日志 + log.error("==前置通知异常=="); + log.error("异常信息:{}", exp.getMessage()); + exp.printStackTrace(); + } + } + + /** + * 获取注解中对方法的描述信息 用于Controller层注解 + * + * @param log 日志 + * @param operLog 操作日志 + * @throws Exception + */ + public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception { + // 设置action动作 + operLog.setBusinessType(log.businessType().ordinal()); + // 设置标题 + operLog.setTitle(log.title()); + // 设置操作人类别 + operLog.setOperatorType(log.operatorType().ordinal()); + // 是否需要保存request,参数和值 + if (log.isSaveRequestData()) { + // 获取参数的信息,传入到数据库中。 + setRequestValue(joinPoint, operLog); + } + // 是否需要保存response,参数和值 + if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult)) { + operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000)); + } + } + + /** + * 获取请求的参数,放到log中 + * + * @param operLog 操作日志 + * @throws Exception 异常 + */ + private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog) throws Exception { + String requestMethod = operLog.getRequestMethod(); + if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) { + String params = argsArrayToString(joinPoint.getArgs()); + operLog.setOperParam(StringUtils.substring(params, 0, 2000)); + } else { + Map paramsMap = (Map) ServletUtils.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + operLog.setOperParam(StringUtils.substring(paramsMap.toString(), 0, 2000)); + } + } + + /** + * 参数拼装 + */ + private String argsArrayToString(Object[] paramsArray) { + String params = ""; + if (paramsArray != null && paramsArray.length > 0) { + for (Object o : paramsArray) { + if (StringUtils.isNotNull(o) && !isFilterObject(o)) { + try { + String jsonObj = JSON.toJSONString(o, excludePropertyPreFilter()); + params += jsonObj.toString() + " "; + } catch (Exception e) { + } + } + } + } + return params.trim(); + } + + /** + * 忽略敏感属性 + */ + public PropertyPreExcludeFilter excludePropertyPreFilter() { + return new PropertyPreExcludeFilter().addExcludes(EXCLUDE_PROPERTIES); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param o 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + @SuppressWarnings("rawtypes") + public boolean isFilterObject(final Object o) { + Class clazz = o.getClass(); + if (clazz.isArray()) { + return clazz.getComponentType().isAssignableFrom(MultipartFile.class); + } else if (Collection.class.isAssignableFrom(clazz)) { + Collection collection = (Collection) o; + for (Object value : collection) { + return value instanceof MultipartFile; + } + } else if (Map.class.isAssignableFrom(clazz)) { + Map map = (Map) o; + for (Object value : map.entrySet()) { + Map.Entry entry = (Map.Entry) value; + return entry.getValue() instanceof MultipartFile; + } + } + return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse + || o instanceof BindingResult; + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/aspectj/RateLimiterAspect.java b/jsowell-framework/src/main/java/com/jsowell/framework/aspectj/RateLimiterAspect.java new file mode 100644 index 000000000..12cddc5cc --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/aspectj/RateLimiterAspect.java @@ -0,0 +1,80 @@ +package com.jsowell.framework.aspectj; + +import com.jsowell.common.annotation.RateLimiter; +import com.jsowell.common.enums.LimitType; +import com.jsowell.common.exception.ServiceException; +import com.jsowell.common.util.ServletUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.ip.IpUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; + +/** + * 限流处理 + * + * @author jsowell + */ +@Aspect +@Component +public class RateLimiterAspect { + private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class); + + private RedisTemplate redisTemplate; + + private RedisScript limitScript; + + @Autowired + public void setRedisTemplate1(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + @Autowired + public void setLimitScript(RedisScript limitScript) { + this.limitScript = limitScript; + } + + @Before("@annotation(rateLimiter)") + public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable { + String key = rateLimiter.key(); + int time = rateLimiter.time(); + int count = rateLimiter.count(); + + String combineKey = getCombineKey(rateLimiter, point); + List keys = Collections.singletonList(combineKey); + try { + Long number = redisTemplate.execute(limitScript, keys, count, time); + if (StringUtils.isNull(number) || number.intValue() > count) { + throw new ServiceException("访问过于频繁,请稍候再试"); + } + log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), key); + } catch (ServiceException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("服务器限流异常,请稍候再试"); + } + } + + public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) { + StringBuffer stringBuffer = new StringBuffer(rateLimiter.key()); + if (rateLimiter.limitType() == LimitType.IP) { + stringBuffer.append(IpUtils.getIpAddr(ServletUtils.getRequest())).append("-"); + } + MethodSignature signature = (MethodSignature) point.getSignature(); + Method method = signature.getMethod(); + Class targetClass = method.getDeclaringClass(); + stringBuffer.append(targetClass.getName()).append("-").append(method.getName()); + return stringBuffer.toString(); + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/config/ApplicationConfig.java b/jsowell-framework/src/main/java/com/jsowell/framework/config/ApplicationConfig.java new file mode 100644 index 000000000..e9984beb9 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/config/ApplicationConfig.java @@ -0,0 +1,29 @@ +package com.jsowell.framework.config; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +import java.util.TimeZone; + +/** + * 程序注解配置 + * + * @author jsowell + */ +@Configuration +// 表示通过aop框架暴露该代理对象,AopContext能够访问 +@EnableAspectJAutoProxy(exposeProxy = true) +// 指定要扫描的Mapper类的包的路径 +@MapperScan("com.jsowell.**.mapper") +public class ApplicationConfig { + /** + * 时区配置 + */ + @Bean + public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization() { + return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault()); + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/config/CaptchaConfig.java b/jsowell-framework/src/main/java/com/jsowell/framework/config/CaptchaConfig.java new file mode 100644 index 000000000..fe52dce81 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/config/CaptchaConfig.java @@ -0,0 +1,84 @@ +package com.jsowell.framework.config; + +import com.google.code.kaptcha.impl.DefaultKaptcha; +import com.google.code.kaptcha.util.Config; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Properties; + +import static com.google.code.kaptcha.Constants.*; + +/** + * 验证码配置 + * + * @author jsowell + */ +@Configuration +public class CaptchaConfig { + @Bean(name = "captchaProducer") + public DefaultKaptcha getKaptchaBean() { + DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); + Properties properties = new Properties(); + // 是否有边框 默认为true 我们可以自己设置yes,no + properties.setProperty(KAPTCHA_BORDER, "yes"); + // 验证码文本字符颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black"); + // 验证码图片宽度 默认为200 + properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160"); + // 验证码图片高度 默认为50 + properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60"); + // 验证码文本字符大小 默认为40 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38"); + // KAPTCHA_SESSION_KEY + properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode"); + // 文本集合,验证码值从此集合中获取 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_STRING, "012356789"); + // 验证码文本字符长度 默认为5 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4"); + // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize) + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier"); + // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy + properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); + Config config = new Config(properties); + defaultKaptcha.setConfig(config); + return defaultKaptcha; + } + + @Bean(name = "captchaProducerMath") + public DefaultKaptcha getKaptchaBeanMath() { + DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); + Properties properties = new Properties(); + // 是否有边框 默认为true 我们可以自己设置yes,no + properties.setProperty(KAPTCHA_BORDER, "yes"); + // 边框颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90"); + // 验证码文本字符颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue"); + // 验证码图片宽度 默认为200 + properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160"); + // 验证码图片高度 默认为50 + properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60"); + // 验证码文本字符大小 默认为40 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35"); + // KAPTCHA_SESSION_KEY + properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath"); + // 验证码文本生成器 + properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.jsowell.framework.config.KaptchaTextCreator"); + // 验证码文本字符间距 默认为2 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3"); + // 验证码文本字符长度 默认为5 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6"); + // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize) + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier"); + // 验证码噪点颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_NOISE_COLOR, "white"); + // 干扰实现类 + properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise"); + // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy + properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); + Config config = new Config(properties); + defaultKaptcha.setConfig(config); + return defaultKaptcha; + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/config/DruidConfig.java b/jsowell-framework/src/main/java/com/jsowell/framework/config/DruidConfig.java new file mode 100644 index 000000000..95650c205 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/config/DruidConfig.java @@ -0,0 +1,112 @@ +package com.jsowell.framework.config; + +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; +import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties; +import com.alibaba.druid.util.Utils; +import com.jsowell.common.enums.DataSourceType; +import com.jsowell.common.util.spring.SpringUtils; +import com.jsowell.framework.config.properties.DruidProperties; +import com.jsowell.framework.datasource.DynamicDataSource; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import javax.servlet.*; +import javax.sql.DataSource; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * druid 配置多数据源 + * + * @author jsowell + */ +@Configuration +public class DruidConfig { + @Bean + @ConfigurationProperties("spring.datasource.druid.master") + public DataSource masterDataSource(DruidProperties druidProperties) { + DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); + return druidProperties.dataSource(dataSource); + } + + @Bean + @ConfigurationProperties("spring.datasource.druid.slave") + @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true") + public DataSource slaveDataSource(DruidProperties druidProperties) { + DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); + return druidProperties.dataSource(dataSource); + } + + @Bean(name = "dynamicDataSource") + @Primary + public DynamicDataSource dataSource(DataSource masterDataSource) { + Map targetDataSources = new HashMap<>(); + targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource); + setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource"); + return new DynamicDataSource(masterDataSource, targetDataSources); + } + + /** + * 设置数据源 + * + * @param targetDataSources 备选数据源集合 + * @param sourceName 数据源名称 + * @param beanName bean名称 + */ + public void setDataSource(Map targetDataSources, String sourceName, String beanName) { + try { + DataSource dataSource = SpringUtils.getBean(beanName); + targetDataSources.put(sourceName, dataSource); + } catch (Exception e) { + } + } + + /** + * 去除监控页面底部的广告 + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + @Bean + @ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true") + public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties) { + // 获取web监控页面的参数 + DruidStatProperties.StatViewServlet config = properties.getStatViewServlet(); + // 提取common.js的配置路径 + String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*"; + String commonJsPattern = pattern.replaceAll("\\*", "js/common.js"); + final String filePath = "support/http/resources/js/common.js"; + // 创建filter进行过滤 + Filter filter = new Filter() { + @Override + public void init(javax.servlet.FilterConfig filterConfig) throws ServletException { + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + chain.doFilter(request, response); + // 重置缓冲区,响应头不会被重置 + response.resetBuffer(); + // 获取common.js + String text = Utils.readFromResource(filePath); + // 正则替换banner, 除去底部的广告信息 + text = text.replaceAll("
", ""); + text = text.replaceAll("powered.*?shrek.wang", ""); + response.getWriter().write(text); + } + + @Override + public void destroy() { + } + }; + FilterRegistrationBean registrationBean = new FilterRegistrationBean(); + registrationBean.setFilter(filter); + registrationBean.addUrlPatterns(commonJsPattern); + return registrationBean; + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/config/FastJson2JsonRedisSerializer.java b/jsowell-framework/src/main/java/com/jsowell/framework/config/FastJson2JsonRedisSerializer.java new file mode 100644 index 000000000..83425e3e9 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/config/FastJson2JsonRedisSerializer.java @@ -0,0 +1,43 @@ +package com.jsowell.framework.config; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.JSONWriter; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; + +import java.nio.charset.Charset; + +/** + * Redis使用FastJson序列化 + * + * @author jsowell + */ +public class FastJson2JsonRedisSerializer implements RedisSerializer { + public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + + private Class clazz; + + public FastJson2JsonRedisSerializer(Class clazz) { + super(); + this.clazz = clazz; + } + + @Override + public byte[] serialize(T t) throws SerializationException { + if (t == null) { + return new byte[0]; + } + return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET); + } + + @Override + public T deserialize(byte[] bytes) throws SerializationException { + if (bytes == null || bytes.length <= 0) { + return null; + } + String str = new String(bytes, DEFAULT_CHARSET); + + return JSON.parseObject(str, clazz, JSONReader.Feature.SupportAutoType); + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/config/FilterConfig.java b/jsowell-framework/src/main/java/com/jsowell/framework/config/FilterConfig.java new file mode 100644 index 000000000..6be571a5e --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/config/FilterConfig.java @@ -0,0 +1,56 @@ +package com.jsowell.framework.config; + +import com.jsowell.common.filter.RepeatableFilter; +import com.jsowell.common.filter.XssFilter; +import com.jsowell.common.util.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.servlet.DispatcherType; +import java.util.HashMap; +import java.util.Map; + +/** + * Filter配置 + * + * @author jsowell + */ +@Configuration +public class FilterConfig { + @Value("${xss.excludes}") + private String excludes; + + @Value("${xss.urlPatterns}") + private String urlPatterns; + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Bean + @ConditionalOnProperty(value = "xss.enabled", havingValue = "true") + public FilterRegistrationBean xssFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setDispatcherTypes(DispatcherType.REQUEST); + registration.setFilter(new XssFilter()); + registration.addUrlPatterns(StringUtils.split(urlPatterns, ",")); + registration.setName("xssFilter"); + registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE); + Map initParameters = new HashMap(); + initParameters.put("excludes", excludes); + registration.setInitParameters(initParameters); + return registration; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Bean + public FilterRegistrationBean someFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setFilter(new RepeatableFilter()); + registration.addUrlPatterns("/*"); + registration.setName("repeatableFilter"); + registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE); + return registration; + } + +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/config/KaptchaTextCreator.java b/jsowell-framework/src/main/java/com/jsowell/framework/config/KaptchaTextCreator.java new file mode 100644 index 000000000..fabe6024f --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/config/KaptchaTextCreator.java @@ -0,0 +1,56 @@ +package com.jsowell.framework.config; + +import com.google.code.kaptcha.text.impl.DefaultTextCreator; + +import java.util.Random; + +/** + * 验证码文本生成器 + * + * @author jsowell + */ +public class KaptchaTextCreator extends DefaultTextCreator { + private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(","); + + @Override + public String getText() { + Integer result = 0; + Random random = new Random(); + int x = random.nextInt(10); + int y = random.nextInt(10); + StringBuilder suChinese = new StringBuilder(); + int randomoperands = random.nextInt(3); + if (randomoperands == 0) { + result = x * y; + suChinese.append(CNUMBERS[x]); + suChinese.append("*"); + suChinese.append(CNUMBERS[y]); + } else if (randomoperands == 1) { + if ((x != 0) && y % x == 0) { + result = y / x; + suChinese.append(CNUMBERS[y]); + suChinese.append("/"); + suChinese.append(CNUMBERS[x]); + } else { + result = x + y; + suChinese.append(CNUMBERS[x]); + suChinese.append("+"); + suChinese.append(CNUMBERS[y]); + } + } else { + if (x >= y) { + result = x - y; + suChinese.append(CNUMBERS[x]); + suChinese.append("-"); + suChinese.append(CNUMBERS[y]); + } else { + result = y - x; + suChinese.append(CNUMBERS[y]); + suChinese.append("-"); + suChinese.append(CNUMBERS[x]); + } + } + suChinese.append("=?@" + result); + return suChinese.toString(); + } +} \ No newline at end of file diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/config/MyBatisConfig.java b/jsowell-framework/src/main/java/com/jsowell/framework/config/MyBatisConfig.java new file mode 100644 index 000000000..d16425150 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/config/MyBatisConfig.java @@ -0,0 +1,110 @@ +package com.jsowell.framework.config; + +import com.jsowell.common.util.StringUtils; +import org.apache.ibatis.io.VFS; +import org.apache.ibatis.session.SqlSessionFactory; +import org.mybatis.spring.SqlSessionFactoryBean; +import org.mybatis.spring.boot.autoconfigure.SpringBootVFS; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.util.ClassUtils; + +import javax.sql.DataSource; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +/** + * Mybatis支持*匹配扫描包 + * + * @author jsowell + */ +@Configuration +public class MyBatisConfig { + @Autowired + private Environment env; + + static final String DEFAULT_RESOURCE_PATTERN = "**/*.class"; + + public static String setTypeAliasesPackage(String typeAliasesPackage) { + ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver(); + MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver); + List allResult = new ArrayList(); + try { + for (String aliasesPackage : typeAliasesPackage.split(",")) { + List result = new ArrayList(); + aliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + + ClassUtils.convertClassNameToResourcePath(aliasesPackage.trim()) + "/" + DEFAULT_RESOURCE_PATTERN; + Resource[] resources = resolver.getResources(aliasesPackage); + if (resources != null && resources.length > 0) { + MetadataReader metadataReader = null; + for (Resource resource : resources) { + if (resource.isReadable()) { + metadataReader = metadataReaderFactory.getMetadataReader(resource); + try { + result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName()); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } + } + } + if (result.size() > 0) { + HashSet hashResult = new HashSet(result); + allResult.addAll(hashResult); + } + } + if (allResult.size() > 0) { + typeAliasesPackage = String.join(",", (String[]) allResult.toArray(new String[0])); + } else { + throw new RuntimeException("mybatis typeAliasesPackage 路径扫描错误,参数typeAliasesPackage:" + typeAliasesPackage + "未找到任何包"); + } + } catch (IOException e) { + e.printStackTrace(); + } + return typeAliasesPackage; + } + + public Resource[] resolveMapperLocations(String[] mapperLocations) { + ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver(); + List resources = new ArrayList(); + if (mapperLocations != null) { + for (String mapperLocation : mapperLocations) { + try { + Resource[] mappers = resourceResolver.getResources(mapperLocation); + resources.addAll(Arrays.asList(mappers)); + } catch (IOException e) { + // ignore + } + } + } + return resources.toArray(new Resource[resources.size()]); + } + + @Bean + public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { + String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage"); + String mapperLocations = env.getProperty("mybatis.mapperLocations"); + String configLocation = env.getProperty("mybatis.configLocation"); + typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage); + VFS.addImplClass(SpringBootVFS.class); + + final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); + sessionFactory.setDataSource(dataSource); + sessionFactory.setTypeAliasesPackage(typeAliasesPackage); + sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ","))); + sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation)); + return sessionFactory.getObject(); + } +} \ No newline at end of file diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/config/RedisConfig.java b/jsowell-framework/src/main/java/com/jsowell/framework/config/RedisConfig.java new file mode 100644 index 000000000..b691c5b6b --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/config/RedisConfig.java @@ -0,0 +1,65 @@ +package com.jsowell.framework.config; + +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * redis配置 + * + * @author jsowell + */ +@Configuration +@EnableCaching +public class RedisConfig extends CachingConfigurerSupport { + @Bean + @SuppressWarnings(value = {"unchecked", "rawtypes"}) + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class); + + // 使用StringRedisSerializer来序列化和反序列化redis的key值 + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(serializer); + + // Hash的key也采用StringRedisSerializer的序列化方式 + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(serializer); + + template.afterPropertiesSet(); + return template; + } + + @Bean + public DefaultRedisScript limitScript() { + DefaultRedisScript redisScript = new DefaultRedisScript<>(); + redisScript.setScriptText(limitScriptText()); + redisScript.setResultType(Long.class); + return redisScript; + } + + /** + * 限流脚本 + */ + private String limitScriptText() { + return "local key = KEYS[1]\n" + + "local count = tonumber(ARGV[1])\n" + + "local time = tonumber(ARGV[2])\n" + + "local current = redis.call('get', key);\n" + + "if current and tonumber(current) > count then\n" + + " return tonumber(current);\n" + + "end\n" + + "current = redis.call('incr', key)\n" + + "if tonumber(current) == 1 then\n" + + " redis.call('expire', key, time)\n" + + "end\n" + + "return tonumber(current);"; + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/config/ResourcesConfig.java b/jsowell-framework/src/main/java/com/jsowell/framework/config/ResourcesConfig.java new file mode 100644 index 000000000..61a8d39bf --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/config/ResourcesConfig.java @@ -0,0 +1,66 @@ +package com.jsowell.framework.config; + +import org.springframework.beans.factory.annotation.Autowired; +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 org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import com.jsowell.common.config.JsowellConfig; +import com.jsowell.common.constant.Constants; +import com.jsowell.framework.interceptor.RepeatSubmitInterceptor; + +/** + * 通用配置 + * + * @author jsowell + */ +@Configuration +public class ResourcesConfig implements WebMvcConfigurer { + @Autowired + private RepeatSubmitInterceptor repeatSubmitInterceptor; + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + /** 本地文件上传路径 */ + registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**") + .addResourceLocations("file:" + JsowellConfig.getProfile() + "/"); + + /** swagger配置 */ + registry.addResourceHandler("/swagger-ui/**") + .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/"); + } + + /** + * 自定义拦截规则 + */ + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**"); + } + + /** + * 跨域配置 + */ + @Bean + public CorsFilter corsFilter() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + // 设置访问源地址 + config.addAllowedOriginPattern("*"); + // 设置访问源请求头 + config.addAllowedHeader("*"); + // 设置访问源请求方法 + config.addAllowedMethod("*"); + // 有效期 1800秒 + config.setMaxAge(1800L); + // 添加映射路径,拦截一切请求 + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + // 返回新的CorsFilter + return new CorsFilter(source); + } +} \ No newline at end of file diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/config/SecurityConfig.java b/jsowell-framework/src/main/java/com/jsowell/framework/config/SecurityConfig.java new file mode 100644 index 000000000..0f101dd1f --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/config/SecurityConfig.java @@ -0,0 +1,141 @@ +package com.jsowell.framework.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.web.filter.CorsFilter; +import com.jsowell.framework.config.properties.PermitAllUrlProperties; +import com.jsowell.framework.security.filter.JwtAuthenticationTokenFilter; +import com.jsowell.framework.security.handle.AuthenticationEntryPointImpl; +import com.jsowell.framework.security.handle.LogoutSuccessHandlerImpl; + +/** + * spring security配置 + * + * @author jsowell + */ +@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) +public class SecurityConfig extends WebSecurityConfigurerAdapter { + /** + * 自定义用户认证逻辑 + */ + @Autowired + private UserDetailsService userDetailsService; + + /** + * 认证失败处理类 + */ + @Autowired + private AuthenticationEntryPointImpl unauthorizedHandler; + + /** + * 退出处理类 + */ + @Autowired + private LogoutSuccessHandlerImpl logoutSuccessHandler; + + /** + * token认证过滤器 + */ + @Autowired + private JwtAuthenticationTokenFilter authenticationTokenFilter; + + /** + * 跨域过滤器 + */ + @Autowired + private CorsFilter corsFilter; + + /** + * 允许匿名访问的地址 + */ + @Autowired + private PermitAllUrlProperties permitAllUrl; + + /** + * 解决 无法直接注入 AuthenticationManager + * + * @return + * @throws Exception + */ + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + /** + * anyRequest | 匹配所有请求路径 + * access | SpringEl表达式结果为true时可以访问 + * anonymous | 匿名可以访问 + * denyAll | 用户不能访问 + * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录) + * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问 + * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问 + * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问 + * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 + * hasRole | 如果有参数,参数表示角色,则其角色可以访问 + * permitAll | 用户可以任意访问 + * rememberMe | 允许通过remember-me登录的用户访问 + * authenticated | 用户登录后可访问 + */ + @Override + protected void configure(HttpSecurity httpSecurity) throws Exception { + // 注解标记允许匿名访问的url + ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests(); + permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll()); + + httpSecurity + // CSRF禁用,因为不使用session + .csrf().disable() + // 认证失败处理类 + .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() + // 基于token,所以不需要session + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() + // 过滤请求 + .authorizeRequests() + // 对于登录login 注册register 验证码captchaImage 允许匿名访问 + .antMatchers("/login", "/register", "/captchaImage").anonymous() + // 静态资源,可匿名访问 + .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() + .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll() + // 除上面外的所有请求全部需要鉴权认证 + .anyRequest().authenticated() + .and() + .headers().frameOptions().disable(); + // 添加Logout filter + httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); + // 添加JWT filter + httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); + // 添加CORS filter + httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class); + httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class); + } + + /** + * 强散列哈希加密实现 + */ + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() { + return new BCryptPasswordEncoder(); + } + + /** + * 身份认证接口 + */ + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/config/ServerConfig.java b/jsowell-framework/src/main/java/com/jsowell/framework/config/ServerConfig.java new file mode 100644 index 000000000..3b68cc3dc --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/config/ServerConfig.java @@ -0,0 +1,30 @@ +package com.jsowell.framework.config; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.stereotype.Component; +import com.jsowell.common.util.ServletUtils; + +/** + * 服务相关配置 + * + * @author jsowell + */ +@Component +public class ServerConfig { + /** + * 获取完整的请求路径,包括:域名,端口,上下文访问路径 + * + * @return 服务地址 + */ + public String getUrl() { + HttpServletRequest request = ServletUtils.getRequest(); + return getDomain(request); + } + + public static String getDomain(HttpServletRequest request) { + StringBuffer url = request.getRequestURL(); + String contextPath = request.getServletContext().getContextPath(); + return url.delete(url.length() - request.getRequestURI().length(), url.length()).append(contextPath).toString(); + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/config/ThreadPoolConfig.java b/jsowell-framework/src/main/java/com/jsowell/framework/config/ThreadPoolConfig.java new file mode 100644 index 000000000..628cd7bcf --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/config/ThreadPoolConfig.java @@ -0,0 +1,62 @@ +package com.jsowell.framework.config; + +import com.jsowell.common.util.Threads; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 线程池配置 + * + * @author jsowell + **/ +@Configuration +public class ThreadPoolConfig { + // 核心线程池大小 + private final int corePoolSize = 50; + + // 最大可创建的线程数 + private final int maxPoolSize = 200; + + // 队列最大长度 + private final int queueCapacity = 1000; + + // 线程池维护线程所允许的空闲时间 + private final int keepAliveSeconds = 300; + + /** + * 线程池 + */ + @Bean(name = "threadPoolTaskExecutor") + public ThreadPoolTaskExecutor threadPoolTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setMaxPoolSize(maxPoolSize); + executor.setCorePoolSize(corePoolSize); + executor.setQueueCapacity(queueCapacity); + executor.setKeepAliveSeconds(keepAliveSeconds); + // 线程池对拒绝任务(无线程可用)的处理策略 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + return executor; + } + + /** + * 执行周期性或定时任务 + */ + @Bean(name = "scheduledExecutorService") + protected ScheduledExecutorService scheduledExecutorService() { + return new ScheduledThreadPoolExecutor(corePoolSize, + new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(), + new ThreadPoolExecutor.CallerRunsPolicy()) { + @Override + protected void afterExecute(Runnable r, Throwable t) { + super.afterExecute(r, t); + Threads.printException(r, t); + } + }; + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/config/properties/DruidProperties.java b/jsowell-framework/src/main/java/com/jsowell/framework/config/properties/DruidProperties.java new file mode 100644 index 000000000..3f433936d --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/config/properties/DruidProperties.java @@ -0,0 +1,75 @@ +package com.jsowell.framework.config.properties; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import com.alibaba.druid.pool.DruidDataSource; + +/** + * druid 配置属性 + * + * @author jsowell + */ +@Configuration +public class DruidProperties { + @Value("${spring.datasource.druid.initialSize}") + private int initialSize; + + @Value("${spring.datasource.druid.minIdle}") + private int minIdle; + + @Value("${spring.datasource.druid.maxActive}") + private int maxActive; + + @Value("${spring.datasource.druid.maxWait}") + private int maxWait; + + @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}") + private int timeBetweenEvictionRunsMillis; + + @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}") + private int minEvictableIdleTimeMillis; + + @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}") + private int maxEvictableIdleTimeMillis; + + @Value("${spring.datasource.druid.validationQuery}") + private String validationQuery; + + @Value("${spring.datasource.druid.testWhileIdle}") + private boolean testWhileIdle; + + @Value("${spring.datasource.druid.testOnBorrow}") + private boolean testOnBorrow; + + @Value("${spring.datasource.druid.testOnReturn}") + private boolean testOnReturn; + + public DruidDataSource dataSource(DruidDataSource datasource) { + /** 配置初始化大小、最小、最大 */ + datasource.setInitialSize(initialSize); + datasource.setMaxActive(maxActive); + datasource.setMinIdle(minIdle); + + /** 配置获取连接等待超时的时间 */ + datasource.setMaxWait(maxWait); + + /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */ + datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); + + /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */ + datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); + datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis); + + /** + * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。 + */ + datasource.setValidationQuery(validationQuery); + /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */ + datasource.setTestWhileIdle(testWhileIdle); + /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */ + datasource.setTestOnBorrow(testOnBorrow); + /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */ + datasource.setTestOnReturn(testOnReturn); + return datasource; + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/config/properties/PermitAllUrlProperties.java b/jsowell-framework/src/main/java/com/jsowell/framework/config/properties/PermitAllUrlProperties.java new file mode 100644 index 000000000..8a5b680e6 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/config/properties/PermitAllUrlProperties.java @@ -0,0 +1,68 @@ +package com.jsowell.framework.config.properties; + +import com.jsowell.common.annotation.Anonymous; +import org.apache.commons.lang3.RegExUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; + +/** + * 设置Anonymous注解允许匿名访问的url + * + * @author jsowell + */ +@Configuration +public class PermitAllUrlProperties implements InitializingBean, ApplicationContextAware { + private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}"); + + private ApplicationContext applicationContext; + + private List urls = new ArrayList<>(); + + public String ASTERISK = "*"; + + @Override + public void afterPropertiesSet() { + RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class); + Map map = mapping.getHandlerMethods(); + + map.keySet().forEach(info -> { + HandlerMethod handlerMethod = map.get(info); + + // 获取方法上边的注解 替代path variable 为 * + Anonymous method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Anonymous.class); + Optional.ofNullable(method).ifPresent(anonymous -> info.getPatternsCondition().getPatterns() + .forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK)))); + + // 获取类上边的注解, 替代path variable 为 * + Anonymous controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Anonymous.class); + Optional.ofNullable(controller).ifPresent(anonymous -> info.getPatternsCondition().getPatterns() + .forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK)))); + }); + } + + @Override + public void setApplicationContext(ApplicationContext context) throws BeansException { + this.applicationContext = context; + } + + public List getUrls() { + return urls; + } + + public void setUrls(List urls) { + this.urls = urls; + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/datasource/DynamicDataSource.java b/jsowell-framework/src/main/java/com/jsowell/framework/datasource/DynamicDataSource.java new file mode 100644 index 000000000..a1ebaa7f7 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/datasource/DynamicDataSource.java @@ -0,0 +1,24 @@ +package com.jsowell.framework.datasource; + +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; + +import javax.sql.DataSource; +import java.util.Map; + +/** + * 动态数据源 + * + * @author jsowell + */ +public class DynamicDataSource extends AbstractRoutingDataSource { + public DynamicDataSource(DataSource defaultTargetDataSource, Map targetDataSources) { + super.setDefaultTargetDataSource(defaultTargetDataSource); + super.setTargetDataSources(targetDataSources); + super.afterPropertiesSet(); + } + + @Override + protected Object determineCurrentLookupKey() { + return DynamicDataSourceContextHolder.getDataSourceType(); + } +} \ No newline at end of file diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/datasource/DynamicDataSourceContextHolder.java b/jsowell-framework/src/main/java/com/jsowell/framework/datasource/DynamicDataSourceContextHolder.java new file mode 100644 index 000000000..9f1c64c51 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/datasource/DynamicDataSourceContextHolder.java @@ -0,0 +1,41 @@ +package com.jsowell.framework.datasource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 数据源切换处理 + * + * @author jsowell + */ +public class DynamicDataSourceContextHolder { + public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class); + + /** + * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本, + * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。 + */ + private static final ThreadLocal CONTEXT_HOLDER = new ThreadLocal<>(); + + /** + * 设置数据源的变量 + */ + public static void setDataSourceType(String dsType) { + log.info("切换到{}数据源", dsType); + CONTEXT_HOLDER.set(dsType); + } + + /** + * 获得数据源的变量 + */ + public static String getDataSourceType() { + return CONTEXT_HOLDER.get(); + } + + /** + * 清空数据源变量 + */ + public static void clearDataSourceType() { + CONTEXT_HOLDER.remove(); + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/interceptor/RepeatSubmitInterceptor.java b/jsowell-framework/src/main/java/com/jsowell/framework/interceptor/RepeatSubmitInterceptor.java new file mode 100644 index 000000000..1dcdc76ec --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/interceptor/RepeatSubmitInterceptor.java @@ -0,0 +1,49 @@ +package com.jsowell.framework.interceptor; + +import com.alibaba.fastjson2.JSON; +import com.jsowell.common.annotation.RepeatSubmit; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.util.ServletUtils; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.lang.reflect.Method; + +/** + * 防止重复提交拦截器 + * + * @author jsowell + */ +@Component +public abstract class RepeatSubmitInterceptor implements HandlerInterceptor { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + if (handler instanceof HandlerMethod) { + HandlerMethod handlerMethod = (HandlerMethod) handler; + Method method = handlerMethod.getMethod(); + RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class); + if (annotation != null) { + if (this.isRepeatSubmit(request, annotation)) { + AjaxResult ajaxResult = AjaxResult.error(annotation.message()); + ServletUtils.renderString(response, JSON.toJSONString(ajaxResult)); + return false; + } + } + return true; + } else { + return true; + } + } + + /** + * 验证是否重复提交由子类实现具体的防重复提交的规则 + * + * @param request + * @return + * @throws Exception + */ + public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation); +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/interceptor/impl/SameUrlDataInterceptor.java b/jsowell-framework/src/main/java/com/jsowell/framework/interceptor/impl/SameUrlDataInterceptor.java new file mode 100644 index 000000000..34f05fc56 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/interceptor/impl/SameUrlDataInterceptor.java @@ -0,0 +1,101 @@ +package com.jsowell.framework.interceptor.impl; + +import com.alibaba.fastjson2.JSON; +import com.jsowell.common.annotation.RepeatSubmit; +import com.jsowell.common.constant.CacheConstants; +import com.jsowell.common.core.redis.RedisCache; +import com.jsowell.common.filter.RepeatedlyRequestWrapper; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.http.HttpHelper; +import com.jsowell.framework.interceptor.RepeatSubmitInterceptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * 判断请求url和数据是否和上一次相同, + * 如果和上次相同,则是重复提交表单。 有效时间为10秒内。 + * + * @author jsowell + */ +@Component +public class SameUrlDataInterceptor extends RepeatSubmitInterceptor { + public final String REPEAT_PARAMS = "repeatParams"; + + public final String REPEAT_TIME = "repeatTime"; + + // 令牌自定义标识 + @Value("${token.header}") + private String header; + + @Autowired + private RedisCache redisCache; + + @SuppressWarnings("unchecked") + @Override + public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) { + String nowParams = ""; + if (request instanceof RepeatedlyRequestWrapper) { + RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request; + nowParams = HttpHelper.getBodyString(repeatedlyRequest); + } + + // body参数为空,获取Parameter的数据 + if (StringUtils.isEmpty(nowParams)) { + nowParams = JSON.toJSONString(request.getParameterMap()); + } + Map nowDataMap = new HashMap(); + nowDataMap.put(REPEAT_PARAMS, nowParams); + nowDataMap.put(REPEAT_TIME, System.currentTimeMillis()); + + // 请求地址(作为存放cache的key值) + String url = request.getRequestURI(); + + // 唯一值(没有消息头则使用请求地址) + String submitKey = StringUtils.trimToEmpty(request.getHeader(header)); + + // 唯一标识(指定key + url + 消息头) + String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey; + + Object sessionObj = redisCache.getCacheObject(cacheRepeatKey); + if (sessionObj != null) { + Map sessionMap = (Map) sessionObj; + if (sessionMap.containsKey(url)) { + Map preDataMap = (Map) sessionMap.get(url); + if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval())) { + return true; + } + } + } + Map cacheMap = new HashMap(); + cacheMap.put(url, nowDataMap); + redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS); + return false; + } + + /** + * 判断参数是否相同 + */ + private boolean compareParams(Map nowMap, Map preMap) { + String nowParams = (String) nowMap.get(REPEAT_PARAMS); + String preParams = (String) preMap.get(REPEAT_PARAMS); + return nowParams.equals(preParams); + } + + /** + * 判断两次间隔时间 + */ + private boolean compareTime(Map nowMap, Map preMap, int interval) { + long time1 = (Long) nowMap.get(REPEAT_TIME); + long time2 = (Long) preMap.get(REPEAT_TIME); + if ((time1 - time2) < interval) { + return true; + } + return false; + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/manager/AsyncManager.java b/jsowell-framework/src/main/java/com/jsowell/framework/manager/AsyncManager.java new file mode 100644 index 000000000..e41cbc459 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/manager/AsyncManager.java @@ -0,0 +1,53 @@ +package com.jsowell.framework.manager; + +import com.jsowell.common.util.Threads; +import com.jsowell.common.util.spring.SpringUtils; + +import java.util.TimerTask; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * 异步任务管理器 + * + * @author jsowell + */ +public class AsyncManager { + /** + * 操作延迟10毫秒 + */ + private final int OPERATE_DELAY_TIME = 10; + + /** + * 异步操作任务调度线程池 + */ + private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService"); + + /** + * 单例模式 + */ + private AsyncManager() { + } + + private static AsyncManager me = new AsyncManager(); + + public static AsyncManager me() { + return me; + } + + /** + * 执行任务 + * + * @param task 任务 + */ + public void execute(TimerTask task) { + executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS); + } + + /** + * 停止任务线程池 + */ + public void shutdown() { + Threads.shutdownAndAwaitTermination(executor); + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/manager/ShutdownManager.java b/jsowell-framework/src/main/java/com/jsowell/framework/manager/ShutdownManager.java new file mode 100644 index 000000000..d3ebb9294 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/manager/ShutdownManager.java @@ -0,0 +1,34 @@ +package com.jsowell.framework.manager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.PreDestroy; + +/** + * 确保应用退出时能关闭后台线程 + * + * @author jsowell + */ +@Component +public class ShutdownManager { + private static final Logger logger = LoggerFactory.getLogger("sys-user"); + + @PreDestroy + public void destroy() { + shutdownAsyncManager(); + } + + /** + * 停止异步执行任务 + */ + private void shutdownAsyncManager() { + try { + logger.info("====关闭后台任务任务线程池===="); + AsyncManager.me().shutdown(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/manager/factory/AsyncFactory.java b/jsowell-framework/src/main/java/com/jsowell/framework/manager/factory/AsyncFactory.java new file mode 100644 index 000000000..270106ced --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/manager/factory/AsyncFactory.java @@ -0,0 +1,93 @@ +package com.jsowell.framework.manager.factory; + +import com.jsowell.common.constant.Constants; +import com.jsowell.common.util.LogUtils; +import com.jsowell.common.util.ServletUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.ip.AddressUtils; +import com.jsowell.common.util.ip.IpUtils; +import com.jsowell.common.util.spring.SpringUtils; +import com.jsowell.system.domain.SysLogininfor; +import com.jsowell.system.domain.SysOperLog; +import com.jsowell.system.service.SysLogininforService; +import com.jsowell.system.service.SysOperLogService; +import eu.bitwalker.useragentutils.UserAgent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.TimerTask; + +/** + * 异步工厂(产生任务用) + * + * @author jsowell + */ +public class AsyncFactory { + private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user"); + + /** + * 记录登录信息 + * + * @param username 用户名 + * @param status 状态 + * @param message 消息 + * @param args 列表 + * @return 任务task + */ + public static TimerTask recordLogininfor(final String username, final String status, final String message, + final Object... args) { + final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent")); + final String ip = IpUtils.getIpAddr(ServletUtils.getRequest()); + return new TimerTask() { + @Override + public void run() { + String address = AddressUtils.getRealAddressByIP(ip); + StringBuilder s = new StringBuilder(); + s.append(LogUtils.getBlock(ip)); + s.append(address); + s.append(LogUtils.getBlock(username)); + s.append(LogUtils.getBlock(status)); + s.append(LogUtils.getBlock(message)); + // 打印信息到日志 + sys_user_logger.info(s.toString(), args); + // 获取客户端操作系统 + String os = userAgent.getOperatingSystem().getName(); + // 获取客户端浏览器 + String browser = userAgent.getBrowser().getName(); + // 封装对象 + SysLogininfor logininfor = new SysLogininfor(); + logininfor.setUserName(username); + logininfor.setIpaddr(ip); + logininfor.setLoginLocation(address); + logininfor.setBrowser(browser); + logininfor.setOs(os); + logininfor.setMsg(message); + // 日志状态 + if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER)) { + logininfor.setStatus(Constants.SUCCESS); + } else if (Constants.LOGIN_FAIL.equals(status)) { + logininfor.setStatus(Constants.FAIL); + } + // 插入数据 + SpringUtils.getBean(SysLogininforService.class).insertLogininfor(logininfor); + } + }; + } + + /** + * 操作日志记录 + * + * @param operLog 操作日志信息 + * @return 任务task + */ + public static TimerTask recordOper(final SysOperLog operLog) { + return new TimerTask() { + @Override + public void run() { + // 远程查询操作地点 + operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp())); + SpringUtils.getBean(SysOperLogService.class).insertOperlog(operLog); + } + }; + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/security/context/AuthenticationContextHolder.java b/jsowell-framework/src/main/java/com/jsowell/framework/security/context/AuthenticationContextHolder.java new file mode 100644 index 000000000..9a73b6e45 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/security/context/AuthenticationContextHolder.java @@ -0,0 +1,24 @@ +package com.jsowell.framework.security.context; + +import org.springframework.security.core.Authentication; + +/** + * 身份验证信息 + * + * @author jsowell + */ +public class AuthenticationContextHolder { + private static final ThreadLocal contextHolder = new ThreadLocal<>(); + + public static Authentication getContext() { + return contextHolder.get(); + } + + public static void setContext(Authentication context) { + contextHolder.set(context); + } + + public static void clearContext() { + contextHolder.remove(); + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/security/filter/JwtAuthenticationTokenFilter.java b/jsowell-framework/src/main/java/com/jsowell/framework/security/filter/JwtAuthenticationTokenFilter.java new file mode 100644 index 000000000..8fc2d948f --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/security/filter/JwtAuthenticationTokenFilter.java @@ -0,0 +1,42 @@ +package com.jsowell.framework.security.filter; + +import com.jsowell.common.core.domain.model.LoginUser; +import com.jsowell.common.util.SecurityUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.framework.web.service.TokenService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * token过滤器 验证token有效性 + * + * @author jsowell + */ +@Component +public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { + @Autowired + private TokenService tokenService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + LoginUser loginUser = tokenService.getLoginUser(request); + if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) { + tokenService.verifyToken(loginUser); + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + } + chain.doFilter(request, response); + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/security/handle/AuthenticationEntryPointImpl.java b/jsowell-framework/src/main/java/com/jsowell/framework/security/handle/AuthenticationEntryPointImpl.java new file mode 100644 index 000000000..b0e01e783 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/security/handle/AuthenticationEntryPointImpl.java @@ -0,0 +1,33 @@ +package com.jsowell.framework.security.handle; + +import com.alibaba.fastjson2.JSON; +import com.jsowell.common.constant.HttpStatus; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.util.ServletUtils; +import com.jsowell.common.util.StringUtils; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.Serializable; + +/** + * 认证失败处理类 返回未授权 + * + * @author jsowell + */ +@Component +public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable { + private static final long serialVersionUID = -8970718410437077606L; + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) + throws IOException { + int code = HttpStatus.UNAUTHORIZED; + String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI()); + ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg))); + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/security/handle/LogoutSuccessHandlerImpl.java b/jsowell-framework/src/main/java/com/jsowell/framework/security/handle/LogoutSuccessHandlerImpl.java new file mode 100644 index 000000000..b3adc1e37 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/security/handle/LogoutSuccessHandlerImpl.java @@ -0,0 +1,51 @@ +package com.jsowell.framework.security.handle; + +import com.alibaba.fastjson2.JSON; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.constant.HttpStatus; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.domain.model.LoginUser; +import com.jsowell.common.util.ServletUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.framework.manager.AsyncManager; +import com.jsowell.framework.manager.factory.AsyncFactory; +import com.jsowell.framework.web.service.TokenService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 自定义退出处理类 返回成功 + * + * @author jsowell + */ +@Configuration +public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler { + @Autowired + private TokenService tokenService; + + /** + * 退出处理 + * + * @return + */ + @Override + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) + throws IOException, ServletException { + LoginUser loginUser = tokenService.getLoginUser(request); + if (StringUtils.isNotNull(loginUser)) { + String userName = loginUser.getUsername(); + // 删除用户缓存记录 + tokenService.delLoginUser(loginUser.getToken()); + // 记录用户退出日志 + AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, "退出成功")); + } + ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(HttpStatus.SUCCESS, "退出成功"))); + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/web/domain/Server.java b/jsowell-framework/src/main/java/com/jsowell/framework/web/domain/Server.java new file mode 100644 index 000000000..1d046e192 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/web/domain/Server.java @@ -0,0 +1,215 @@ +package com.jsowell.framework.web.domain; + +import com.jsowell.common.util.Arith; +import com.jsowell.common.util.ip.IpUtils; +import com.jsowell.framework.web.domain.server.Cpu; +import com.jsowell.framework.web.domain.server.Jvm; +import com.jsowell.framework.web.domain.server.Mem; +import com.jsowell.framework.web.domain.server.Sys; +import com.jsowell.framework.web.domain.server.SysFile; +import oshi.SystemInfo; +import oshi.hardware.CentralProcessor; +import oshi.hardware.CentralProcessor.TickType; +import oshi.hardware.GlobalMemory; +import oshi.hardware.HardwareAbstractionLayer; +import oshi.software.os.FileSystem; +import oshi.software.os.OSFileStore; +import oshi.software.os.OperatingSystem; +import oshi.util.Util; + +import java.net.UnknownHostException; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; + +/** + * 服务器相关信息 + * + * @author jsowell + */ +public class Server { + private static final int OSHI_WAIT_SECOND = 1000; + + /** + * CPU相关信息 + */ + private Cpu cpu = new Cpu(); + + /** + * 內存相关信息 + */ + private Mem mem = new Mem(); + + /** + * JVM相关信息 + */ + private Jvm jvm = new Jvm(); + + /** + * 服务器相关信息 + */ + private Sys sys = new Sys(); + + /** + * 磁盘相关信息 + */ + private List sysFiles = new LinkedList(); + + public Cpu getCpu() { + return cpu; + } + + public void setCpu(Cpu cpu) { + this.cpu = cpu; + } + + public Mem getMem() { + return mem; + } + + public void setMem(Mem mem) { + this.mem = mem; + } + + public Jvm getJvm() { + return jvm; + } + + public void setJvm(Jvm jvm) { + this.jvm = jvm; + } + + public Sys getSys() { + return sys; + } + + public void setSys(Sys sys) { + this.sys = sys; + } + + public List getSysFiles() { + return sysFiles; + } + + public void setSysFiles(List sysFiles) { + this.sysFiles = sysFiles; + } + + public void copyTo() throws Exception { + SystemInfo si = new SystemInfo(); + HardwareAbstractionLayer hal = si.getHardware(); + + setCpuInfo(hal.getProcessor()); + + setMemInfo(hal.getMemory()); + + setSysInfo(); + + setJvmInfo(); + + setSysFiles(si.getOperatingSystem()); + } + + /** + * 设置CPU信息 + */ + private void setCpuInfo(CentralProcessor processor) { + // CPU信息 + long[] prevTicks = processor.getSystemCpuLoadTicks(); + Util.sleep(OSHI_WAIT_SECOND); + long[] ticks = processor.getSystemCpuLoadTicks(); + long nice = ticks[TickType.NICE.getIndex()] - prevTicks[TickType.NICE.getIndex()]; + long irq = ticks[TickType.IRQ.getIndex()] - prevTicks[TickType.IRQ.getIndex()]; + long softirq = ticks[TickType.SOFTIRQ.getIndex()] - prevTicks[TickType.SOFTIRQ.getIndex()]; + long steal = ticks[TickType.STEAL.getIndex()] - prevTicks[TickType.STEAL.getIndex()]; + long cSys = ticks[TickType.SYSTEM.getIndex()] - prevTicks[TickType.SYSTEM.getIndex()]; + long user = ticks[TickType.USER.getIndex()] - prevTicks[TickType.USER.getIndex()]; + long iowait = ticks[TickType.IOWAIT.getIndex()] - prevTicks[TickType.IOWAIT.getIndex()]; + long idle = ticks[TickType.IDLE.getIndex()] - prevTicks[TickType.IDLE.getIndex()]; + long totalCpu = user + nice + cSys + idle + iowait + irq + softirq + steal; + cpu.setCpuNum(processor.getLogicalProcessorCount()); + cpu.setTotal(totalCpu); + cpu.setSys(cSys); + cpu.setUsed(user); + cpu.setWait(iowait); + cpu.setFree(idle); + } + + /** + * 设置内存信息 + */ + private void setMemInfo(GlobalMemory memory) { + mem.setTotal(memory.getTotal()); + mem.setUsed(memory.getTotal() - memory.getAvailable()); + mem.setFree(memory.getAvailable()); + } + + /** + * 设置服务器信息 + */ + private void setSysInfo() { + Properties props = System.getProperties(); + sys.setComputerName(IpUtils.getHostName()); + sys.setComputerIp(IpUtils.getHostIp()); + sys.setOsName(props.getProperty("os.name")); + sys.setOsArch(props.getProperty("os.arch")); + sys.setUserDir(props.getProperty("user.dir")); + } + + /** + * 设置Java虚拟机 + */ + private void setJvmInfo() throws UnknownHostException { + Properties props = System.getProperties(); + jvm.setTotal(Runtime.getRuntime().totalMemory()); + jvm.setMax(Runtime.getRuntime().maxMemory()); + jvm.setFree(Runtime.getRuntime().freeMemory()); + jvm.setVersion(props.getProperty("java.version")); + jvm.setHome(props.getProperty("java.home")); + } + + /** + * 设置磁盘信息 + */ + private void setSysFiles(OperatingSystem os) { + FileSystem fileSystem = os.getFileSystem(); + List fsArray = fileSystem.getFileStores(); + for (OSFileStore fs : fsArray) { + long free = fs.getUsableSpace(); + long total = fs.getTotalSpace(); + long used = total - free; + SysFile sysFile = new SysFile(); + sysFile.setDirName(fs.getMount()); + sysFile.setSysTypeName(fs.getType()); + sysFile.setTypeName(fs.getName()); + sysFile.setTotal(convertFileSize(total)); + sysFile.setFree(convertFileSize(free)); + sysFile.setUsed(convertFileSize(used)); + sysFile.setUsage(Arith.mul(Arith.div(used, total, 4), 100)); + sysFiles.add(sysFile); + } + } + + /** + * 字节转换 + * + * @param size 字节大小 + * @return 转换后值 + */ + public String convertFileSize(long size) { + long kb = 1024; + long mb = kb * 1024; + long gb = mb * 1024; + if (size >= gb) { + return String.format("%.1f GB", (float) size / gb); + } else if (size >= mb) { + float f = (float) size / mb; + return String.format(f > 100 ? "%.0f MB" : "%.1f MB", f); + } else if (size >= kb) { + float f = (float) size / kb; + return String.format(f > 100 ? "%.0f KB" : "%.1f KB", f); + } else { + return String.format("%d B", size); + } + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/web/domain/server/Cpu.java b/jsowell-framework/src/main/java/com/jsowell/framework/web/domain/server/Cpu.java new file mode 100644 index 000000000..692c14d77 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/web/domain/server/Cpu.java @@ -0,0 +1,88 @@ +package com.jsowell.framework.web.domain.server; + +import com.jsowell.common.util.Arith; + +/** + * CPU相关信息 + * + * @author jsowell + */ +public class Cpu { + /** + * 核心数 + */ + private int cpuNum; + + /** + * CPU总的使用率 + */ + private double total; + + /** + * CPU系统使用率 + */ + private double sys; + + /** + * CPU用户使用率 + */ + private double used; + + /** + * CPU当前等待率 + */ + private double wait; + + /** + * CPU当前空闲率 + */ + private double free; + + public int getCpuNum() { + return cpuNum; + } + + public void setCpuNum(int cpuNum) { + this.cpuNum = cpuNum; + } + + public double getTotal() { + return Arith.round(Arith.mul(total, 100), 2); + } + + public void setTotal(double total) { + this.total = total; + } + + public double getSys() { + return Arith.round(Arith.mul(sys / total, 100), 2); + } + + public void setSys(double sys) { + this.sys = sys; + } + + public double getUsed() { + return Arith.round(Arith.mul(used / total, 100), 2); + } + + public void setUsed(double used) { + this.used = used; + } + + public double getWait() { + return Arith.round(Arith.mul(wait / total, 100), 2); + } + + public void setWait(double wait) { + this.wait = wait; + } + + public double getFree() { + return Arith.round(Arith.mul(free / total, 100), 2); + } + + public void setFree(double free) { + this.free = free; + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/web/domain/server/Jvm.java b/jsowell-framework/src/main/java/com/jsowell/framework/web/domain/server/Jvm.java new file mode 100644 index 000000000..c93b5b28d --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/web/domain/server/Jvm.java @@ -0,0 +1,114 @@ +package com.jsowell.framework.web.domain.server; + +import com.jsowell.common.util.Arith; +import com.jsowell.common.util.DateUtils; + +import java.lang.management.ManagementFactory; + +/** + * JVM相关信息 + * + * @author jsowell + */ +public class Jvm { + /** + * 当前JVM占用的内存总数(M) + */ + private double total; + + /** + * JVM最大可用内存总数(M) + */ + private double max; + + /** + * JVM空闲内存(M) + */ + private double free; + + /** + * JDK版本 + */ + private String version; + + /** + * JDK路径 + */ + private String home; + + public double getTotal() { + return Arith.div(total, (1024 * 1024), 2); + } + + public void setTotal(double total) { + this.total = total; + } + + public double getMax() { + return Arith.div(max, (1024 * 1024), 2); + } + + public void setMax(double max) { + this.max = max; + } + + public double getFree() { + return Arith.div(free, (1024 * 1024), 2); + } + + public void setFree(double free) { + this.free = free; + } + + public double getUsed() { + return Arith.div(total - free, (1024 * 1024), 2); + } + + public double getUsage() { + return Arith.mul(Arith.div(total - free, total, 4), 100); + } + + /** + * 获取JDK名称 + */ + public String getName() { + return ManagementFactory.getRuntimeMXBean().getVmName(); + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getHome() { + return home; + } + + public void setHome(String home) { + this.home = home; + } + + /** + * JDK启动时间 + */ + public String getStartTime() { + return DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, DateUtils.getServerStartDate()); + } + + /** + * JDK运行时间 + */ + public String getRunTime() { + return DateUtils.getDatePoor(DateUtils.getNowDate(), DateUtils.getServerStartDate()); + } + + /** + * 运行参数 + */ + public String getInputArgs() { + return ManagementFactory.getRuntimeMXBean().getInputArguments().toString(); + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/web/domain/server/Mem.java b/jsowell-framework/src/main/java/com/jsowell/framework/web/domain/server/Mem.java new file mode 100644 index 000000000..871919457 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/web/domain/server/Mem.java @@ -0,0 +1,53 @@ +package com.jsowell.framework.web.domain.server; + +import com.jsowell.common.util.Arith; + +/** + * 內存相关信息 + * + * @author jsowell + */ +public class Mem { + /** + * 内存总量 + */ + private double total; + + /** + * 已用内存 + */ + private double used; + + /** + * 剩余内存 + */ + private double free; + + public double getTotal() { + return Arith.div(total, (1024 * 1024 * 1024), 2); + } + + public void setTotal(long total) { + this.total = total; + } + + public double getUsed() { + return Arith.div(used, (1024 * 1024 * 1024), 2); + } + + public void setUsed(long used) { + this.used = used; + } + + public double getFree() { + return Arith.div(free, (1024 * 1024 * 1024), 2); + } + + public void setFree(long free) { + this.free = free; + } + + public double getUsage() { + return Arith.mul(Arith.div(used, total, 4), 100); + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/web/domain/server/Sys.java b/jsowell-framework/src/main/java/com/jsowell/framework/web/domain/server/Sys.java new file mode 100644 index 000000000..853586757 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/web/domain/server/Sys.java @@ -0,0 +1,73 @@ +package com.jsowell.framework.web.domain.server; + +/** + * 系统相关信息 + * + * @author jsowell + */ +public class Sys { + /** + * 服务器名称 + */ + private String computerName; + + /** + * 服务器Ip + */ + private String computerIp; + + /** + * 项目路径 + */ + private String userDir; + + /** + * 操作系统 + */ + private String osName; + + /** + * 系统架构 + */ + private String osArch; + + public String getComputerName() { + return computerName; + } + + public void setComputerName(String computerName) { + this.computerName = computerName; + } + + public String getComputerIp() { + return computerIp; + } + + public void setComputerIp(String computerIp) { + this.computerIp = computerIp; + } + + public String getUserDir() { + return userDir; + } + + public void setUserDir(String userDir) { + this.userDir = userDir; + } + + public String getOsName() { + return osName; + } + + public void setOsName(String osName) { + this.osName = osName; + } + + public String getOsArch() { + return osArch; + } + + public void setOsArch(String osArch) { + this.osArch = osArch; + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/web/domain/server/SysFile.java b/jsowell-framework/src/main/java/com/jsowell/framework/web/domain/server/SysFile.java new file mode 100644 index 000000000..75b4df57a --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/web/domain/server/SysFile.java @@ -0,0 +1,99 @@ +package com.jsowell.framework.web.domain.server; + +/** + * 系统文件相关信息 + * + * @author jsowell + */ +public class SysFile { + /** + * 盘符路径 + */ + private String dirName; + + /** + * 盘符类型 + */ + private String sysTypeName; + + /** + * 文件类型 + */ + private String typeName; + + /** + * 总大小 + */ + private String total; + + /** + * 剩余大小 + */ + private String free; + + /** + * 已经使用量 + */ + private String used; + + /** + * 资源的使用率 + */ + private double usage; + + public String getDirName() { + return dirName; + } + + public void setDirName(String dirName) { + this.dirName = dirName; + } + + public String getSysTypeName() { + return sysTypeName; + } + + public void setSysTypeName(String sysTypeName) { + this.sysTypeName = sysTypeName; + } + + public String getTypeName() { + return typeName; + } + + public void setTypeName(String typeName) { + this.typeName = typeName; + } + + public String getTotal() { + return total; + } + + public void setTotal(String total) { + this.total = total; + } + + public String getFree() { + return free; + } + + public void setFree(String free) { + this.free = free; + } + + public String getUsed() { + return used; + } + + public void setUsed(String used) { + this.used = used; + } + + public double getUsage() { + return usage; + } + + public void setUsage(double usage) { + this.usage = usage; + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/web/exception/GlobalExceptionHandler.java b/jsowell-framework/src/main/java/com/jsowell/framework/web/exception/GlobalExceptionHandler.java new file mode 100644 index 000000000..17164ae33 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/web/exception/GlobalExceptionHandler.java @@ -0,0 +1,106 @@ +package com.jsowell.framework.web.exception; + +import com.jsowell.common.constant.HttpStatus; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.exception.DemoModeException; +import com.jsowell.common.exception.ServiceException; +import com.jsowell.common.util.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.validation.BindException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import javax.servlet.http.HttpServletRequest; + +/** + * 全局异常处理器 + * + * @author jsowell + */ +@RestControllerAdvice +public class GlobalExceptionHandler { + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + /** + * 权限校验异常 + */ + @ExceptionHandler(AccessDeniedException.class) + public AjaxResult handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request) { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',权限校验失败'{}'", requestURI, e.getMessage()); + return AjaxResult.error(HttpStatus.FORBIDDEN, "没有权限,请联系管理员授权"); + } + + /** + * 请求方式不支持 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public AjaxResult handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e, + HttpServletRequest request) { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod()); + return AjaxResult.error(e.getMessage()); + } + + /** + * 业务异常 + */ + @ExceptionHandler(ServiceException.class) + public AjaxResult handleServiceException(ServiceException e, HttpServletRequest request) { + log.error(e.getMessage(), e); + Integer code = e.getCode(); + return StringUtils.isNotNull(code) ? AjaxResult.error(code, e.getMessage()) : AjaxResult.error(e.getMessage()); + } + + /** + * 拦截未知的运行时异常 + */ + @ExceptionHandler(RuntimeException.class) + public AjaxResult handleRuntimeException(RuntimeException e, HttpServletRequest request) { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',发生未知异常.", requestURI, e); + return AjaxResult.error(e.getMessage()); + } + + /** + * 系统异常 + */ + @ExceptionHandler(Exception.class) + public AjaxResult handleException(Exception e, HttpServletRequest request) { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',发生系统异常.", requestURI, e); + return AjaxResult.error(e.getMessage()); + } + + /** + * 自定义验证异常 + */ + @ExceptionHandler(BindException.class) + public AjaxResult handleBindException(BindException e) { + log.error(e.getMessage(), e); + String message = e.getAllErrors().get(0).getDefaultMessage(); + return AjaxResult.error(message); + } + + /** + * 自定义验证异常 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + log.error(e.getMessage(), e); + String message = e.getBindingResult().getFieldError().getDefaultMessage(); + return AjaxResult.error(message); + } + + /** + * 演示模式异常 + */ + @ExceptionHandler(DemoModeException.class) + public AjaxResult handleDemoModeException(DemoModeException e) { + return AjaxResult.error("演示模式,不允许操作"); + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/web/service/PermissionService.java b/jsowell-framework/src/main/java/com/jsowell/framework/web/service/PermissionService.java new file mode 100644 index 000000000..c510610d0 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/web/service/PermissionService.java @@ -0,0 +1,148 @@ +package com.jsowell.framework.web.service; + +import com.jsowell.common.core.domain.entity.SysRole; +import com.jsowell.common.core.domain.model.LoginUser; +import com.jsowell.common.util.SecurityUtils; +import com.jsowell.common.util.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.util.Set; + +/** + * 自定义权限实现,ss取自SpringSecurity首字母 + * + * @author jsowell + */ +@Service("ss") +public class PermissionService { + /** + * 所有权限标识 + */ + private static final String ALL_PERMISSION = "*:*:*"; + + /** + * 管理员角色权限标识 + */ + private static final String SUPER_ADMIN = "admin"; + + private static final String ROLE_DELIMETER = ","; + + private static final String PERMISSION_DELIMETER = ","; + + /** + * 验证用户是否具备某权限 + * + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + public boolean hasPermi(String permission) { + if (StringUtils.isEmpty(permission)) { + return false; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) { + return false; + } + return hasPermissions(loginUser.getPermissions(), permission); + } + + /** + * 验证用户是否不具备某权限,与 hasPermi逻辑相反 + * + * @param permission 权限字符串 + * @return 用户是否不具备某权限 + */ + public boolean lacksPermi(String permission) { + return hasPermi(permission) != true; + } + + /** + * 验证用户是否具有以下任意一个权限 + * + * @param permissions 以 PERMISSION_NAMES_DELIMETER 为分隔符的权限列表 + * @return 用户是否具有以下任意一个权限 + */ + public boolean hasAnyPermi(String permissions) { + if (StringUtils.isEmpty(permissions)) { + return false; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) { + return false; + } + Set authorities = loginUser.getPermissions(); + for (String permission : permissions.split(PERMISSION_DELIMETER)) { + if (permission != null && hasPermissions(authorities, permission)) { + return true; + } + } + return false; + } + + /** + * 判断用户是否拥有某个角色 + * + * @param role 角色字符串 + * @return 用户是否具备某角色 + */ + public boolean hasRole(String role) { + if (StringUtils.isEmpty(role)) { + return false; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())) { + return false; + } + for (SysRole sysRole : loginUser.getUser().getRoles()) { + String roleKey = sysRole.getRoleKey(); + if (SUPER_ADMIN.equals(roleKey) || roleKey.equals(StringUtils.trim(role))) { + return true; + } + } + return false; + } + + /** + * 验证用户是否不具备某角色,与 isRole逻辑相反。 + * + * @param role 角色名称 + * @return 用户是否不具备某角色 + */ + public boolean lacksRole(String role) { + return hasRole(role) != true; + } + + /** + * 验证用户是否具有以下任意一个角色 + * + * @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表 + * @return 用户是否具有以下任意一个角色 + */ + public boolean hasAnyRoles(String roles) { + if (StringUtils.isEmpty(roles)) { + return false; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())) { + return false; + } + for (String role : roles.split(ROLE_DELIMETER)) { + if (hasRole(role)) { + return true; + } + } + return false; + } + + /** + * 判断是否包含权限 + * + * @param permissions 权限列表 + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + private boolean hasPermissions(Set permissions, String permission) { + return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission)); + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/web/service/SysLoginService.java b/jsowell-framework/src/main/java/com/jsowell/framework/web/service/SysLoginService.java new file mode 100644 index 000000000..1676786a7 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/web/service/SysLoginService.java @@ -0,0 +1,125 @@ +package com.jsowell.framework.web.service; + +import com.jsowell.common.constant.CacheConstants; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.domain.entity.SysUser; +import com.jsowell.common.core.domain.model.LoginUser; +import com.jsowell.common.core.redis.RedisCache; +import com.jsowell.common.exception.ServiceException; +import com.jsowell.common.exception.user.CaptchaException; +import com.jsowell.common.exception.user.CaptchaExpireException; +import com.jsowell.common.exception.user.UserPasswordNotMatchException; +import com.jsowell.common.util.DateUtils; +import com.jsowell.common.util.MessageUtils; +import com.jsowell.common.util.ServletUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.ip.IpUtils; +import com.jsowell.framework.manager.AsyncManager; +import com.jsowell.framework.manager.factory.AsyncFactory; +import com.jsowell.framework.security.context.AuthenticationContextHolder; +import com.jsowell.system.service.SysConfigService; +import com.jsowell.system.service.SysUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 登录校验方法 + * + * @author jsowell + */ +@Component +public class SysLoginService { + @Autowired + private TokenService tokenService; + + @Resource + private AuthenticationManager authenticationManager; + + @Autowired + private RedisCache redisCache; + + @Autowired + private SysUserService userService; + + @Autowired + private SysConfigService configService; + + /** + * 登录验证 + * + * @param username 用户名 + * @param password 密码 + * @param code 验证码 + * @param uuid 唯一标识 + * @return 结果 + */ + public String login(String username, String password, String code, String uuid) { + boolean captchaEnabled = configService.selectCaptchaEnabled(); + // 验证码开关 + if (captchaEnabled) { + validateCaptcha(username, code, uuid); + } + // 用户验证 + Authentication authentication = null; + try { + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); + AuthenticationContextHolder.setContext(authenticationToken); + // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername + authentication = authenticationManager.authenticate(authenticationToken); + } catch (Exception e) { + if (e instanceof BadCredentialsException) { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); + throw new UserPasswordNotMatchException(); + } else { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage())); + throw new ServiceException(e.getMessage()); + } + } + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"))); + LoginUser loginUser = (LoginUser) authentication.getPrincipal(); + recordLoginInfo(loginUser.getUserId()); + // 生成token + return tokenService.createToken(loginUser); + } + + /** + * 校验验证码 + * + * @param username 用户名 + * @param code 验证码 + * @param uuid 唯一标识 + * @return 结果 + */ + public void validateCaptcha(String username, String code, String uuid) { + String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); + String captcha = redisCache.getCacheObject(verifyKey); + redisCache.deleteObject(verifyKey); + if (captcha == null) { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"))); + throw new CaptchaExpireException(); + } + if (!code.equalsIgnoreCase(captcha)) { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"))); + throw new CaptchaException(); + } + } + + /** + * 记录登录信息 + * + * @param userId 用户ID + */ + public void recordLoginInfo(Long userId) { + SysUser sysUser = new SysUser(); + sysUser.setUserId(userId); + sysUser.setLoginIp(IpUtils.getIpAddr(ServletUtils.getRequest())); + sysUser.setLoginDate(DateUtils.getNowDate()); + userService.updateUserProfile(sysUser); + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/web/service/SysPasswordService.java b/jsowell-framework/src/main/java/com/jsowell/framework/web/service/SysPasswordService.java new file mode 100644 index 000000000..adbaa1a45 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/web/service/SysPasswordService.java @@ -0,0 +1,84 @@ +package com.jsowell.framework.web.service; + +import com.jsowell.common.constant.CacheConstants; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.domain.entity.SysUser; +import com.jsowell.common.core.redis.RedisCache; +import com.jsowell.common.exception.user.UserPasswordNotMatchException; +import com.jsowell.common.exception.user.UserPasswordRetryLimitExceedException; +import com.jsowell.common.util.MessageUtils; +import com.jsowell.common.util.SecurityUtils; +import com.jsowell.framework.manager.AsyncManager; +import com.jsowell.framework.manager.factory.AsyncFactory; +import com.jsowell.framework.security.context.AuthenticationContextHolder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * 登录密码方法 + * + * @author jsowell + */ +@Component +public class SysPasswordService { + @Autowired + private RedisCache redisCache; + + @Value(value = "${user.password.maxRetryCount}") + private int maxRetryCount; + + @Value(value = "${user.password.lockTime}") + private int lockTime; + + /** + * 登录账户密码错误次数缓存键名 + * + * @param username 用户名 + * @return 缓存键key + */ + private String getCacheKey(String username) { + return CacheConstants.PWD_ERR_CNT_KEY + username; + } + + public void validate(SysUser user) { + Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext(); + String username = usernamePasswordAuthenticationToken.getName(); + String password = usernamePasswordAuthenticationToken.getCredentials().toString(); + + Integer retryCount = redisCache.getCacheObject(getCacheKey(username)); + + if (retryCount == null) { + retryCount = 0; + } + + if (retryCount >= Integer.valueOf(maxRetryCount).intValue()) { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, + MessageUtils.message("user.password.retry.limit.exceed", maxRetryCount, lockTime))); + throw new UserPasswordRetryLimitExceedException(maxRetryCount, lockTime); + } + + if (!matches(user, password)) { + retryCount = retryCount + 1; + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, + MessageUtils.message("user.password.retry.limit.count", retryCount))); + redisCache.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES); + throw new UserPasswordNotMatchException(); + } else { + clearLoginRecordCache(username); + } + } + + public boolean matches(SysUser user, String rawPassword) { + return SecurityUtils.matchesPassword(rawPassword, user.getPassword()); + } + + public void clearLoginRecordCache(String loginName) { + if (redisCache.hasKey(getCacheKey(loginName))) { + redisCache.deleteObject(getCacheKey(loginName)); + } + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/web/service/SysPermissionService.java b/jsowell-framework/src/main/java/com/jsowell/framework/web/service/SysPermissionService.java new file mode 100644 index 000000000..78e971693 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/web/service/SysPermissionService.java @@ -0,0 +1,58 @@ +package com.jsowell.framework.web.service; + +import com.jsowell.common.core.domain.entity.SysUser; +import com.jsowell.system.service.SysMenuService; +import com.jsowell.system.service.SysRoleService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashSet; +import java.util.Set; + +/** + * 用户权限处理 + * + * @author jsowell + */ +@Component +public class SysPermissionService { + @Autowired + private SysRoleService roleService; + + @Autowired + private SysMenuService menuService; + + /** + * 获取角色数据权限 + * + * @param user 用户信息 + * @return 角色权限信息 + */ + public Set getRolePermission(SysUser user) { + Set roles = new HashSet(); + // 管理员拥有所有权限 + if (user.isAdmin()) { + roles.add("admin"); + } else { + roles.addAll(roleService.selectRolePermissionByUserId(user.getUserId())); + } + return roles; + } + + /** + * 获取菜单数据权限 + * + * @param user 用户信息 + * @return 菜单权限信息 + */ + public Set getMenuPermission(SysUser user) { + Set perms = new HashSet(); + // 管理员拥有所有权限 + if (user.isAdmin()) { + perms.add("*:*:*"); + } else { + perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId())); + } + return perms; + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/web/service/SysRegisterService.java b/jsowell-framework/src/main/java/com/jsowell/framework/web/service/SysRegisterService.java new file mode 100644 index 000000000..2f7458c36 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/web/service/SysRegisterService.java @@ -0,0 +1,96 @@ +package com.jsowell.framework.web.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import com.jsowell.common.constant.CacheConstants; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.constant.UserConstants; +import com.jsowell.common.core.domain.entity.SysUser; +import com.jsowell.common.core.domain.model.RegisterBody; +import com.jsowell.common.core.redis.RedisCache; +import com.jsowell.common.exception.user.CaptchaException; +import com.jsowell.common.exception.user.CaptchaExpireException; +import com.jsowell.common.util.MessageUtils; +import com.jsowell.common.util.SecurityUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.framework.manager.AsyncManager; +import com.jsowell.framework.manager.factory.AsyncFactory; +import com.jsowell.system.service.SysConfigService; +import com.jsowell.system.service.SysUserService; + +/** + * 注册校验方法 + * + * @author jsowell + */ +@Component +public class SysRegisterService { + @Autowired + private SysUserService userService; + + @Autowired + private SysConfigService configService; + + @Autowired + private RedisCache redisCache; + + /** + * 注册 + */ + public String register(RegisterBody registerBody) { + String msg = "", username = registerBody.getUsername(), password = registerBody.getPassword(); + + boolean captchaEnabled = configService.selectCaptchaEnabled(); + // 验证码开关 + if (captchaEnabled) { + validateCaptcha(username, registerBody.getCode(), registerBody.getUuid()); + } + + if (StringUtils.isEmpty(username)) { + msg = "用户名不能为空"; + } else if (StringUtils.isEmpty(password)) { + msg = "用户密码不能为空"; + } else if (username.length() < UserConstants.USERNAME_MIN_LENGTH + || username.length() > UserConstants.USERNAME_MAX_LENGTH) { + msg = "账户长度必须在2到20个字符之间"; + } else if (password.length() < UserConstants.PASSWORD_MIN_LENGTH + || password.length() > UserConstants.PASSWORD_MAX_LENGTH) { + msg = "密码长度必须在5到20个字符之间"; + } else if (UserConstants.NOT_UNIQUE.equals(userService.checkUserNameUnique(username))) { + msg = "保存用户'" + username + "'失败,注册账号已存在"; + } else { + SysUser sysUser = new SysUser(); + sysUser.setUserName(username); + sysUser.setNickName(username); + sysUser.setPassword(SecurityUtils.encryptPassword(registerBody.getPassword())); + boolean regFlag = userService.registerUser(sysUser); + if (!regFlag) { + msg = "注册失败,请联系系统管理人员"; + } else { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.REGISTER, + MessageUtils.message("user.register.success"))); + } + } + return msg; + } + + /** + * 校验验证码 + * + * @param username 用户名 + * @param code 验证码 + * @param uuid 唯一标识 + * @return 结果 + */ + public void validateCaptcha(String username, String code, String uuid) { + String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); + String captcha = redisCache.getCacheObject(verifyKey); + redisCache.deleteObject(verifyKey); + if (captcha == null) { + throw new CaptchaExpireException(); + } + if (!code.equalsIgnoreCase(captcha)) { + throw new CaptchaException(); + } + } +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/web/service/TokenService.java b/jsowell-framework/src/main/java/com/jsowell/framework/web/service/TokenService.java new file mode 100644 index 000000000..d7f6ba6cc --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/web/service/TokenService.java @@ -0,0 +1,211 @@ +package com.jsowell.framework.web.service; + +import com.jsowell.common.constant.CacheConstants; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.domain.model.LoginUser; +import com.jsowell.common.core.redis.RedisCache; +import com.jsowell.common.util.ServletUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.id.IdUtils; +import com.jsowell.common.util.ip.AddressUtils; +import com.jsowell.common.util.ip.IpUtils; +import eu.bitwalker.useragentutils.UserAgent; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * token验证处理 + * + * @author jsowell + */ +@Component +public class TokenService { + // 令牌自定义标识 + @Value("${token.header}") + private String header; + + // 令牌秘钥 + @Value("${token.secret}") + private String secret; + + // 令牌有效期(默认30分钟) + @Value("${token.expireTime}") + private int expireTime; + + // 接口服务 令牌有效期 + @Value("${token.serviceExpireTime}") + private int serviceExpireTime; + + protected static final long MILLIS_SECOND = 1000; + + protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND; + + private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L; + + @Autowired + private RedisCache redisCache; + + /** + * 获取用户身份信息 + * + * @return 用户信息 + */ + public LoginUser getLoginUser(HttpServletRequest request) { + // 获取请求携带的令牌 + String token = getToken(request); + if (StringUtils.isNotEmpty(token)) { + try { + Claims claims = parseToken(token); + // 解析对应的权限以及用户信息 + String uuid = (String) claims.get(Constants.LOGIN_USER_KEY); + String userKey = getTokenKey(uuid); + LoginUser user = redisCache.getCacheObject(userKey); + return user; + } catch (Exception e) { + } + } + return null; + } + + /** + * 设置用户身份信息 + */ + public void setLoginUser(LoginUser loginUser) { + if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken())) { + refreshToken(loginUser); + } + } + + /** + * 删除用户身份信息 + */ + public void delLoginUser(String token) { + if (StringUtils.isNotEmpty(token)) { + String userKey = getTokenKey(token); + redisCache.deleteObject(userKey); + } + } + + /** + * 创建令牌 + * + * @param loginUser 用户信息 + * @return 令牌 + */ + public String createToken(LoginUser loginUser) { + String token = IdUtils.fastUUID(); + loginUser.setToken(token); + setUserAgent(loginUser); + refreshToken(loginUser); + + Map claims = new HashMap<>(); + claims.put(Constants.LOGIN_USER_KEY, token); + return createToken(claims); + } + + /** + * 验证令牌有效期,相差不足20分钟,自动刷新缓存 + * + * @param loginUser + * @return 令牌 + */ + public void verifyToken(LoginUser loginUser) { + long expireTime = loginUser.getExpireTime(); + long currentTime = System.currentTimeMillis(); + if (expireTime - currentTime <= MILLIS_MINUTE_TEN) { + refreshToken(loginUser); + } + } + + /** + * 刷新令牌有效期 + * + * @param loginUser 登录信息 + */ + public void refreshToken(LoginUser loginUser) { + loginUser.setLoginTime(System.currentTimeMillis()); + loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE); + // 根据uuid将loginUser缓存 + String userKey = getTokenKey(loginUser.getToken()); + redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES); + } + + /** + * 设置用户代理信息 + * + * @param loginUser 登录信息 + */ + public void setUserAgent(LoginUser loginUser) { + UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent")); + String ip = IpUtils.getIpAddr(ServletUtils.getRequest()); + loginUser.setIpaddr(ip); + loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip)); + loginUser.setBrowser(userAgent.getBrowser().getName()); + loginUser.setOs(userAgent.getOperatingSystem().getName()); + } + + /** + * 从数据声明生成令牌 + * + * @param claims 数据声明 + * @return 令牌 + */ + private String createToken(Map claims) { + String token = Jwts.builder() + .setClaims(claims) + .signWith(SignatureAlgorithm.HS512, secret).compact(); + return token; + } + + /** + * 从令牌中获取数据声明 + * + * @param token 令牌 + * @return 数据声明 + */ + private Claims parseToken(String token) { + return Jwts.parser() + .setSigningKey(secret) + .parseClaimsJws(token) + .getBody(); + } + + /** + * 从令牌中获取用户名 + * + * @param token 令牌 + * @return 用户名 + */ + public String getUsernameFromToken(String token) { + Claims claims = parseToken(token); + return claims.getSubject(); + } + + /** + * 获取请求token + * + * @param request + * @return token + */ + private String getToken(HttpServletRequest request) { + String token = request.getHeader(header); + if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) { + token = token.replace(Constants.TOKEN_PREFIX, ""); + } + return token; + } + + private String getTokenKey(String uuid) { + return CacheConstants.LOGIN_TOKEN_KEY + uuid; + } + +} diff --git a/jsowell-framework/src/main/java/com/jsowell/framework/web/service/UserDetailsServiceImpl.java b/jsowell-framework/src/main/java/com/jsowell/framework/web/service/UserDetailsServiceImpl.java new file mode 100644 index 000000000..f0f9108f7 --- /dev/null +++ b/jsowell-framework/src/main/java/com/jsowell/framework/web/service/UserDetailsServiceImpl.java @@ -0,0 +1,57 @@ +package com.jsowell.framework.web.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import com.jsowell.common.core.domain.entity.SysUser; +import com.jsowell.common.core.domain.model.LoginUser; +import com.jsowell.common.enums.UserStatus; +import com.jsowell.common.exception.ServiceException; +import com.jsowell.common.util.StringUtils; +import com.jsowell.system.service.SysUserService; + +/** + * 用户验证处理 + * + * @author jsowell + */ +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class); + + @Autowired + private SysUserService userService; + + @Autowired + private SysPasswordService passwordService; + + @Autowired + private SysPermissionService permissionService; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + SysUser user = userService.selectUserByUserName(username); + if (StringUtils.isNull(user)) { + log.info("登录用户:{} 不存在.", username); + throw new ServiceException("登录用户:" + username + " 不存在"); + } else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) { + log.info("登录用户:{} 已被删除.", username); + throw new ServiceException("对不起,您的账号:" + username + " 已被删除"); + } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) { + log.info("登录用户:{} 已被停用.", username); + throw new ServiceException("对不起,您的账号:" + username + " 已停用"); + } + + passwordService.validate(user); + + return createLoginUser(user); + } + + public UserDetails createLoginUser(SysUser user) { + return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user)); + } +} diff --git a/jsowell-generator/pom.xml b/jsowell-generator/pom.xml new file mode 100644 index 000000000..9a7aa3fc1 --- /dev/null +++ b/jsowell-generator/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + com.jsowell + jsowell-charger-web + 1.0.0 + + + jsowell-generator + + + generator代码生成 + + + + + + + org.apache.velocity + velocity-engine-core + + + + + commons-collections + commons-collections + + + + + com.jsowell + jsowell-common + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.8 + 1.8 + + /src/test/** + + utf-8 + + + + + + \ No newline at end of file diff --git a/jsowell-generator/src/main/java/com/jsowell/generator/config/GenConfig.java b/jsowell-generator/src/main/java/com/jsowell/generator/config/GenConfig.java new file mode 100644 index 000000000..16e3c2b97 --- /dev/null +++ b/jsowell-generator/src/main/java/com/jsowell/generator/config/GenConfig.java @@ -0,0 +1,72 @@ +package com.jsowell.generator.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Component; + +/** + * 读取代码生成相关配置 + * + * @author jsowell + */ +@Component +@ConfigurationProperties(prefix = "gen") +@PropertySource(value = {"classpath:generator.yml"}) +public class GenConfig { + /** + * 作者 + */ + public static String author; + + /** + * 生成包路径 + */ + public static String packageName; + + /** + * 自动去除表前缀,默认是false + */ + public static boolean autoRemovePre; + + /** + * 表前缀(类名不会包含表前缀) + */ + public static String tablePrefix; + + public static String getAuthor() { + return author; + } + + @Value("${author}") + public void setAuthor(String author) { + GenConfig.author = author; + } + + public static String getPackageName() { + return packageName; + } + + @Value("${packageName}") + public void setPackageName(String packageName) { + GenConfig.packageName = packageName; + } + + public static boolean getAutoRemovePre() { + return autoRemovePre; + } + + @Value("${autoRemovePre}") + public void setAutoRemovePre(boolean autoRemovePre) { + GenConfig.autoRemovePre = autoRemovePre; + } + + public static String getTablePrefix() { + return tablePrefix; + } + + @Value("${tablePrefix}") + public void setTablePrefix(String tablePrefix) { + GenConfig.tablePrefix = tablePrefix; + } +} diff --git a/jsowell-generator/src/main/java/com/jsowell/generator/controller/GenController.java b/jsowell-generator/src/main/java/com/jsowell/generator/controller/GenController.java new file mode 100644 index 000000000..3283a6ad9 --- /dev/null +++ b/jsowell-generator/src/main/java/com/jsowell/generator/controller/GenController.java @@ -0,0 +1,194 @@ +package com.jsowell.generator.controller; + +import com.jsowell.common.annotation.Log; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.core.text.Convert; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.generator.domain.GenTable; +import com.jsowell.generator.domain.GenTableColumn; +import com.jsowell.generator.service.IGenTableColumnService; +import com.jsowell.generator.service.IGenTableService; +import org.apache.commons.io.IOUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 代码生成 操作处理 + * + * @author jsowell + */ +@RestController +@RequestMapping("/tool/gen") +public class GenController extends BaseController { + @Autowired + private IGenTableService genTableService; + + @Autowired + private IGenTableColumnService genTableColumnService; + + /** + * 查询代码生成列表 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:list')") + @GetMapping("/list") + public TableDataInfo genList(GenTable genTable) { + startPage(); + List list = genTableService.selectGenTableList(genTable); + return getDataTable(list); + } + + /** + * 修改代码生成业务 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:query')") + @GetMapping(value = "/{tableId}") + public AjaxResult getInfo(@PathVariable Long tableId) { + GenTable table = genTableService.selectGenTableById(tableId); + List tables = genTableService.selectGenTableAll(); + List list = genTableColumnService.selectGenTableColumnListByTableId(tableId); + Map map = new HashMap(); + map.put("info", table); + map.put("rows", list); + map.put("tables", tables); + return AjaxResult.success(map); + } + + /** + * 查询数据库列表 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:list')") + @GetMapping("/db/list") + public TableDataInfo dataList(GenTable genTable) { + startPage(); + List list = genTableService.selectDbTableList(genTable); + return getDataTable(list); + } + + /** + * 查询数据表字段列表 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:list')") + @GetMapping(value = "/column/{tableId}") + public TableDataInfo columnList(Long tableId) { + TableDataInfo dataInfo = new TableDataInfo(); + List list = genTableColumnService.selectGenTableColumnListByTableId(tableId); + dataInfo.setRows(list); + dataInfo.setTotal(list.size()); + return dataInfo; + } + + /** + * 导入表结构(保存) + */ + @PreAuthorize("@ss.hasPermi('tool:gen:import')") + @Log(title = "代码生成", businessType = BusinessType.IMPORT) + @PostMapping("/importTable") + public AjaxResult importTableSave(String tables) { + String[] tableNames = Convert.toStrArray(tables); + // 查询表信息 + List tableList = genTableService.selectDbTableListByNames(tableNames); + genTableService.importGenTable(tableList); + return AjaxResult.success(); + } + + /** + * 修改保存代码生成业务 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:edit')") + @Log(title = "代码生成", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult editSave(@Validated @RequestBody GenTable genTable) { + genTableService.validateEdit(genTable); + genTableService.updateGenTable(genTable); + return AjaxResult.success(); + } + + /** + * 删除代码生成 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:remove')") + @Log(title = "代码生成", businessType = BusinessType.DELETE) + @DeleteMapping("/{tableIds}") + public AjaxResult remove(@PathVariable Long[] tableIds) { + genTableService.deleteGenTableByIds(tableIds); + return AjaxResult.success(); + } + + /** + * 预览代码 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:preview')") + @GetMapping("/preview/{tableId}") + public AjaxResult preview(@PathVariable("tableId") Long tableId) throws IOException { + Map dataMap = genTableService.previewCode(tableId); + return AjaxResult.success(dataMap); + } + + /** + * 生成代码(下载方式) + */ + @PreAuthorize("@ss.hasPermi('tool:gen:code')") + @Log(title = "代码生成", businessType = BusinessType.GENCODE) + @GetMapping("/download/{tableName}") + public void download(HttpServletResponse response, @PathVariable("tableName") String tableName) throws IOException { + byte[] data = genTableService.downloadCode(tableName); + genCode(response, data); + } + + /** + * 生成代码(自定义路径) + */ + @PreAuthorize("@ss.hasPermi('tool:gen:code')") + @Log(title = "代码生成", businessType = BusinessType.GENCODE) + @GetMapping("/genCode/{tableName}") + public AjaxResult genCode(@PathVariable("tableName") String tableName) { + genTableService.generatorCode(tableName); + return AjaxResult.success(); + } + + /** + * 同步数据库 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:edit')") + @Log(title = "代码生成", businessType = BusinessType.UPDATE) + @GetMapping("/synchDb/{tableName}") + public AjaxResult synchDb(@PathVariable("tableName") String tableName) { + genTableService.synchDb(tableName); + return AjaxResult.success(); + } + + /** + * 批量生成代码 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:code')") + @Log(title = "代码生成", businessType = BusinessType.GENCODE) + @GetMapping("/batchGenCode") + public void batchGenCode(HttpServletResponse response, String tables) throws IOException { + String[] tableNames = Convert.toStrArray(tables); + byte[] data = genTableService.downloadCode(tableNames); + genCode(response, data); + } + + /** + * 生成zip文件 + */ + private void genCode(HttpServletResponse response, byte[] data) throws IOException { + response.reset(); + response.addHeader("Access-Control-Allow-Origin", "*"); + response.addHeader("Access-Control-Expose-Headers", "Content-Disposition"); + response.setHeader("Content-Disposition", "attachment; filename=\"jsowell.zip\""); + response.addHeader("Content-Length", "" + data.length); + response.setContentType("application/octet-stream; charset=UTF-8"); + IOUtils.write(data, response.getOutputStream()); + } +} \ No newline at end of file diff --git a/jsowell-generator/src/main/java/com/jsowell/generator/domain/GenTable.java b/jsowell-generator/src/main/java/com/jsowell/generator/domain/GenTable.java new file mode 100644 index 000000000..641caefb9 --- /dev/null +++ b/jsowell-generator/src/main/java/com/jsowell/generator/domain/GenTable.java @@ -0,0 +1,363 @@ +package com.jsowell.generator.domain; + +import com.jsowell.common.constant.GenConstants; +import com.jsowell.common.core.domain.BaseEntity; +import com.jsowell.common.util.StringUtils; +import org.apache.commons.lang3.ArrayUtils; + +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import java.util.List; + +/** + * 业务表 gen_table + * + * @author jsowell + */ +public class GenTable extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * 编号 + */ + private Long tableId; + + /** + * 表名称 + */ + @NotBlank(message = "表名称不能为空") + private String tableName; + + /** + * 表描述 + */ + @NotBlank(message = "表描述不能为空") + private String tableComment; + + /** + * 关联父表的表名 + */ + private String subTableName; + + /** + * 本表关联父表的外键名 + */ + private String subTableFkName; + + /** + * 实体类名称(首字母大写) + */ + @NotBlank(message = "实体类名称不能为空") + private String className; + + /** + * 使用的模板(crud单表操作 tree树表操作 sub主子表操作) + */ + private String tplCategory; + + /** + * 生成包路径 + */ + @NotBlank(message = "生成包路径不能为空") + private String packageName; + + /** + * 生成模块名 + */ + @NotBlank(message = "生成模块名不能为空") + private String moduleName; + + /** + * 生成业务名 + */ + @NotBlank(message = "生成业务名不能为空") + private String businessName; + + /** + * 生成功能名 + */ + @NotBlank(message = "生成功能名不能为空") + private String functionName; + + /** + * 生成作者 + */ + @NotBlank(message = "作者不能为空") + private String functionAuthor; + + /** + * 生成代码方式(0zip压缩包 1自定义路径) + */ + private String genType; + + /** + * 生成路径(不填默认项目路径) + */ + private String genPath; + + /** + * 主键信息 + */ + private GenTableColumn pkColumn; + + /** + * 子表信息 + */ + private GenTable subTable; + + /** + * 表列信息 + */ + @Valid + private List columns; + + /** + * 其它生成选项 + */ + private String options; + + /** + * 树编码字段 + */ + private String treeCode; + + /** + * 树父编码字段 + */ + private String treeParentCode; + + /** + * 树名称字段 + */ + private String treeName; + + /** + * 上级菜单ID字段 + */ + private String parentMenuId; + + /** + * 上级菜单名称字段 + */ + private String parentMenuName; + + public Long getTableId() { + return tableId; + } + + public void setTableId(Long tableId) { + this.tableId = tableId; + } + + public String getTableName() { + return tableName; + } + + public void setTableName(String tableName) { + this.tableName = tableName; + } + + public String getTableComment() { + return tableComment; + } + + public void setTableComment(String tableComment) { + this.tableComment = tableComment; + } + + public String getSubTableName() { + return subTableName; + } + + public void setSubTableName(String subTableName) { + this.subTableName = subTableName; + } + + public String getSubTableFkName() { + return subTableFkName; + } + + public void setSubTableFkName(String subTableFkName) { + this.subTableFkName = subTableFkName; + } + + public String getClassName() { + return className; + } + + public void setClassName(String className) { + this.className = className; + } + + public String getTplCategory() { + return tplCategory; + } + + public void setTplCategory(String tplCategory) { + this.tplCategory = tplCategory; + } + + public String getPackageName() { + return packageName; + } + + public void setPackageName(String packageName) { + this.packageName = packageName; + } + + public String getModuleName() { + return moduleName; + } + + public void setModuleName(String moduleName) { + this.moduleName = moduleName; + } + + public String getBusinessName() { + return businessName; + } + + public void setBusinessName(String businessName) { + this.businessName = businessName; + } + + public String getFunctionName() { + return functionName; + } + + public void setFunctionName(String functionName) { + this.functionName = functionName; + } + + public String getFunctionAuthor() { + return functionAuthor; + } + + public void setFunctionAuthor(String functionAuthor) { + this.functionAuthor = functionAuthor; + } + + public String getGenType() { + return genType; + } + + public void setGenType(String genType) { + this.genType = genType; + } + + public String getGenPath() { + return genPath; + } + + public void setGenPath(String genPath) { + this.genPath = genPath; + } + + public GenTableColumn getPkColumn() { + return pkColumn; + } + + public void setPkColumn(GenTableColumn pkColumn) { + this.pkColumn = pkColumn; + } + + public GenTable getSubTable() { + return subTable; + } + + public void setSubTable(GenTable subTable) { + this.subTable = subTable; + } + + public List getColumns() { + return columns; + } + + public void setColumns(List columns) { + this.columns = columns; + } + + public String getOptions() { + return options; + } + + public void setOptions(String options) { + this.options = options; + } + + public String getTreeCode() { + return treeCode; + } + + public void setTreeCode(String treeCode) { + this.treeCode = treeCode; + } + + public String getTreeParentCode() { + return treeParentCode; + } + + public void setTreeParentCode(String treeParentCode) { + this.treeParentCode = treeParentCode; + } + + public String getTreeName() { + return treeName; + } + + public void setTreeName(String treeName) { + this.treeName = treeName; + } + + public String getParentMenuId() { + return parentMenuId; + } + + public void setParentMenuId(String parentMenuId) { + this.parentMenuId = parentMenuId; + } + + public String getParentMenuName() { + return parentMenuName; + } + + public void setParentMenuName(String parentMenuName) { + this.parentMenuName = parentMenuName; + } + + public boolean isSub() { + return isSub(this.tplCategory); + } + + public static boolean isSub(String tplCategory) { + return tplCategory != null && StringUtils.equals(GenConstants.TPL_SUB, tplCategory); + } + + public boolean isTree() { + return isTree(this.tplCategory); + } + + public static boolean isTree(String tplCategory) { + return tplCategory != null && StringUtils.equals(GenConstants.TPL_TREE, tplCategory); + } + + public boolean isCrud() { + return isCrud(this.tplCategory); + } + + public static boolean isCrud(String tplCategory) { + return tplCategory != null && StringUtils.equals(GenConstants.TPL_CRUD, tplCategory); + } + + public boolean isSuperColumn(String javaField) { + return isSuperColumn(this.tplCategory, javaField); + } + + public static boolean isSuperColumn(String tplCategory, String javaField) { + if (isTree(tplCategory)) { + return StringUtils.equalsAnyIgnoreCase(javaField, + ArrayUtils.addAll(GenConstants.TREE_ENTITY, GenConstants.BASE_ENTITY)); + } + return StringUtils.equalsAnyIgnoreCase(javaField, GenConstants.BASE_ENTITY); + } +} \ No newline at end of file diff --git a/jsowell-generator/src/main/java/com/jsowell/generator/domain/GenTableColumn.java b/jsowell-generator/src/main/java/com/jsowell/generator/domain/GenTableColumn.java new file mode 100644 index 000000000..0f50c274b --- /dev/null +++ b/jsowell-generator/src/main/java/com/jsowell/generator/domain/GenTableColumn.java @@ -0,0 +1,348 @@ +package com.jsowell.generator.domain; + +import com.jsowell.common.core.domain.BaseEntity; +import com.jsowell.common.util.StringUtils; + +import javax.validation.constraints.NotBlank; + +/** + * 代码生成业务字段表 gen_table_column + * + * @author jsowell + */ +public class GenTableColumn extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * 编号 + */ + private Long columnId; + + /** + * 归属表编号 + */ + private Long tableId; + + /** + * 列名称 + */ + private String columnName; + + /** + * 列描述 + */ + private String columnComment; + + /** + * 列类型 + */ + private String columnType; + + /** + * JAVA类型 + */ + private String javaType; + + /** + * JAVA字段名 + */ + @NotBlank(message = "Java属性不能为空") + private String javaField; + + /** + * 是否主键(1是) + */ + private String isPk; + + /** + * 是否自增(1是) + */ + private String isIncrement; + + /** + * 是否必填(1是) + */ + private String isRequired; + + /** + * 是否为插入字段(1是) + */ + private String isInsert; + + /** + * 是否编辑字段(1是) + */ + private String isEdit; + + /** + * 是否列表字段(1是) + */ + private String isList; + + /** + * 是否查询字段(1是) + */ + private String isQuery; + + /** + * 查询方式(EQ等于、NE不等于、GT大于、LT小于、LIKE模糊、BETWEEN范围) + */ + private String queryType; + + /** + * 显示类型(input文本框、textarea文本域、select下拉框、checkbox复选框、radio单选框、datetime日期控件、image图片上传控件、upload文件上传控件、editor富文本控件) + */ + private String htmlType; + + /** + * 字典类型 + */ + private String dictType; + + /** + * 排序 + */ + private Integer sort; + + public void setColumnId(Long columnId) { + this.columnId = columnId; + } + + public Long getColumnId() { + return columnId; + } + + public void setTableId(Long tableId) { + this.tableId = tableId; + } + + public Long getTableId() { + return tableId; + } + + public void setColumnName(String columnName) { + this.columnName = columnName; + } + + public String getColumnName() { + return columnName; + } + + public void setColumnComment(String columnComment) { + this.columnComment = columnComment; + } + + public String getColumnComment() { + return columnComment; + } + + public void setColumnType(String columnType) { + this.columnType = columnType; + } + + public String getColumnType() { + return columnType; + } + + public void setJavaType(String javaType) { + this.javaType = javaType; + } + + public String getJavaType() { + return javaType; + } + + public void setJavaField(String javaField) { + this.javaField = javaField; + } + + public String getJavaField() { + return javaField; + } + + public String getCapJavaField() { + return StringUtils.capitalize(javaField); + } + + public void setIsPk(String isPk) { + this.isPk = isPk; + } + + public String getIsPk() { + return isPk; + } + + public boolean isPk() { + return isPk(this.isPk); + } + + public boolean isPk(String isPk) { + return isPk != null && StringUtils.equals("1", isPk); + } + + public String getIsIncrement() { + return isIncrement; + } + + public void setIsIncrement(String isIncrement) { + this.isIncrement = isIncrement; + } + + public boolean isIncrement() { + return isIncrement(this.isIncrement); + } + + public boolean isIncrement(String isIncrement) { + return isIncrement != null && StringUtils.equals("1", isIncrement); + } + + public void setIsRequired(String isRequired) { + this.isRequired = isRequired; + } + + public String getIsRequired() { + return isRequired; + } + + public boolean isRequired() { + return isRequired(this.isRequired); + } + + public boolean isRequired(String isRequired) { + return isRequired != null && StringUtils.equals("1", isRequired); + } + + public void setIsInsert(String isInsert) { + this.isInsert = isInsert; + } + + public String getIsInsert() { + return isInsert; + } + + public boolean isInsert() { + return isInsert(this.isInsert); + } + + public boolean isInsert(String isInsert) { + return isInsert != null && StringUtils.equals("1", isInsert); + } + + public void setIsEdit(String isEdit) { + this.isEdit = isEdit; + } + + public String getIsEdit() { + return isEdit; + } + + public boolean isEdit() { + return isInsert(this.isEdit); + } + + public boolean isEdit(String isEdit) { + return isEdit != null && StringUtils.equals("1", isEdit); + } + + public void setIsList(String isList) { + this.isList = isList; + } + + public String getIsList() { + return isList; + } + + public boolean isList() { + return isList(this.isList); + } + + public boolean isList(String isList) { + return isList != null && StringUtils.equals("1", isList); + } + + public void setIsQuery(String isQuery) { + this.isQuery = isQuery; + } + + public String getIsQuery() { + return isQuery; + } + + public boolean isQuery() { + return isQuery(this.isQuery); + } + + public boolean isQuery(String isQuery) { + return isQuery != null && StringUtils.equals("1", isQuery); + } + + public void setQueryType(String queryType) { + this.queryType = queryType; + } + + public String getQueryType() { + return queryType; + } + + public String getHtmlType() { + return htmlType; + } + + public void setHtmlType(String htmlType) { + this.htmlType = htmlType; + } + + public void setDictType(String dictType) { + this.dictType = dictType; + } + + public String getDictType() { + return dictType; + } + + public void setSort(Integer sort) { + this.sort = sort; + } + + public Integer getSort() { + return sort; + } + + public boolean isSuperColumn() { + return isSuperColumn(this.javaField); + } + + public static boolean isSuperColumn(String javaField) { + return StringUtils.equalsAnyIgnoreCase(javaField, + // BaseEntity + "createBy", "createTime", "updateBy", "updateTime", "remark", + // TreeEntity + "parentName", "parentId", "orderNum", "ancestors"); + } + + public boolean isUsableColumn() { + return isUsableColumn(javaField); + } + + public static boolean isUsableColumn(String javaField) { + // isSuperColumn()中的名单用于避免生成多余Domain属性,若某些属性在生成页面时需要用到不能忽略,则放在此处白名单 + return StringUtils.equalsAnyIgnoreCase(javaField, "parentId", "orderNum", "remark"); + } + + public String readConverterExp() { + String remarks = StringUtils.substringBetween(this.columnComment, "(", ")"); + StringBuffer sb = new StringBuffer(); + if (StringUtils.isNotEmpty(remarks)) { + for (String value : remarks.split(" ")) { + if (StringUtils.isNotEmpty(value)) { + Object startStr = value.subSequence(0, 1); + String endStr = value.substring(1); + sb.append("").append(startStr).append("=").append(endStr).append(","); + } + } + return sb.deleteCharAt(sb.length() - 1).toString(); + } else { + return this.columnComment; + } + } +} diff --git a/jsowell-generator/src/main/java/com/jsowell/generator/mapper/GenTableColumnMapper.java b/jsowell-generator/src/main/java/com/jsowell/generator/mapper/GenTableColumnMapper.java new file mode 100644 index 000000000..120697836 --- /dev/null +++ b/jsowell-generator/src/main/java/com/jsowell/generator/mapper/GenTableColumnMapper.java @@ -0,0 +1,62 @@ +package com.jsowell.generator.mapper; + +import com.jsowell.generator.domain.GenTableColumn; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 业务字段 数据层 + * + * @author jsowell + */ +@Repository +public interface GenTableColumnMapper { + /** + * 根据表名称查询列信息 + * + * @param tableName 表名称 + * @return 列信息 + */ + public List selectDbTableColumnsByName(String tableName); + + /** + * 查询业务字段列表 + * + * @param tableId 业务字段编号 + * @return 业务字段集合 + */ + public List selectGenTableColumnListByTableId(Long tableId); + + /** + * 新增业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int insertGenTableColumn(GenTableColumn genTableColumn); + + /** + * 修改业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int updateGenTableColumn(GenTableColumn genTableColumn); + + /** + * 删除业务字段 + * + * @param genTableColumns 列数据 + * @return 结果 + */ + public int deleteGenTableColumns(List genTableColumns); + + /** + * 批量删除业务字段 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteGenTableColumnByIds(Long[] ids); +} diff --git a/jsowell-generator/src/main/java/com/jsowell/generator/mapper/GenTableMapper.java b/jsowell-generator/src/main/java/com/jsowell/generator/mapper/GenTableMapper.java new file mode 100644 index 000000000..049279544 --- /dev/null +++ b/jsowell-generator/src/main/java/com/jsowell/generator/mapper/GenTableMapper.java @@ -0,0 +1,85 @@ +package com.jsowell.generator.mapper; + +import com.jsowell.generator.domain.GenTable; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 业务 数据层 + * + * @author jsowell + */ +@Repository +public interface GenTableMapper { + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + public List selectGenTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + public List selectDbTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + public List selectDbTableListByNames(String[] tableNames); + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + public List selectGenTableAll(); + + /** + * 查询表ID业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + public GenTable selectGenTableById(Long id); + + /** + * 查询表名称业务信息 + * + * @param tableName 表名称 + * @return 业务信息 + */ + public GenTable selectGenTableByName(String tableName); + + /** + * 新增业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + public int insertGenTable(GenTable genTable); + + /** + * 修改业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + public int updateGenTable(GenTable genTable); + + /** + * 批量删除业务 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteGenTableByIds(Long[] ids); +} diff --git a/jsowell-generator/src/main/java/com/jsowell/generator/service/IGenTableColumnService.java b/jsowell-generator/src/main/java/com/jsowell/generator/service/IGenTableColumnService.java new file mode 100644 index 000000000..1e1dc3553 --- /dev/null +++ b/jsowell-generator/src/main/java/com/jsowell/generator/service/IGenTableColumnService.java @@ -0,0 +1,44 @@ +package com.jsowell.generator.service; + +import com.jsowell.generator.domain.GenTableColumn; + +import java.util.List; + +/** + * 业务字段 服务层 + * + * @author jsowell + */ +public interface IGenTableColumnService { + /** + * 查询业务字段列表 + * + * @param tableId 业务字段编号 + * @return 业务字段集合 + */ + public List selectGenTableColumnListByTableId(Long tableId); + + /** + * 新增业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int insertGenTableColumn(GenTableColumn genTableColumn); + + /** + * 修改业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int updateGenTableColumn(GenTableColumn genTableColumn); + + /** + * 删除业务字段信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteGenTableColumnByIds(String ids); +} diff --git a/jsowell-generator/src/main/java/com/jsowell/generator/service/IGenTableService.java b/jsowell-generator/src/main/java/com/jsowell/generator/service/IGenTableService.java new file mode 100644 index 000000000..69ae9d57e --- /dev/null +++ b/jsowell-generator/src/main/java/com/jsowell/generator/service/IGenTableService.java @@ -0,0 +1,121 @@ +package com.jsowell.generator.service; + +import com.jsowell.generator.domain.GenTable; + +import java.util.List; +import java.util.Map; + +/** + * 业务 服务层 + * + * @author jsowell + */ +public interface IGenTableService { + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + public List selectGenTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + public List selectDbTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + public List selectDbTableListByNames(String[] tableNames); + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + public List selectGenTableAll(); + + /** + * 查询业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + public GenTable selectGenTableById(Long id); + + /** + * 修改业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + public void updateGenTable(GenTable genTable); + + /** + * 删除业务信息 + * + * @param tableIds 需要删除的表数据ID + * @return 结果 + */ + public void deleteGenTableByIds(Long[] tableIds); + + /** + * 导入表结构 + * + * @param tableList 导入表列表 + */ + public void importGenTable(List tableList); + + /** + * 预览代码 + * + * @param tableId 表编号 + * @return 预览数据列表 + */ + public Map previewCode(Long tableId); + + /** + * 生成代码(下载方式) + * + * @param tableName 表名称 + * @return 数据 + */ + public byte[] downloadCode(String tableName); + + /** + * 生成代码(自定义路径) + * + * @param tableName 表名称 + * @return 数据 + */ + public void generatorCode(String tableName); + + /** + * 同步数据库 + * + * @param tableName 表名称 + */ + public void synchDb(String tableName); + + /** + * 批量生成代码(下载方式) + * + * @param tableNames 表数组 + * @return 数据 + */ + public byte[] downloadCode(String[] tableNames); + + /** + * 修改保存参数校验 + * + * @param genTable 业务信息 + */ + public void validateEdit(GenTable genTable); +} diff --git a/jsowell-generator/src/main/java/com/jsowell/generator/service/impl/GenTableColumnServiceImpl.java b/jsowell-generator/src/main/java/com/jsowell/generator/service/impl/GenTableColumnServiceImpl.java new file mode 100644 index 000000000..96e480362 --- /dev/null +++ b/jsowell-generator/src/main/java/com/jsowell/generator/service/impl/GenTableColumnServiceImpl.java @@ -0,0 +1,65 @@ +package com.jsowell.generator.service.impl; + +import com.jsowell.common.core.text.Convert; +import com.jsowell.generator.domain.GenTableColumn; +import com.jsowell.generator.mapper.GenTableColumnMapper; +import com.jsowell.generator.service.IGenTableColumnService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 业务字段 服务层实现 + * + * @author jsowell + */ +@Service +public class GenTableColumnServiceImpl implements IGenTableColumnService { + @Autowired + private GenTableColumnMapper genTableColumnMapper; + + /** + * 查询业务字段列表 + * + * @param tableId 业务字段编号 + * @return 业务字段集合 + */ + @Override + public List selectGenTableColumnListByTableId(Long tableId) { + return genTableColumnMapper.selectGenTableColumnListByTableId(tableId); + } + + /** + * 新增业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + @Override + public int insertGenTableColumn(GenTableColumn genTableColumn) { + return genTableColumnMapper.insertGenTableColumn(genTableColumn); + } + + /** + * 修改业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + @Override + public int updateGenTableColumn(GenTableColumn genTableColumn) { + return genTableColumnMapper.updateGenTableColumn(genTableColumn); + } + + /** + * 删除业务字段对象 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + @Override + public int deleteGenTableColumnByIds(String ids) { + return genTableColumnMapper.deleteGenTableColumnByIds(Convert.toLongArray(ids)); + } +} diff --git a/jsowell-generator/src/main/java/com/jsowell/generator/service/impl/GenTableServiceImpl.java b/jsowell-generator/src/main/java/com/jsowell/generator/service/impl/GenTableServiceImpl.java new file mode 100644 index 000000000..fbb83851b --- /dev/null +++ b/jsowell-generator/src/main/java/com/jsowell/generator/service/impl/GenTableServiceImpl.java @@ -0,0 +1,456 @@ +package com.jsowell.generator.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.constant.GenConstants; +import com.jsowell.common.core.text.CharsetKit; +import com.jsowell.common.exception.ServiceException; +import com.jsowell.common.util.SecurityUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.generator.domain.GenTable; +import com.jsowell.generator.domain.GenTableColumn; +import com.jsowell.generator.mapper.GenTableColumnMapper; +import com.jsowell.generator.mapper.GenTableMapper; +import com.jsowell.generator.service.IGenTableService; +import com.jsowell.generator.util.GenUtils; +import com.jsowell.generator.util.VelocityInitializer; +import com.jsowell.generator.util.VelocityUtils; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.velocity.Template; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.Velocity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * 业务 服务层实现 + * + * @author jsowell + */ +@Service +public class GenTableServiceImpl implements IGenTableService { + private static final Logger log = LoggerFactory.getLogger(GenTableServiceImpl.class); + + @Autowired + private GenTableMapper genTableMapper; + + @Autowired + private GenTableColumnMapper genTableColumnMapper; + + /** + * 查询业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + @Override + public GenTable selectGenTableById(Long id) { + GenTable genTable = genTableMapper.selectGenTableById(id); + setTableFromOptions(genTable); + return genTable; + } + + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + @Override + public List selectGenTableList(GenTable genTable) { + return genTableMapper.selectGenTableList(genTable); + } + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + @Override + public List selectDbTableList(GenTable genTable) { + return genTableMapper.selectDbTableList(genTable); + } + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + @Override + public List selectDbTableListByNames(String[] tableNames) { + return genTableMapper.selectDbTableListByNames(tableNames); + } + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + @Override + public List selectGenTableAll() { + return genTableMapper.selectGenTableAll(); + } + + /** + * 修改业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + @Override + @Transactional + public void updateGenTable(GenTable genTable) { + String options = JSON.toJSONString(genTable.getParams()); + genTable.setOptions(options); + int row = genTableMapper.updateGenTable(genTable); + if (row > 0) { + for (GenTableColumn cenTableColumn : genTable.getColumns()) { + genTableColumnMapper.updateGenTableColumn(cenTableColumn); + } + } + } + + /** + * 删除业务对象 + * + * @param tableIds 需要删除的数据ID + * @return 结果 + */ + @Override + @Transactional + public void deleteGenTableByIds(Long[] tableIds) { + genTableMapper.deleteGenTableByIds(tableIds); + genTableColumnMapper.deleteGenTableColumnByIds(tableIds); + } + + /** + * 导入表结构 + * + * @param tableList 导入表列表 + */ + @Override + @Transactional + public void importGenTable(List tableList) { + String operName = SecurityUtils.getUsername(); + try { + for (GenTable table : tableList) { + String tableName = table.getTableName(); + GenUtils.initTable(table, operName); + int row = genTableMapper.insertGenTable(table); + if (row > 0) { + // 保存列信息 + List genTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName); + for (GenTableColumn column : genTableColumns) { + GenUtils.initColumnField(column, table); + genTableColumnMapper.insertGenTableColumn(column); + } + } + } + } catch (Exception e) { + throw new ServiceException("导入失败:" + e.getMessage()); + } + } + + /** + * 预览代码 + * + * @param tableId 表编号 + * @return 预览数据列表 + */ + @Override + public Map previewCode(Long tableId) { + Map dataMap = new LinkedHashMap<>(); + // 查询表信息 + GenTable table = genTableMapper.selectGenTableById(tableId); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + VelocityInitializer.initVelocity(); + + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table.getTplCategory()); + for (String template : templates) { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, Constants.UTF8); + tpl.merge(context, sw); + dataMap.put(template, sw.toString()); + } + return dataMap; + } + + /** + * 生成代码(下载方式) + * + * @param tableName 表名称 + * @return 数据 + */ + @Override + public byte[] downloadCode(String tableName) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream); + generatorCode(tableName, zip); + IOUtils.closeQuietly(zip); + return outputStream.toByteArray(); + } + + /** + * 生成代码(自定义路径) + * + * @param tableName 表名称 + */ + @Override + public void generatorCode(String tableName) { + // 查询表信息 + GenTable table = genTableMapper.selectGenTableByName(tableName); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + + VelocityInitializer.initVelocity(); + + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table.getTplCategory()); + for (String template : templates) { + if (!StringUtils.containsAny(template, "sql.vm", "api.js.vm", "index.vue.vm", "index-tree.vue.vm")) { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, Constants.UTF8); + tpl.merge(context, sw); + try { + String path = getGenPath(table, template); + FileUtils.writeStringToFile(new File(path), sw.toString(), CharsetKit.UTF_8); + } catch (IOException e) { + throw new ServiceException("渲染模板失败,表名:" + table.getTableName()); + } + } + } + } + + /** + * 同步数据库 + * + * @param tableName 表名称 + */ + @Override + @Transactional + public void synchDb(String tableName) { + GenTable table = genTableMapper.selectGenTableByName(tableName); + List tableColumns = table.getColumns(); + Map tableColumnMap = tableColumns.stream().collect(Collectors.toMap(GenTableColumn::getColumnName, Function.identity())); + + List dbTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName); + if (StringUtils.isEmpty(dbTableColumns)) { + throw new ServiceException("同步数据失败,原表结构不存在"); + } + List dbTableColumnNames = dbTableColumns.stream().map(GenTableColumn::getColumnName).collect(Collectors.toList()); + + dbTableColumns.forEach(column -> { + GenUtils.initColumnField(column, table); + if (tableColumnMap.containsKey(column.getColumnName())) { + GenTableColumn prevColumn = tableColumnMap.get(column.getColumnName()); + column.setColumnId(prevColumn.getColumnId()); + if (column.isList()) { + // 如果是列表,继续保留查询方式/字典类型选项 + column.setDictType(prevColumn.getDictType()); + column.setQueryType(prevColumn.getQueryType()); + } + if (StringUtils.isNotEmpty(prevColumn.getIsRequired()) && !column.isPk() + && (column.isInsert() || column.isEdit()) + && ((column.isUsableColumn()) || (!column.isSuperColumn()))) { + // 如果是(新增/修改&非主键/非忽略及父属性),继续保留必填/显示类型选项 + column.setIsRequired(prevColumn.getIsRequired()); + column.setHtmlType(prevColumn.getHtmlType()); + } + genTableColumnMapper.updateGenTableColumn(column); + } else { + genTableColumnMapper.insertGenTableColumn(column); + } + }); + + List delColumns = tableColumns.stream().filter(column -> !dbTableColumnNames.contains(column.getColumnName())).collect(Collectors.toList()); + if (StringUtils.isNotEmpty(delColumns)) { + genTableColumnMapper.deleteGenTableColumns(delColumns); + } + } + + /** + * 批量生成代码(下载方式) + * + * @param tableNames 表数组 + * @return 数据 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream); + for (String tableName : tableNames) { + generatorCode(tableName, zip); + } + IOUtils.closeQuietly(zip); + return outputStream.toByteArray(); + } + + /** + * 查询表信息并生成代码 + */ + private void generatorCode(String tableName, ZipOutputStream zip) { + // 查询表信息 + GenTable table = genTableMapper.selectGenTableByName(tableName); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + + VelocityInitializer.initVelocity(); + + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table.getTplCategory()); + for (String template : templates) { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, Constants.UTF8); + tpl.merge(context, sw); + try { + // 添加到zip + zip.putNextEntry(new ZipEntry(VelocityUtils.getFileName(template, table))); + IOUtils.write(sw.toString(), zip, Constants.UTF8); + IOUtils.closeQuietly(sw); + zip.flush(); + zip.closeEntry(); + } catch (IOException e) { + log.error("渲染模板失败,表名:" + table.getTableName(), e); + } + } + } + + /** + * 修改保存参数校验 + * + * @param genTable 业务信息 + */ + @Override + public void validateEdit(GenTable genTable) { + if (GenConstants.TPL_TREE.equals(genTable.getTplCategory())) { + String options = JSON.toJSONString(genTable.getParams()); + JSONObject paramsObj = JSON.parseObject(options); + if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_CODE))) { + throw new ServiceException("树编码字段不能为空"); + } else if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_PARENT_CODE))) { + throw new ServiceException("树父编码字段不能为空"); + } else if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_NAME))) { + throw new ServiceException("树名称字段不能为空"); + } else if (GenConstants.TPL_SUB.equals(genTable.getTplCategory())) { + if (StringUtils.isEmpty(genTable.getSubTableName())) { + throw new ServiceException("关联子表的表名不能为空"); + } else if (StringUtils.isEmpty(genTable.getSubTableFkName())) { + throw new ServiceException("子表关联的外键名不能为空"); + } + } + } + } + + /** + * 设置主键列信息 + * + * @param table 业务表信息 + */ + public void setPkColumn(GenTable table) { + for (GenTableColumn column : table.getColumns()) { + if (column.isPk()) { + table.setPkColumn(column); + break; + } + } + if (StringUtils.isNull(table.getPkColumn())) { + table.setPkColumn(table.getColumns().get(0)); + } + if (GenConstants.TPL_SUB.equals(table.getTplCategory())) { + for (GenTableColumn column : table.getSubTable().getColumns()) { + if (column.isPk()) { + table.getSubTable().setPkColumn(column); + break; + } + } + if (StringUtils.isNull(table.getSubTable().getPkColumn())) { + table.getSubTable().setPkColumn(table.getSubTable().getColumns().get(0)); + } + } + } + + /** + * 设置主子表信息 + * + * @param table 业务表信息 + */ + public void setSubTable(GenTable table) { + String subTableName = table.getSubTableName(); + if (StringUtils.isNotEmpty(subTableName)) { + table.setSubTable(genTableMapper.selectGenTableByName(subTableName)); + } + } + + /** + * 设置代码生成其他选项值 + * + * @param genTable 设置后的生成对象 + */ + public void setTableFromOptions(GenTable genTable) { + JSONObject paramsObj = JSON.parseObject(genTable.getOptions()); + if (StringUtils.isNotNull(paramsObj)) { + String treeCode = paramsObj.getString(GenConstants.TREE_CODE); + String treeParentCode = paramsObj.getString(GenConstants.TREE_PARENT_CODE); + String treeName = paramsObj.getString(GenConstants.TREE_NAME); + String parentMenuId = paramsObj.getString(GenConstants.PARENT_MENU_ID); + String parentMenuName = paramsObj.getString(GenConstants.PARENT_MENU_NAME); + + genTable.setTreeCode(treeCode); + genTable.setTreeParentCode(treeParentCode); + genTable.setTreeName(treeName); + genTable.setParentMenuId(parentMenuId); + genTable.setParentMenuName(parentMenuName); + } + } + + /** + * 获取代码生成地址 + * + * @param table 业务表信息 + * @param template 模板文件路径 + * @return 生成地址 + */ + public static String getGenPath(GenTable table, String template) { + String genPath = table.getGenPath(); + if (StringUtils.equals(genPath, "/")) { + return System.getProperty("user.dir") + File.separator + "src" + File.separator + VelocityUtils.getFileName(template, table); + } + return genPath + File.separator + VelocityUtils.getFileName(template, table); + } +} \ No newline at end of file diff --git a/jsowell-generator/src/main/java/com/jsowell/generator/util/GenUtils.java b/jsowell-generator/src/main/java/com/jsowell/generator/util/GenUtils.java new file mode 100644 index 000000000..7ed1e8381 --- /dev/null +++ b/jsowell-generator/src/main/java/com/jsowell/generator/util/GenUtils.java @@ -0,0 +1,221 @@ +package com.jsowell.generator.util; + +import com.jsowell.common.constant.GenConstants; +import com.jsowell.common.util.StringUtils; +import com.jsowell.generator.config.GenConfig; +import com.jsowell.generator.domain.GenTable; +import com.jsowell.generator.domain.GenTableColumn; +import org.apache.commons.lang3.RegExUtils; + +import java.util.Arrays; + +/** + * 代码生成器 工具类 + * + * @author jsowell + */ +public class GenUtils { + /** + * 初始化表信息 + */ + public static void initTable(GenTable genTable, String operName) { + genTable.setClassName(convertClassName(genTable.getTableName())); + genTable.setPackageName(GenConfig.getPackageName()); + genTable.setModuleName(getModuleName(GenConfig.getPackageName())); + genTable.setBusinessName(getBusinessName(genTable.getTableName())); + genTable.setFunctionName(replaceText(genTable.getTableComment())); + genTable.setFunctionAuthor(GenConfig.getAuthor()); + genTable.setCreateBy(operName); + } + + /** + * 初始化列属性字段 + */ + public static void initColumnField(GenTableColumn column, GenTable table) { + String dataType = getDbType(column.getColumnType()); + String columnName = column.getColumnName(); + column.setTableId(table.getTableId()); + column.setCreateBy(table.getCreateBy()); + // 设置java字段名 + column.setJavaField(StringUtils.toCamelCase(columnName)); + // 设置默认类型 + column.setJavaType(GenConstants.TYPE_STRING); + column.setQueryType(GenConstants.QUERY_EQ); + + if (arraysContains(GenConstants.COLUMNTYPE_STR, dataType) || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType)) { + // 字符串长度超过500设置为文本域 + Integer columnLength = getColumnLength(column.getColumnType()); + String htmlType = columnLength >= 500 || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType) ? GenConstants.HTML_TEXTAREA : GenConstants.HTML_INPUT; + column.setHtmlType(htmlType); + } else if (arraysContains(GenConstants.COLUMNTYPE_TIME, dataType)) { + column.setJavaType(GenConstants.TYPE_DATE); + column.setHtmlType(GenConstants.HTML_DATETIME); + } else if (arraysContains(GenConstants.COLUMNTYPE_NUMBER, dataType)) { + column.setHtmlType(GenConstants.HTML_INPUT); + + // 如果是浮点型 统一用BigDecimal + String[] str = StringUtils.split(StringUtils.substringBetween(column.getColumnType(), "(", ")"), ","); + if (str != null && str.length == 2 && Integer.parseInt(str[1]) > 0) { + column.setJavaType(GenConstants.TYPE_BIGDECIMAL); + } + // 如果是整形 + else if (str != null && str.length == 1 && Integer.parseInt(str[0]) <= 10) { + column.setJavaType(GenConstants.TYPE_INTEGER); + } + // 长整形 + else { + column.setJavaType(GenConstants.TYPE_LONG); + } + } + + // 插入字段(默认所有字段都需要插入) + column.setIsInsert(GenConstants.REQUIRE); + + // 编辑字段 + if (!arraysContains(GenConstants.COLUMNNAME_NOT_EDIT, columnName) && !column.isPk()) { + column.setIsEdit(GenConstants.REQUIRE); + } + // 列表字段 + if (!arraysContains(GenConstants.COLUMNNAME_NOT_LIST, columnName) && !column.isPk()) { + column.setIsList(GenConstants.REQUIRE); + } + // 查询字段 + if (!arraysContains(GenConstants.COLUMNNAME_NOT_QUERY, columnName) && !column.isPk()) { + column.setIsQuery(GenConstants.REQUIRE); + } + + // 查询字段类型 + if (StringUtils.endsWithIgnoreCase(columnName, "name")) { + column.setQueryType(GenConstants.QUERY_LIKE); + } + // 状态字段设置单选框 + if (StringUtils.endsWithIgnoreCase(columnName, "status")) { + column.setHtmlType(GenConstants.HTML_RADIO); + } + // 类型&性别字段设置下拉框 + else if (StringUtils.endsWithIgnoreCase(columnName, "type") + || StringUtils.endsWithIgnoreCase(columnName, "sex")) { + column.setHtmlType(GenConstants.HTML_SELECT); + } + // 图片字段设置图片上传控件 + else if (StringUtils.endsWithIgnoreCase(columnName, "image")) { + column.setHtmlType(GenConstants.HTML_IMAGE_UPLOAD); + } + // 文件字段设置文件上传控件 + else if (StringUtils.endsWithIgnoreCase(columnName, "file")) { + column.setHtmlType(GenConstants.HTML_FILE_UPLOAD); + } + // 内容字段设置富文本控件 + else if (StringUtils.endsWithIgnoreCase(columnName, "content")) { + column.setHtmlType(GenConstants.HTML_EDITOR); + } + } + + /** + * 校验数组是否包含指定值 + * + * @param arr 数组 + * @param targetValue 值 + * @return 是否包含 + */ + public static boolean arraysContains(String[] arr, String targetValue) { + return Arrays.asList(arr).contains(targetValue); + } + + /** + * 获取模块名 + * + * @param packageName 包名 + * @return 模块名 + */ + public static String getModuleName(String packageName) { + int lastIndex = packageName.lastIndexOf("."); + int nameLength = packageName.length(); + return StringUtils.substring(packageName, lastIndex + 1, nameLength); + } + + /** + * 获取业务名 + * + * @param tableName 表名 + * @return 业务名 + */ + public static String getBusinessName(String tableName) { + int lastIndex = tableName.lastIndexOf("_"); + int nameLength = tableName.length(); + return StringUtils.substring(tableName, lastIndex + 1, nameLength); + } + + /** + * 表名转换成Java类名 + * + * @param tableName 表名称 + * @return 类名 + */ + public static String convertClassName(String tableName) { + boolean autoRemovePre = GenConfig.getAutoRemovePre(); + String tablePrefix = GenConfig.getTablePrefix(); + if (autoRemovePre && StringUtils.isNotEmpty(tablePrefix)) { + String[] searchList = StringUtils.split(tablePrefix, ","); + tableName = replaceFirst(tableName, searchList); + } + return StringUtils.convertToCamelCase(tableName); + } + + /** + * 批量替换前缀 + * + * @param replacementm 替换值 + * @param searchList 替换列表 + * @return + */ + public static String replaceFirst(String replacementm, String[] searchList) { + String text = replacementm; + for (String searchString : searchList) { + if (replacementm.startsWith(searchString)) { + text = replacementm.replaceFirst(searchString, ""); + break; + } + } + return text; + } + + /** + * 关键字替换 + * + * @param text 需要被替换的名字 + * @return 替换后的名字 + */ + public static String replaceText(String text) { + return RegExUtils.replaceAll(text, "(?:表|若依)", ""); + } + + /** + * 获取数据库类型字段 + * + * @param columnType 列类型 + * @return 截取后的列类型 + */ + public static String getDbType(String columnType) { + if (StringUtils.indexOf(columnType, "(") > 0) { + return StringUtils.substringBefore(columnType, "("); + } else { + return columnType; + } + } + + /** + * 获取字段长度 + * + * @param columnType 列类型 + * @return 截取后的列类型 + */ + public static Integer getColumnLength(String columnType) { + if (StringUtils.indexOf(columnType, "(") > 0) { + String length = StringUtils.substringBetween(columnType, "(", ")"); + return Integer.valueOf(length); + } else { + return 0; + } + } +} diff --git a/jsowell-generator/src/main/java/com/jsowell/generator/util/VelocityInitializer.java b/jsowell-generator/src/main/java/com/jsowell/generator/util/VelocityInitializer.java new file mode 100644 index 000000000..51799a06b --- /dev/null +++ b/jsowell-generator/src/main/java/com/jsowell/generator/util/VelocityInitializer.java @@ -0,0 +1,30 @@ +package com.jsowell.generator.util; + +import com.jsowell.common.constant.Constants; +import org.apache.velocity.app.Velocity; + +import java.util.Properties; + +/** + * VelocityEngine工厂 + * + * @author jsowell + */ +public class VelocityInitializer { + /** + * 初始化vm方法 + */ + public static void initVelocity() { + Properties p = new Properties(); + try { + // 加载classpath目录下的vm文件 + p.setProperty("resource.loader.file.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader"); + // 定义字符集 + p.setProperty(Velocity.INPUT_ENCODING, Constants.UTF8); + // 初始化Velocity引擎,指定配置Properties + Velocity.init(p); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/jsowell-generator/src/main/java/com/jsowell/generator/util/VelocityUtils.java b/jsowell-generator/src/main/java/com/jsowell/generator/util/VelocityUtils.java new file mode 100644 index 000000000..5224128f0 --- /dev/null +++ b/jsowell-generator/src/main/java/com/jsowell/generator/util/VelocityUtils.java @@ -0,0 +1,348 @@ +package com.jsowell.generator.util; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.constant.GenConstants; +import com.jsowell.common.util.DateUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.generator.domain.GenTable; +import com.jsowell.generator.domain.GenTableColumn; +import org.apache.velocity.VelocityContext; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * 模板处理工具类 + * + * @author jsowell + */ +public class VelocityUtils { + /** + * 项目空间路径 + */ + private static final String PROJECT_PATH = "main/java"; + + /** + * mybatis空间路径 + */ + private static final String MYBATIS_PATH = "main/resources/mapper"; + + /** + * 默认上级菜单,系统工具 + */ + private static final String DEFAULT_PARENT_MENU_ID = "3"; + + /** + * 设置模板变量信息 + * + * @return 模板列表 + */ + public static VelocityContext prepareContext(GenTable genTable) { + String moduleName = genTable.getModuleName(); + String businessName = genTable.getBusinessName(); + String packageName = genTable.getPackageName(); + String tplCategory = genTable.getTplCategory(); + String functionName = genTable.getFunctionName(); + + VelocityContext velocityContext = new VelocityContext(); + velocityContext.put("tplCategory", genTable.getTplCategory()); + velocityContext.put("tableName", genTable.getTableName()); + velocityContext.put("functionName", StringUtils.isNotEmpty(functionName) ? functionName : "【请填写功能名称】"); + velocityContext.put("ClassName", genTable.getClassName()); + velocityContext.put("className", StringUtils.uncapitalize(genTable.getClassName())); + velocityContext.put("moduleName", genTable.getModuleName()); + velocityContext.put("BusinessName", StringUtils.capitalize(genTable.getBusinessName())); + velocityContext.put("businessName", genTable.getBusinessName()); + velocityContext.put("basePackage", getPackagePrefix(packageName)); + velocityContext.put("packageName", packageName); + velocityContext.put("author", genTable.getFunctionAuthor()); + velocityContext.put("datetime", DateUtils.getDate()); + velocityContext.put("pkColumn", genTable.getPkColumn()); + velocityContext.put("importList", getImportList(genTable)); + velocityContext.put("permissionPrefix", getPermissionPrefix(moduleName, businessName)); + velocityContext.put("columns", genTable.getColumns()); + velocityContext.put("table", genTable); + velocityContext.put("dicts", getDicts(genTable)); + setMenuVelocityContext(velocityContext, genTable); + if (GenConstants.TPL_TREE.equals(tplCategory)) { + setTreeVelocityContext(velocityContext, genTable); + } + if (GenConstants.TPL_SUB.equals(tplCategory)) { + setSubVelocityContext(velocityContext, genTable); + } + return velocityContext; + } + + public static void setMenuVelocityContext(VelocityContext context, GenTable genTable) { + String options = genTable.getOptions(); + JSONObject paramsObj = JSON.parseObject(options); + String parentMenuId = getParentMenuId(paramsObj); + context.put("parentMenuId", parentMenuId); + } + + public static void setTreeVelocityContext(VelocityContext context, GenTable genTable) { + String options = genTable.getOptions(); + JSONObject paramsObj = JSON.parseObject(options); + String treeCode = getTreecode(paramsObj); + String treeParentCode = getTreeParentCode(paramsObj); + String treeName = getTreeName(paramsObj); + + context.put("treeCode", treeCode); + context.put("treeParentCode", treeParentCode); + context.put("treeName", treeName); + context.put("expandColumn", getExpandColumn(genTable)); + if (paramsObj.containsKey(GenConstants.TREE_PARENT_CODE)) { + context.put("tree_parent_code", paramsObj.getString(GenConstants.TREE_PARENT_CODE)); + } + if (paramsObj.containsKey(GenConstants.TREE_NAME)) { + context.put("tree_name", paramsObj.getString(GenConstants.TREE_NAME)); + } + } + + public static void setSubVelocityContext(VelocityContext context, GenTable genTable) { + GenTable subTable = genTable.getSubTable(); + String subTableName = genTable.getSubTableName(); + String subTableFkName = genTable.getSubTableFkName(); + String subClassName = genTable.getSubTable().getClassName(); + String subTableFkClassName = StringUtils.convertToCamelCase(subTableFkName); + + context.put("subTable", subTable); + context.put("subTableName", subTableName); + context.put("subTableFkName", subTableFkName); + context.put("subTableFkClassName", subTableFkClassName); + context.put("subTableFkclassName", StringUtils.uncapitalize(subTableFkClassName)); + context.put("subClassName", subClassName); + context.put("subclassName", StringUtils.uncapitalize(subClassName)); + context.put("subImportList", getImportList(genTable.getSubTable())); + } + + /** + * 获取模板信息 + * + * @return 模板列表 + */ + public static List getTemplateList(String tplCategory) { + List templates = new ArrayList(); + templates.add("vm/java/domain.java.vm"); + templates.add("vm/java/mapper.java.vm"); + templates.add("vm/java/service.java.vm"); + templates.add("vm/java/serviceImpl.java.vm"); + templates.add("vm/java/controller.java.vm"); + templates.add("vm/xml/mapper.xml.vm"); + templates.add("vm/sql/sql.vm"); + templates.add("vm/js/api.js.vm"); + if (GenConstants.TPL_CRUD.equals(tplCategory)) { + templates.add("vm/vue/index.vue.vm"); + } else if (GenConstants.TPL_TREE.equals(tplCategory)) { + templates.add("vm/vue/index-tree.vue.vm"); + } else if (GenConstants.TPL_SUB.equals(tplCategory)) { + templates.add("vm/vue/index.vue.vm"); + templates.add("vm/java/sub-domain.java.vm"); + } + return templates; + } + + /** + * 获取文件名 + */ + public static String getFileName(String template, GenTable genTable) { + // 文件名称 + String fileName = ""; + // 包路径 + String packageName = genTable.getPackageName(); + // 模块名 + String moduleName = genTable.getModuleName(); + // 大写类名 + String className = genTable.getClassName(); + // 业务名称 + String businessName = genTable.getBusinessName(); + + String javaPath = PROJECT_PATH + "/" + StringUtils.replace(packageName, ".", "/"); + String mybatisPath = MYBATIS_PATH + "/" + moduleName; + String vuePath = "vue"; + + if (template.contains("domain.java.vm")) { + fileName = StringUtils.format("{}/domain/{}.java", javaPath, className); + } + if (template.contains("sub-domain.java.vm") && StringUtils.equals(GenConstants.TPL_SUB, genTable.getTplCategory())) { + fileName = StringUtils.format("{}/domain/{}.java", javaPath, genTable.getSubTable().getClassName()); + } else if (template.contains("mapper.java.vm")) { + fileName = StringUtils.format("{}/mapper/{}Mapper.java", javaPath, className); + } else if (template.contains("service.java.vm")) { + fileName = StringUtils.format("{}/service/I{}Service.java", javaPath, className); + } else if (template.contains("serviceImpl.java.vm")) { + fileName = StringUtils.format("{}/service/impl/{}ServiceImpl.java", javaPath, className); + } else if (template.contains("controller.java.vm")) { + fileName = StringUtils.format("{}/controller/{}Controller.java", javaPath, className); + } else if (template.contains("mapper.xml.vm")) { + fileName = StringUtils.format("{}/{}Mapper.xml", mybatisPath, className); + } else if (template.contains("sql.vm")) { + fileName = businessName + "Menu.sql"; + } else if (template.contains("api.js.vm")) { + fileName = StringUtils.format("{}/api/{}/{}.js", vuePath, moduleName, businessName); + } else if (template.contains("index.vue.vm")) { + fileName = StringUtils.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName); + } else if (template.contains("index-tree.vue.vm")) { + fileName = StringUtils.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName); + } + return fileName; + } + + /** + * 获取包前缀 + * + * @param packageName 包名称 + * @return 包前缀名称 + */ + public static String getPackagePrefix(String packageName) { + int lastIndex = packageName.lastIndexOf("."); + return StringUtils.substring(packageName, 0, lastIndex); + } + + /** + * 根据列类型获取导入包 + * + * @param genTable 业务表对象 + * @return 返回需要导入的包列表 + */ + public static HashSet getImportList(GenTable genTable) { + List columns = genTable.getColumns(); + GenTable subGenTable = genTable.getSubTable(); + HashSet importList = new HashSet(); + if (StringUtils.isNotNull(subGenTable)) { + importList.add("java.util.List"); + } + for (GenTableColumn column : columns) { + if (!column.isSuperColumn() && GenConstants.TYPE_DATE.equals(column.getJavaType())) { + importList.add("java.util.Date"); + importList.add("com.fasterxml.jackson.annotation.JsonFormat"); + } else if (!column.isSuperColumn() && GenConstants.TYPE_BIGDECIMAL.equals(column.getJavaType())) { + importList.add("java.math.BigDecimal"); + } + } + return importList; + } + + /** + * 根据列类型获取字典组 + * + * @param genTable 业务表对象 + * @return 返回字典组 + */ + public static String getDicts(GenTable genTable) { + List columns = genTable.getColumns(); + Set dicts = new HashSet(); + addDicts(dicts, columns); + if (StringUtils.isNotNull(genTable.getSubTable())) { + List subColumns = genTable.getSubTable().getColumns(); + addDicts(dicts, subColumns); + } + return StringUtils.join(dicts, ", "); + } + + /** + * 添加字典列表 + * + * @param dicts 字典列表 + * @param columns 列集合 + */ + public static void addDicts(Set dicts, List columns) { + for (GenTableColumn column : columns) { + if (!column.isSuperColumn() && StringUtils.isNotEmpty(column.getDictType()) && StringUtils.equalsAny( + column.getHtmlType(), + new String[]{GenConstants.HTML_SELECT, GenConstants.HTML_RADIO, GenConstants.HTML_CHECKBOX})) { + dicts.add("'" + column.getDictType() + "'"); + } + } + } + + /** + * 获取权限前缀 + * + * @param moduleName 模块名称 + * @param businessName 业务名称 + * @return 返回权限前缀 + */ + public static String getPermissionPrefix(String moduleName, String businessName) { + return StringUtils.format("{}:{}", moduleName, businessName); + } + + /** + * 获取上级菜单ID字段 + * + * @param paramsObj 生成其他选项 + * @return 上级菜单ID字段 + */ + public static String getParentMenuId(JSONObject paramsObj) { + if (StringUtils.isNotEmpty(paramsObj) && paramsObj.containsKey(GenConstants.PARENT_MENU_ID) + && StringUtils.isNotEmpty(paramsObj.getString(GenConstants.PARENT_MENU_ID))) { + return paramsObj.getString(GenConstants.PARENT_MENU_ID); + } + return DEFAULT_PARENT_MENU_ID; + } + + /** + * 获取树编码 + * + * @param paramsObj 生成其他选项 + * @return 树编码 + */ + public static String getTreecode(JSONObject paramsObj) { + if (paramsObj.containsKey(GenConstants.TREE_CODE)) { + return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_CODE)); + } + return StringUtils.EMPTY; + } + + /** + * 获取树父编码 + * + * @param paramsObj 生成其他选项 + * @return 树父编码 + */ + public static String getTreeParentCode(JSONObject paramsObj) { + if (paramsObj.containsKey(GenConstants.TREE_PARENT_CODE)) { + return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_PARENT_CODE)); + } + return StringUtils.EMPTY; + } + + /** + * 获取树名称 + * + * @param paramsObj 生成其他选项 + * @return 树名称 + */ + public static String getTreeName(JSONObject paramsObj) { + if (paramsObj.containsKey(GenConstants.TREE_NAME)) { + return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_NAME)); + } + return StringUtils.EMPTY; + } + + /** + * 获取需要在哪一列上面显示展开按钮 + * + * @param genTable 业务表对象 + * @return 展开按钮列序号 + */ + public static int getExpandColumn(GenTable genTable) { + String options = genTable.getOptions(); + JSONObject paramsObj = JSON.parseObject(options); + String treeName = paramsObj.getString(GenConstants.TREE_NAME); + int num = 0; + for (GenTableColumn column : genTable.getColumns()) { + if (column.isList()) { + num++; + String columnName = column.getColumnName(); + if (columnName.equals(treeName)) { + break; + } + } + } + return num; + } +} diff --git a/jsowell-generator/src/main/resources/generator.yml b/jsowell-generator/src/main/resources/generator.yml new file mode 100644 index 000000000..a2dfbb5c9 --- /dev/null +++ b/jsowell-generator/src/main/resources/generator.yml @@ -0,0 +1,10 @@ +# 代码生成 +gen: + # 作者 + author: jsowell + # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool + packageName: com.jsowell.pile + # 自动去除表前缀,默认是false + autoRemovePre: false + # 表前缀(生成类名不会包含表前缀,多个用逗号分隔) + tablePrefix: sys_ \ No newline at end of file diff --git a/jsowell-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml b/jsowell-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml new file mode 100644 index 000000000..13b9b2143 --- /dev/null +++ b/jsowell-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select column_id, table_id, column_name, column_comment, column_type, java_type, java_field, is_pk, is_increment, is_required, is_insert, is_edit, is_list, is_query, query_type, html_type, dict_type, sort, create_by, create_time, update_by, update_time from gen_table_column + + + + + + + + insert into gen_table_column ( + table_id, + column_name, + column_comment, + column_type, + java_type, + java_field, + is_pk, + is_increment, + is_required, + is_insert, + is_edit, + is_list, + is_query, + query_type, + html_type, + dict_type, + sort, + create_by, + create_time + )values( + #{tableId}, + #{columnName}, + #{columnComment}, + #{columnType}, + #{javaType}, + #{javaField}, + #{isPk}, + #{isIncrement}, + #{isRequired}, + #{isInsert}, + #{isEdit}, + #{isList}, + #{isQuery}, + #{queryType}, + #{htmlType}, + #{dictType}, + #{sort}, + #{createBy}, + sysdate() + ) + + + + update gen_table_column + + column_comment = #{columnComment}, + java_type = #{javaType}, + java_field = #{javaField}, + is_insert = #{isInsert}, + is_edit = #{isEdit}, + is_list = #{isList}, + is_query = #{isQuery}, + is_required = #{isRequired}, + query_type = #{queryType}, + html_type = #{htmlType}, + dict_type = #{dictType}, + sort = #{sort}, + update_by = #{updateBy}, + update_time = sysdate() + + where column_id = #{columnId} + + + + delete from gen_table_column where table_id in + + #{tableId} + + + + + delete from gen_table_column where column_id in + + #{item.columnId} + + + + \ No newline at end of file diff --git a/jsowell-generator/src/main/resources/mapper/generator/GenTableMapper.xml b/jsowell-generator/src/main/resources/mapper/generator/GenTableMapper.xml new file mode 100644 index 000000000..bc76c8140 --- /dev/null +++ b/jsowell-generator/src/main/resources/mapper/generator/GenTableMapper.xml @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select + table_id, table_name, table_comment, sub_table_name, sub_table_fk_name, class_name, tpl_category, + package_name, module_name, business_name, function_name, function_author, gen_type, gen_path, options, + create_by, create_time, update_by, update_time, remark + from gen_table + + + + + + + + + + + + + + + + + + insert into gen_table ( + table_name, + table_comment, + class_name, + tpl_category, + package_name, + module_name, + business_name, + function_name, + function_author, + gen_type, + gen_path, + remark, + create_by, + create_time + )values( + #{tableName}, + #{tableComment}, + #{className}, + #{tplCategory}, + #{packageName}, + #{moduleName}, + #{businessName}, + #{functionName}, + #{functionAuthor}, + #{genType}, + #{genPath}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + update gen_table + + table_name = #{tableName}, + table_comment = #{tableComment}, + sub_table_name = #{subTableName}, + sub_table_fk_name = #{subTableFkName}, + class_name = #{className}, + function_author = #{functionAuthor}, + gen_type = #{genType}, + gen_path = #{genPath}, + tpl_category = #{tplCategory}, + package_name = #{packageName}, + module_name = #{moduleName}, + business_name = #{businessName}, + function_name = #{functionName}, + options = #{options}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where table_id = #{tableId} + + + + delete from gen_table where table_id in + + #{tableId} + + + + \ No newline at end of file diff --git a/jsowell-generator/src/main/resources/vm/java/controller.java.vm b/jsowell-generator/src/main/resources/vm/java/controller.java.vm new file mode 100644 index 000000000..2f18870b3 --- /dev/null +++ b/jsowell-generator/src/main/resources/vm/java/controller.java.vm @@ -0,0 +1,115 @@ +package ${packageName}.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.jsowell.common.annotation.Log; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.enums.BusinessType; +import ${packageName}.domain.${ClassName}; +import ${packageName}.service.I${ClassName}Service; +import com.jsowell.common.util.poi.ExcelUtil; +#if($table.crud || $table.sub) +import com.jsowell.common.core.page.TableDataInfo; +#elseif($table.tree) +#end + +/** + * ${functionName}Controller + * + * @author ${author} + * @date ${datetime} + */ +@RestController +@RequestMapping("/${moduleName}/${businessName}") +public class ${ClassName}Controller extends BaseController +{ + @Autowired + private I${ClassName}Service ${className}Service; + + /** + * 查询${functionName}列表 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:list')") + @GetMapping("/list") +#if($table.crud || $table.sub) + public TableDataInfo list(${ClassName} ${className}) + { + startPage(); + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + return getDataTable(list); + } +#elseif($table.tree) + public AjaxResult list(${ClassName} ${className}) + { + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + return AjaxResult.success(list); + } +#end + + /** + * 导出${functionName}列表 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:export')") + @Log(title = "${functionName}", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, ${ClassName} ${className}) + { + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + ExcelUtil<${ClassName}> util = new ExcelUtil<${ClassName}>(${ClassName}.class); + util.exportExcel(response, list, "${functionName}数据"); + } + + /** + * 获取${functionName}详细信息 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:query')") + @GetMapping(value = "/{${pkColumn.javaField}}") + public AjaxResult getInfo(@PathVariable("${pkColumn.javaField}") ${pkColumn.javaType} ${pkColumn.javaField}) + { + return AjaxResult.success(${className}Service.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField})); + } + + /** + * 新增${functionName} + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:add')") + @Log(title = "${functionName}", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody ${ClassName} ${className}) + { + return toAjax(${className}Service.insert${ClassName}(${className})); + } + + /** + * 修改${functionName} + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:edit')") + @Log(title = "${functionName}", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody ${ClassName} ${className}) + { + return toAjax(${className}Service.update${ClassName}(${className})); + } + + /** + * 删除${functionName} + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:remove')") + @Log(title = "${functionName}", businessType = BusinessType.DELETE) + @DeleteMapping("/{${pkColumn.javaField}s}") + public AjaxResult remove(@PathVariable ${pkColumn.javaType}[] ${pkColumn.javaField}s) + { + return toAjax(${className}Service.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s)); + } +} diff --git a/jsowell-generator/src/main/resources/vm/java/domain.java.vm b/jsowell-generator/src/main/resources/vm/java/domain.java.vm new file mode 100644 index 000000000..e27acb01b --- /dev/null +++ b/jsowell-generator/src/main/resources/vm/java/domain.java.vm @@ -0,0 +1,101 @@ +package ${packageName}.domain; + +#foreach ($import in $importList) +import ${import}; +#end +import com.jsowell.common.annotation.Excel; +#if($table.crud || $table.sub) +#elseif($table.tree) +#end + +/** + * ${functionName}对象 ${tableName} + * + * @author ${author} + * @date ${datetime} + */ +#if($table.crud || $table.sub) +#set($Entity="BaseEntity") +#elseif($table.tree) +#set($Entity="TreeEntity") +#end +public class ${ClassName} extends ${Entity} +{ + private static final long serialVersionUID = 1L; + +#foreach ($column in $columns) +#if(!$table.isSuperColumn($column.javaField)) + /** $column.columnComment */ +#if($column.list) +#set($parentheseIndex=$column.columnComment.indexOf("(")) +#if($parentheseIndex != -1) +#set($comment=$column.columnComment.substring(0, $parentheseIndex)) +#else +#set($comment=$column.columnComment) +#end +#if($parentheseIndex != -1) + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") +#elseif($column.javaType == 'Date') + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd") +#else + @Excel(name = "${comment}") +#end +#end + private $column.javaType $column.javaField; + +#end +#end +#if($table.sub) + /** $table.subTable.functionName信息 */ + private List<${subClassName}> ${subclassName}List; + +#end +#foreach ($column in $columns) +#if(!$table.isSuperColumn($column.javaField)) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + public void set${AttrName}($column.javaType $column.javaField) + { + this.$column.javaField = $column.javaField; + } + + public $column.javaType get${AttrName}() + { + return $column.javaField; + } +#end +#end + +#if($table.sub) + public List<${subClassName}> get${subClassName}List() + { + return ${subclassName}List; + } + + public void set${subClassName}List(List<${subClassName}> ${subclassName}List) + { + this.${subclassName}List = ${subclassName}List; + } + +#end + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.JSON_STYLE) +#foreach ($column in $columns) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + .append("${column.javaField}", get${AttrName}()) +#end +#if($table.sub) + .append("${subclassName}List", get${subClassName}List()) +#end + .toString(); + } +} diff --git a/jsowell-generator/src/main/resources/vm/java/mapper.java.vm b/jsowell-generator/src/main/resources/vm/java/mapper.java.vm new file mode 100644 index 000000000..7e7d7c26f --- /dev/null +++ b/jsowell-generator/src/main/resources/vm/java/mapper.java.vm @@ -0,0 +1,91 @@ +package ${packageName}.mapper; + +import java.util.List; +import ${packageName}.domain.${ClassName}; +#if($table.sub) +import ${packageName}.domain.${subClassName}; +#end + +/** + * ${functionName}Mapper接口 + * + * @author ${author} + * @date ${datetime} + */ +public interface ${ClassName}Mapper +{ + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName}集合 + */ + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}); + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int insert${ClassName}(${ClassName} ${className}); + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int update${ClassName}(${ClassName} ${className}); + + /** + * 删除${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的数据主键集合 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); +#if($table.sub) + + /** + * 批量删除${subTable.functionName} + * + * @param ${pkColumn.javaField}s 需要删除的数据主键集合 + * @return 结果 + */ + public int delete${subClassName}By${subTableFkClassName}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); + + /** + * 批量新增${subTable.functionName} + * + * @param ${subclassName}List ${subTable.functionName}列表 + * @return 结果 + */ + public int batch${subClassName}(List<${subClassName}> ${subclassName}List); + + + /** + * 通过${functionName}主键删除${subTable.functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}ID + * @return 结果 + */ + public int delete${subClassName}By${subTableFkClassName}(${pkColumn.javaType} ${pkColumn.javaField}); +#end +} diff --git a/jsowell-generator/src/main/resources/vm/java/service.java.vm b/jsowell-generator/src/main/resources/vm/java/service.java.vm new file mode 100644 index 000000000..264882b27 --- /dev/null +++ b/jsowell-generator/src/main/resources/vm/java/service.java.vm @@ -0,0 +1,61 @@ +package ${packageName}.service; + +import java.util.List; +import ${packageName}.domain.${ClassName}; + +/** + * ${functionName}Service接口 + * + * @author ${author} + * @date ${datetime} + */ +public interface I${ClassName}Service +{ + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName}集合 + */ + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}); + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int insert${ClassName}(${ClassName} ${className}); + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int update${ClassName}(${ClassName} ${className}); + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的${functionName}主键集合 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); + + /** + * 删除${functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); +} diff --git a/jsowell-generator/src/main/resources/vm/java/serviceImpl.java.vm b/jsowell-generator/src/main/resources/vm/java/serviceImpl.java.vm new file mode 100644 index 000000000..6976ded59 --- /dev/null +++ b/jsowell-generator/src/main/resources/vm/java/serviceImpl.java.vm @@ -0,0 +1,169 @@ +package ${packageName}.service.impl; + +import java.util.List; +#foreach ($column in $columns) +#if($column.javaField == 'createTime' || $column.javaField == 'updateTime') +import com.jsowell.common.util.DateUtils; +#break +#end +#end +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +#if($table.sub) +import java.util.ArrayList; +import com.jsowell.common.util.StringUtils; +import org.springframework.transaction.annotation.Transactional; +import ${packageName}.domain.${subClassName}; +#end +import ${packageName}.mapper.${ClassName}Mapper; +import ${packageName}.domain.${ClassName}; +import ${packageName}.service.I${ClassName}Service; + +/** + * ${functionName}Service业务层处理 + * + * @author ${author} + * @date ${datetime} + */ +@Service +public class ${ClassName}ServiceImpl implements I${ClassName}Service +{ + @Autowired + private ${ClassName}Mapper ${className}Mapper; + + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + @Override + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}) + { + return ${className}Mapper.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField}); + } + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName} + */ + @Override + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}) + { + return ${className}Mapper.select${ClassName}List(${className}); + } + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int insert${ClassName}(${ClassName} ${className}) + { +#foreach ($column in $columns) +#if($column.javaField == 'createTime') + ${className}.setCreateTime(DateUtils.getNowDate()); +#end +#end +#if($table.sub) + int rows = ${className}Mapper.insert${ClassName}(${className}); + insert${subClassName}(${className}); + return rows; +#else + return ${className}Mapper.insert${ClassName}(${className}); +#end + } + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int update${ClassName}(${ClassName} ${className}) + { +#foreach ($column in $columns) +#if($column.javaField == 'updateTime') + ${className}.setUpdateTime(DateUtils.getNowDate()); +#end +#end +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}(${className}.get${pkColumn.capJavaField}()); + insert${subClassName}(${className}); +#end + return ${className}Mapper.update${ClassName}(${className}); + } + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的${functionName}主键 + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s) + { +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}s(${pkColumn.javaField}s); +#end + return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s); + } + + /** + * 删除${functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}) + { +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}(${pkColumn.javaField}); +#end + return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField}); + } +#if($table.sub) + + /** + * 新增${subTable.functionName}信息 + * + * @param ${className} ${functionName}对象 + */ + public void insert${subClassName}(${ClassName} ${className}) + { + List<${subClassName}> ${subclassName}List = ${className}.get${subClassName}List(); + ${pkColumn.javaType} ${pkColumn.javaField} = ${className}.get${pkColumn.capJavaField}(); + if (StringUtils.isNotNull(${subclassName}List)) + { + List<${subClassName}> list = new ArrayList<${subClassName}>(); + for (${subClassName} ${subclassName} : ${subclassName}List) + { + ${subclassName}.set${subTableFkClassName}(${pkColumn.javaField}); + list.add(${subclassName}); + } + if (list.size() > 0) + { + ${className}Mapper.batch${subClassName}(list); + } + } + } +#end +} diff --git a/jsowell-generator/src/main/resources/vm/java/sub-domain.java.vm b/jsowell-generator/src/main/resources/vm/java/sub-domain.java.vm new file mode 100644 index 000000000..563124bdb --- /dev/null +++ b/jsowell-generator/src/main/resources/vm/java/sub-domain.java.vm @@ -0,0 +1,73 @@ +package ${packageName}.domain; + +#foreach ($import in $subImportList) +import ${import}; +#end +import com.jsowell.common.annotation.Excel; + +/** + * ${subTable.functionName}对象 ${subTableName} + * + * @author ${author} + * @date ${datetime} + */ +public class ${subClassName} extends BaseEntity +{ + private static final long serialVersionUID = 1L; + +#foreach ($column in $subTable.columns) +#if(!$table.isSuperColumn($column.javaField)) + /** $column.columnComment */ +#if($column.list) +#set($parentheseIndex=$column.columnComment.indexOf("(")) +#if($parentheseIndex != -1) +#set($comment=$column.columnComment.substring(0, $parentheseIndex)) +#else +#set($comment=$column.columnComment) +#end +#if($parentheseIndex != -1) + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") +#elseif($column.javaType == 'Date') + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd") +#else + @Excel(name = "${comment}") +#end +#end + private $column.javaType $column.javaField; + +#end +#end +#foreach ($column in $subTable.columns) +#if(!$table.isSuperColumn($column.javaField)) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + public void set${AttrName}($column.javaType $column.javaField) + { + this.$column.javaField = $column.javaField; + } + + public $column.javaType get${AttrName}() + { + return $column.javaField; + } +#end +#end + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.JSON_STYLE) +#foreach ($column in $subTable.columns) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + .append("${column.javaField}", get${AttrName}()) +#end + .toString(); + } +} diff --git a/jsowell-generator/src/main/resources/vm/js/api.js.vm b/jsowell-generator/src/main/resources/vm/js/api.js.vm new file mode 100644 index 000000000..9295524a4 --- /dev/null +++ b/jsowell-generator/src/main/resources/vm/js/api.js.vm @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询${functionName}列表 +export function list${BusinessName}(query) { + return request({ + url: '/${moduleName}/${businessName}/list', + method: 'get', + params: query + }) +} + +// 查询${functionName}详细 +export function get${BusinessName}(${pkColumn.javaField}) { + return request({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'get' + }) +} + +// 新增${functionName} +export function add${BusinessName}(data) { + return request({ + url: '/${moduleName}/${businessName}', + method: 'post', + data: data + }) +} + +// 修改${functionName} +export function update${BusinessName}(data) { + return request({ + url: '/${moduleName}/${businessName}', + method: 'put', + data: data + }) +} + +// 删除${functionName} +export function del${BusinessName}(${pkColumn.javaField}) { + return request({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'delete' + }) +} diff --git a/jsowell-generator/src/main/resources/vm/sql/sql.vm b/jsowell-generator/src/main/resources/vm/sql/sql.vm new file mode 100644 index 000000000..0b54dcae6 --- /dev/null +++ b/jsowell-generator/src/main/resources/vm/sql/sql.vm @@ -0,0 +1,22 @@ +-- 菜单 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}', '${parentMenuId}', '1', '${businessName}', '${moduleName}/${businessName}/index', 1, 0, 'C', '0', '0', '${permissionPrefix}:list', '#', 'thinkgem', sysdate(), '', null, '${functionName}菜单'); + +-- 按钮父菜单ID +SELECT @parentId := LAST_INSERT_ID(); + +-- 按钮 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}查询', @parentId, '1', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:query', '#', 'thinkgem', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}新增', @parentId, '2', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:add', '#', 'thinkgem', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}修改', @parentId, '3', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:edit', '#', 'thinkgem', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}删除', @parentId, '4', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:remove', '#', 'thinkgem', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}导出', @parentId, '5', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:export', '#', 'thinkgem', sysdate(), '', null, ''); \ No newline at end of file diff --git a/jsowell-generator/src/main/resources/vm/vue/index-tree.vue.vm b/jsowell-generator/src/main/resources/vm/vue/index-tree.vue.vm new file mode 100644 index 000000000..3def70454 --- /dev/null +++ b/jsowell-generator/src/main/resources/vm/vue/index-tree.vue.vm @@ -0,0 +1,502 @@ + + + diff --git a/jsowell-generator/src/main/resources/vm/vue/index.vue.vm b/jsowell-generator/src/main/resources/vm/vue/index.vue.vm new file mode 100644 index 000000000..e9a1fae13 --- /dev/null +++ b/jsowell-generator/src/main/resources/vm/vue/index.vue.vm @@ -0,0 +1,598 @@ + + + diff --git a/jsowell-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm b/jsowell-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm new file mode 100644 index 000000000..862297c79 --- /dev/null +++ b/jsowell-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm @@ -0,0 +1,486 @@ + + + diff --git a/jsowell-generator/src/main/resources/vm/vue/v3/index.vue.vm b/jsowell-generator/src/main/resources/vm/vue/v3/index.vue.vm new file mode 100644 index 000000000..f66cc3b89 --- /dev/null +++ b/jsowell-generator/src/main/resources/vm/vue/v3/index.vue.vm @@ -0,0 +1,596 @@ + + + diff --git a/jsowell-generator/src/main/resources/vm/xml/mapper.xml.vm b/jsowell-generator/src/main/resources/vm/xml/mapper.xml.vm new file mode 100644 index 000000000..0ceb3d859 --- /dev/null +++ b/jsowell-generator/src/main/resources/vm/xml/mapper.xml.vm @@ -0,0 +1,135 @@ + + + + + +#foreach ($column in $columns) + +#end + +#if($table.sub) + + + + + + +#foreach ($column in $subTable.columns) + +#end + +#end + + + select#foreach($column in $columns) $column.columnName#if($foreach.count != $columns.size()),#end#end from ${tableName} + + + + + + + + insert into ${tableName} + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment) + $column.columnName, +#end +#end + + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment) + #{$column.javaField}, +#end +#end + + + + + update ${tableName} + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName) + $column.columnName = #{$column.javaField}, +#end +#end + + where ${pkColumn.columnName} = #{${pkColumn.javaField}} + + + + delete from ${tableName} where ${pkColumn.columnName} = #{${pkColumn.javaField}} + + + + delete from ${tableName} where ${pkColumn.columnName} in + + #{${pkColumn.javaField}} + + +#if($table.sub) + + + delete from ${subTableName} where ${subTableFkName} in + + #{${subTableFkclassName}} + + + + + delete from ${subTableName} where ${subTableFkName} = #{${subTableFkclassName}} + + + + insert into ${subTableName}(#foreach($column in $subTable.columns) $column.columnName#if($foreach.count != $subTable.columns.size()),#end#end) values + + (#foreach($column in $subTable.columns) #{item.$column.javaField}#if($foreach.count != $subTable.columns.size()),#end#end) + + +#end + \ No newline at end of file diff --git a/jsowell-netty/pom.xml b/jsowell-netty/pom.xml new file mode 100644 index 000000000..b174b8f71 --- /dev/null +++ b/jsowell-netty/pom.xml @@ -0,0 +1,46 @@ + + + + jsowell-charger-web + com.jsowell + 1.0.0 + + 4.0.0 + + jsowell-netty + + + + org.projectlombok + lombok + + + com.jsowell + jsowell-pile + 1.0.0 + + + + + 8 + 8 + + + + + + org.apache.maven.plugins + maven-war-plugin + 3.1.0 + + false + ${project.artifactId} + + + + + + + \ No newline at end of file diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/client/NettyClient.java b/jsowell-netty/src/main/java/com/jsowell/netty/client/NettyClient.java new file mode 100644 index 000000000..6f2b4b593 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/client/NettyClient.java @@ -0,0 +1,109 @@ +package com.jsowell.netty.client; + +import com.jsowell.common.constant.Constants; +import com.jsowell.common.util.DateUtils; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.string.StringDecoder; +import io.netty.handler.codec.string.StringEncoder; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Data +public class NettyClient implements Runnable { + + static final String HOST = System.getProperty("host", Constants.SOCKET_IP); + static final int PORT = Integer.parseInt(System.getProperty("port", "9011")); + static final int SIZE = Integer.parseInt(System.getProperty("size", "256")); + + private String content; + + public NettyClient(String content) { + this.content = content; + } + + @Override + public void run() { + // Configure the client. + EventLoopGroup group = new NioEventLoopGroup(); + try { + int num = 0; + boolean boo = true; + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .option(ChannelOption.TCP_NODELAY, true) + .handler(new NettyClientChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + p.addLast("decoder", new StringDecoder()); + p.addLast("encoder", new StringEncoder()); + p.addLast(new NettyClientHandler()); + } + }); + ChannelFuture future = b.connect(HOST, PORT).sync(); + while (boo) { + num++; + future.channel().writeAndFlush("发送数据=======" + content + "--" + DateUtils.getTime()); + try { //休眠一段时间 + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + //每一条线程向服务端发送的次数 + if (num == 5) { + boo = false; + } + } + log.info(content + "-----------------------------" + num); + //future.channel().closeFuture().sync(); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + group.shutdownGracefully(); + } + } + + /** + * 下面是不加线程的 + */ + /*public static void main(String[] args) throws Exception { + + sendMessage("hhh你好?"); + } + + public static void sendMessage(String content) throws InterruptedException { + // Configure the client. + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .option(ChannelOption.TCP_NODELAY, true) + .handler(new NettyClientChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + p.addLast("decoder", new StringDecoder()); + p.addLast("encoder", new StringEncoder()); + p.addLast(new NettyClientHandler()); + } + }); + + ChannelFuture future = b.connect(HOST, PORT).sync(); + future.channel().writeAndFlush(content); + future.channel().closeFuture().sync(); + } finally { + group.shutdownGracefully(); + } + }*/ + +} \ No newline at end of file diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/client/NettyClientChannelInitializer.java b/jsowell-netty/src/main/java/com/jsowell/netty/client/NettyClientChannelInitializer.java new file mode 100644 index 000000000..d5b61527d --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/client/NettyClientChannelInitializer.java @@ -0,0 +1,23 @@ +package com.jsowell.netty.client; + +import com.jsowell.netty.server.yunkuaichong.NettyServerHandler; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.string.StringDecoder; +import io.netty.handler.codec.string.StringEncoder; +import io.netty.util.CharsetUtil; + +/** + * 客户端初始化,客户端与服务器端连接一旦创建,这个类中方法就会被回调,设置出站编码器和入站解码器,客户端服务端编解码要一致 + */ +public class NettyClientChannelInitializer extends ChannelInitializer { + + @Override + protected void initChannel(SocketChannel channel) throws Exception { + + channel.pipeline().addLast("decoder",new StringDecoder(CharsetUtil.UTF_8)); + channel.pipeline().addLast("encoder",new StringEncoder(CharsetUtil.UTF_8)); + + channel.pipeline().addLast(new NettyServerHandler()); + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/client/NettyClientHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/client/NettyClientHandler.java new file mode 100644 index 000000000..41c1f896f --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/client/NettyClientHandler.java @@ -0,0 +1,71 @@ +package com.jsowell.netty.client; + +import com.jsowell.common.util.DateUtils; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelId; +import io.netty.channel.ChannelInboundHandlerAdapter; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * 客户端处理类 + */ +@Slf4j +public class NettyClientHandler extends ChannelInboundHandlerAdapter { + + /** + * 计算有多少客户端接入,第一个string为客户端ip + */ + private static final ConcurrentHashMap CLIENT_MAP = new ConcurrentHashMap<>(); + + @Override + public void channelActive(ChannelHandlerContext ctx) { + CLIENT_MAP.put(ctx.channel().id(), ctx); + log.info("ClientHandler Active"); + } + + /** + * @param ctx + * @author xiongchuan on 2019/4/28 16:10 + * @DESCRIPTION: 有服务端端终止连接服务器会触发此函数 + * @return: void + */ + @Override + public void channelInactive(ChannelHandlerContext ctx) { + ctx.close(); + log.info("服务端终止了服务"); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + log.info("回写数据:" + msg); + } + + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + //cause.printStackTrace(); + log.info("服务端发生异常【" + cause.getMessage() + "】"); + ctx.close(); + } + + /** + * @param msg 需要发送的消息内容 + * @param channelId 连接通道唯一id + * @author xiongchuan on 2019/4/28 16:10 + * @DESCRIPTION: 客户端给服务端发送消息 + * @return: void + */ + public void channelWrite(ChannelId channelId, String msg) { + ChannelHandlerContext ctx = CLIENT_MAP.get(channelId); + if (ctx == null) { + log.info("通道【" + channelId + "】不存在"); + return; + } + //将客户端的信息直接返回写入ctx + ctx.write(msg + " 时间:" + DateUtils.getTime()); + //刷新缓存区 + ctx.flush(); + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/client/TestNettyClient.java b/jsowell-netty/src/main/java/com/jsowell/netty/client/TestNettyClient.java new file mode 100644 index 000000000..34d4d77c9 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/client/TestNettyClient.java @@ -0,0 +1,14 @@ +package com.jsowell.netty.client; + +/** + * 模拟多客户端发送报文 + */ +public class TestNettyClient { + + public static void main(String[] args) { + //开启10条线程,每条线程就相当于一个客户端 + for (int i = 1; i <= 300; i++) { + new Thread(new NettyClient("thread" + "--" + i)).start(); + } + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/GetRealTimeMonitorDataCommand.java b/jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/GetRealTimeMonitorDataCommand.java new file mode 100644 index 000000000..00e625aca --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/GetRealTimeMonitorDataCommand.java @@ -0,0 +1,18 @@ +package com.jsowell.netty.command.ykc; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 读取实时监测数据命令 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class GetRealTimeMonitorDataCommand { + private String pileSn; + private String connectorCode; +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/IssueQRCodeCommand.java b/jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/IssueQRCodeCommand.java new file mode 100644 index 000000000..757195056 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/IssueQRCodeCommand.java @@ -0,0 +1,14 @@ +package com.jsowell.netty.command.ykc; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class IssueQRCodeCommand { + String pileSn; +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/ProofreadTimeCommand.java b/jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/ProofreadTimeCommand.java new file mode 100644 index 000000000..95508b6df --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/ProofreadTimeCommand.java @@ -0,0 +1,17 @@ +package com.jsowell.netty.command.ykc; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 对时命令 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ProofreadTimeCommand { + private String pileSn; +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/PublishPileBillingTemplateCommand.java b/jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/PublishPileBillingTemplateCommand.java new file mode 100644 index 000000000..2f0c03861 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/PublishPileBillingTemplateCommand.java @@ -0,0 +1,17 @@ +package com.jsowell.netty.command.ykc; + +import com.jsowell.pile.vo.web.BillingTemplateVO; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PublishPileBillingTemplateCommand { + private String pileSn; + + private BillingTemplateVO billingTemplateVO; +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/RebootCommand.java b/jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/RebootCommand.java new file mode 100644 index 000000000..17b1daa07 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/RebootCommand.java @@ -0,0 +1,14 @@ +package com.jsowell.netty.command.ykc; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class RebootCommand { + private String pileSn; +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/StartChargingCommand.java b/jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/StartChargingCommand.java new file mode 100644 index 000000000..891ac7525 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/StartChargingCommand.java @@ -0,0 +1,48 @@ +package com.jsowell.netty.command.ykc; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +/** + * 启动充电指令 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class StartChargingCommand { + /** + * 交易流水号 + */ + private String orderCode; + + /** + * 充电桩编号 + */ + private String pileSn; + + /** + * 枪口号 + */ + private String connectorCode; + + /** + * 充电金额 + */ + private BigDecimal chargeAmount; + + /** + * 逻辑卡号 没有就传0 + */ + private String logicCardNum; + + /** + * 物理卡号 没有就传0 + */ + private String physicsCardNum; + +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/StopChargingCommand.java b/jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/StopChargingCommand.java new file mode 100644 index 000000000..65683aee1 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/StopChargingCommand.java @@ -0,0 +1,18 @@ +package com.jsowell.netty.command.ykc; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 停止充电指令 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class StopChargingCommand { + private String pileSn; + private String connectorCode; +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/UpdateFileCommand.java b/jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/UpdateFileCommand.java new file mode 100644 index 000000000..9d1155ec9 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/command/ykc/UpdateFileCommand.java @@ -0,0 +1,16 @@ +package com.jsowell.netty.command.ykc; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class UpdateFileCommand { + List pileSnList; +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/decoder/CustomDecoder.java b/jsowell-netty/src/main/java/com/jsowell/netty/decoder/CustomDecoder.java new file mode 100644 index 000000000..3ea0044c8 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/decoder/CustomDecoder.java @@ -0,0 +1,40 @@ +package com.jsowell.netty.decoder; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.CorruptedFrameException; + +import java.util.List; + +public class CustomDecoder extends ByteToMessageDecoder { + private static final byte START_FLAG = (byte) 0x68; + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + // 检查可读数据长度是否大于等于起始标志符和数据长度字段的长度 + if (in.readableBytes() >= 3) { + // 标记当前读取位置 + in.markReaderIndex(); + // 读取起始标志符 + byte startFlag = in.readByte(); + // 检查起始标志符是否正确 + if (startFlag != START_FLAG) { + // 如果不正确,重置读取位置,并抛出异常 + in.resetReaderIndex(); + throw new CorruptedFrameException("Invalid start flag: " + startFlag); + } + // 读取数据长度 + byte length = in.readByte(); + // 检查可读数据长度是否大于等于数据长度字段的值 + if (in.readableBytes() >= length) { + // 读取完整数据包 + ByteBuf frame = in.readBytes(length + 2); // 包括校验位 + out.add(frame); + } else { + // 如果可读数据长度不够,重置读取位置,并等待下一次读取 + in.resetReaderIndex(); + } + } + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/decoder/StartAndLengthFieldFrameDecoder.java b/jsowell-netty/src/main/java/com/jsowell/netty/decoder/StartAndLengthFieldFrameDecoder.java new file mode 100644 index 000000000..b38288860 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/decoder/StartAndLengthFieldFrameDecoder.java @@ -0,0 +1,85 @@ +package com.jsowell.netty.decoder; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +public class StartAndLengthFieldFrameDecoder extends ByteToMessageDecoder { + // 起始标志 + private int HEAD_DATA; + + public StartAndLengthFieldFrameDecoder(int HEAD_DATA) { + this.HEAD_DATA = HEAD_DATA; + } + + /** + *
+	 * 协议开始的标准head_data,int类型,占据1个字节.
+	 * 表示数据的长度contentLength,int类型,占据1个字节.
+	 * 
+ */ + public final int BASE_LENGTH = 1 + 1; + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, + List out) throws Exception { + System.out.println(); + // log.info("打印ByteBuf:{}", buffer.toString()); + // 可读长度必须大于基本长度 + if (buffer.readableBytes() <= BASE_LENGTH) { + log.warn("可读字节数:{}小于基础长度:{}", buffer.readableBytes(), BASE_LENGTH); + return; + } + + // 记录包头开始的index + int beginReader; + + while (true) { + // 获取包头开始的index + beginReader = buffer.readerIndex(); + // log.info("包头开始的index:{}", beginReader); + // 标记包头开始的index + buffer.markReaderIndex(); + // 读到了协议的开始标志,结束while循环 + if (buffer.getUnsignedByte(beginReader) == HEAD_DATA) { + // log.info("读到了协议的开始标志,结束while循环 byte:{}, HEAD_DATA:{}", buffer.getUnsignedByte(beginReader), HEAD_DATA); + break; + } + + // 未读到包头,略过一个字节 + // 每次略过,一个字节,去读取,包头信息的开始标记 + buffer.resetReaderIndex(); + buffer.readByte(); + + // 当略过,一个字节之后, + // 数据包的长度,又变得不满足 + // 此时,应该结束。等待后面的数据到达 + if (buffer.readableBytes() < BASE_LENGTH) { + log.info("数据包的长度不满足 readableBytes:{}, BASE_LENGTH:{}", buffer.readableBytes(), BASE_LENGTH); + return; + } + } + + // 消息的长度 + int length = buffer.getUnsignedByte(beginReader + 1); + // 判断请求数据包数据是否到齐 + if (buffer.readableBytes() < length + 4) { + // log.info("请求数据包数据没有到齐,还原读指针 readableBytes:{}, 消息的长度:{}", buffer.readableBytes(), length); + // 还原读指针 + buffer.readerIndex(beginReader); + return; + } + + // 读取data数据 + byte[] data = new byte[length + 4]; + buffer.readBytes(data); + ByteBuf frame = buffer.retainedSlice(beginReader, length + 4); + buffer.readerIndex(beginReader + length + 4); + out.add(frame); + } + +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/factory/YKCOperateFactory.java b/jsowell-netty/src/main/java/com/jsowell/netty/factory/YKCOperateFactory.java new file mode 100644 index 000000000..a0edbfcdf --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/factory/YKCOperateFactory.java @@ -0,0 +1,38 @@ +package com.jsowell.netty.factory; + +import com.google.common.collect.Maps; +import com.jsowell.common.util.StringUtils; +import com.jsowell.netty.handler.AbstractHandler; + +import java.util.Map; +import java.util.Objects; + +/** + * 工厂设计模式 + * 云快充操作 + */ +public class YKCOperateFactory { + + private static Map strategyMap = Maps.newHashMap(); + + /** + * 注册 + * @param str + * @param handler + */ + public static void register(String str, AbstractHandler handler) { + if (StringUtils.isBlank(str) || Objects.isNull(handler)) { + return; + } + strategyMap.put(str, handler); + } + + /** + * 获取 + * @param name + * @return + */ + public static AbstractHandler getInvokeStrategy(String name) { + return strategyMap.get(name); + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/AbstractHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/AbstractHandler.java new file mode 100644 index 000000000..c8aa229db --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/AbstractHandler.java @@ -0,0 +1,73 @@ +package com.jsowell.netty.handler; + +import com.google.common.primitives.Bytes; +import com.jsowell.common.constant.CacheConstants; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.core.redis.RedisCache; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.CRC16Util; +import com.jsowell.common.util.DateUtils; +import io.netty.channel.Channel; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * 模板方法模式 + */ +public abstract class AbstractHandler implements InitializingBean { + + @Autowired + private RedisCache redisCache; + + /** + * 执行逻辑 + * 有应答 + */ + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + throw new UnsupportedOperationException(); + } + + /** + * 执行逻辑 + * 不需要应答 + */ + // public void pushProcess() { + // throw new UnsupportedOperationException(); + // } + + /** + * 组装应答的结果 + * @param ykcDataProtocol 请求数据 + * @param messageBody 消息体 + * @return 应答结果 + */ + protected byte[] getResult(YKCDataProtocol ykcDataProtocol, byte[] messageBody) { + // 起始标志 + byte[] head = ykcDataProtocol.getHead(); + // 序列号域 + byte[] serialNumber = ykcDataProtocol.getSerialNumber(); + // 加密标志 + byte[] encryptFlag = ykcDataProtocol.getEncryptFlag(); + // 请求帧类型 + byte[] requestFrameType = ykcDataProtocol.getFrameType(); + // 应答帧类型 + byte[] responseFrameType = YKCFrameTypeCode.ResponseRelation.getResponseFrameType(requestFrameType); + + // 数据域 值为“序列号域+加密标志+帧类型标志+消息体”字节数之和 + byte[] dataFields = Bytes.concat(serialNumber, encryptFlag, responseFrameType, messageBody); + // 计算crc: 从序列号域到数据域的 CRC 校验 + int crc16 = CRC16Util.calcCrc16(dataFields); + return Bytes.concat(head, BytesUtil.intToBytes(dataFields.length, 1), dataFields, BytesUtil.intToBytes(crc16)); + } + + /** + * 保存桩最后链接到平台的时间 + * @param pileSn 桩编号 + */ + protected void saveLastTime(String pileSn) { + String redisKey = CacheConstants.PILE_LAST_CONNECTION + pileSn; + redisCache.setCacheObject(redisKey, DateUtils.getTime(), 60 * 60 * 24); + } + +} \ No newline at end of file diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/BMSAbortDuringChargingPhaseHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/BMSAbortDuringChargingPhaseHandler.java new file mode 100644 index 000000000..71a2dfd8a --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/BMSAbortDuringChargingPhaseHandler.java @@ -0,0 +1,96 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 充电阶段 BMS 中止Handler + * + * GBT-27930 充电桩与 BMS 充电阶段 BMS 中止报文 + * @author JS-ZZA + * @date 2022/9/19 13:32 + */ +@Slf4j +@Component +public class BMSAbortDuringChargingPhaseHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.BMS_ABORT_DURING_CHARGING_PHASE_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===充电阶段 BMS 中止===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 获取消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + int startIndex = 0; + int length = 16; + + // 交易流水号 + byte[] serialNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String orderCode = BytesUtil.bcd2Str(serialNumByteArr); + + // 桩编码 + startIndex += length; + length = 7; + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.bcd2Str(pileSnByteArr); + + // 保存时间 + saveLastTime(pileSn); + + // 枪号 + startIndex += length; + length = 1; + byte[] pileConnectorNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String connectorCode = BytesUtil.bcd2Str(pileConnectorNumByteArr); + + /** + * BMS 中止充电原因 + * 1-2 位——所需求的 SOC 目标值 + * 3-4 位——达到总电压的设定值 + * 5-6 位——达到单体电压设定值 + * 7-8 位——充电机主动中止 + */ + startIndex += length; + byte[] BMSStopReasonByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + + /** + * BMS 中止充电故障原因 + * 1-2 位——绝缘故障 + * 3-4 位——输出连接器过温故障 + * 5-6 位——BMS 元件、输出连接 器过温 + * 7-8 位——充电连接器故障 + * 9-10 位——电池组温度过高故障 + * 11-12 位——高压继电器故障 + * 13-14 位——检测点 2 电压检测故障 + * 15-16 位——其他故障 + */ + startIndex += length; + length = 2; + byte[] BMSStopFaultReasonByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + /** + * BMS 中止充电错误原因 + * 1-2 位——电流过大 + * 3-4 位——电压异常 + * 5-8 位——预留位 + */ + startIndex += length; + byte[] BMSStopErrorReasonByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/BMSDemandAndChargerOutputHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/BMSDemandAndChargerOutputHandler.java new file mode 100644 index 000000000..9ce2e1b3e --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/BMSDemandAndChargerOutputHandler.java @@ -0,0 +1,108 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 充电过程 BMS 需求与充电机输出 0x23 + * + * GBT-27930 充电桩与 BMS 充电过程 BMS 需求、充电机输出 + * @author JS-ZZA + * @date 2022/9/19 13:51 + */ +@Slf4j +@Component +public class BMSDemandAndChargerOutputHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.CHARGING_PROCESS_BMS_DEMAND_AND_CHARGER_OUTPUT_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===充电过程 BMS 需求与充电机输出===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 获取消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + int startIndex = 0; + int length = 16; + + // 交易流水号 + byte[] serialNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 桩编码 + startIndex += length; + length = 7; + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.bcd2Str(pileSnByteArr); + + // 保存时间 + saveLastTime(pileSn); + + // 枪号 + startIndex += length; + length = 1; + byte[] pileConnectorNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 电压需求 0.1 V/位, 0 V 偏移量 + startIndex += length; + length = 2; + byte[] bmsVoltageDemandByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 电流需求 0.1 A/位, -400 A 偏移量 + startIndex += length; + byte[] bmsCurrentDemandByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 充电模式 0x01:恒压充电; 0x02:恒流充电 + startIndex += length; + length = 1; + byte[] bmsChargingModelByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 充电电压测量值 0.1 V/位, 0 V 偏移量 + startIndex += length; + length = 2; + byte[] bmsChargingVoltageByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 充电电流测量值 0.1 A/位, -400 A 偏移量 + startIndex += length; + byte[] bmsChargingCurrentByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 最高单体动力蓄电池电压及组号 1-12 位:最高单体动力蓄电池电压, 数据分辨率: 0.01 V/位, 0 V 偏移量;数据范围: 0~24 V; 13-16 位: 最高单体动力蓄电池电 压所在组号,数据分辨率: 1/位, 0 偏移量;数据范围: 0~15 + startIndex += length; + byte[] bmsMaxVoltageAndGroupNum = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 当前荷电状态 SOC( %) 1%/位, 0%偏移量; 数据范围: 0~100% + startIndex += length; + length = 1; + byte[] socByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 估算剩余充电时间 1 min/位, 0 min 偏移量; 数据范围: 0~600 min + startIndex += length; + length = 2; + byte[] bmsTheRestChargingTimeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 电桩电压输出值 0.1 V/位, 0 V 偏移量 + startIndex += length; + byte[] pileVoltageOutputByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 电桩电流输出值 0.1 A/位, -400 A 偏移量 + startIndex += length; + byte[] pileCurrentOutputByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 累计充电时间 1 min/位, 0 min 偏移量; 数据范围: 0~600 min + startIndex += length; + byte[] chargingTimeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/BMSInformationHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/BMSInformationHandler.java new file mode 100644 index 000000000..6182d838f --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/BMSInformationHandler.java @@ -0,0 +1,99 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 充电过程 BMS 信息 0x25 + * + * GBT-27930 充电桩与 BMS 充电过程 BMS 信息 + * @author JS-ZZA + * @date 2022/9/19 13:53 + */ +@Slf4j +@Component +public class BMSInformationHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.CHARGING_PROCESS_BMS_INFORMATION_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===充电过程 BMS 信息===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 获取消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + int startIndex = 0; + int length = 16; + + // 交易流水号 + byte[] serialNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 桩编码 + startIndex += length; + length = 7; + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.bcd2Str(pileSnByteArr); + + // 保存时间 + saveLastTime(pileSn); + + // 枪号 + startIndex += length; + length = 1; + byte[] pileConnectorNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 最高单体动力蓄电池电压所在编号 1/位, 1 偏移量; 数据范围: 1~256 + startIndex += length; + byte[] BMSMaxVoltageNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 最高动力蓄电池温度 1ºC/位, -50 ºC 偏移量;数据范 围: -50 ºC ~+200 ºC + startIndex += length; + byte[] BMSMaxBatteryTemperatureByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 最高温度检测点编号 1/位, 1 偏移量; 数据范围: 1~128 + startIndex += length; + byte[] maxTemperatureDetectionNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 最低动力蓄电池温度 1ºC/位, -50 ºC 偏移量;数据范 围: -50 ºC ~+200 ºC + startIndex += length; + byte[] minBatteryTemperature = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 最低动力蓄电池温度检测点编号 1/位, 1 偏移量; 数据范围: 1~128 + startIndex += length; + byte[] minTemperatureDetectionNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + /** + * 9-12 + * 9: BMS 单体动力蓄电池电压过高 /过低 (<00> :=正常 ; <01> :=过高 ; <10>: =过低) + * 10: BMS 整车动力蓄电池荷电状态 SOC 过高/过低 (<00> :=正常 ; <01> :=过高 ; <10>: =过低) + * 11: BMS 动力蓄电池充电过电流 (<00> :=正常 ; <01> :=过流 ; <10>: =不可信状态) + * 12: BMS 动力蓄电池温度过高 (<00> :=正常 ; <01> :=过流 ; <10>: =不可信状态) + */ + startIndex += length; + byte[] numNineToTwelve = BytesUtil.copyBytes(msgBody, startIndex, length); + + /** + * 13-16 + * 13: BMS 动力蓄电池绝缘状态 (<00> :=正常 ; <01> :=过流 ; <10>: =不可信状态) + * 14: BMS 动力蓄电池组输出连接器连接状态 (<00> :=正常 ; <01> :=过流 ; <10>: =不可信状态) + * 15: 充电禁止 (<00>: =禁止; <01>: =允许) + * 16: 预留位 00 + */ + startIndex += length; + byte[] numThirteenToSixteen = BytesUtil.copyBytes(msgBody, startIndex, length); + + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/BillingTemplateRequestHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/BillingTemplateRequestHandler.java new file mode 100644 index 000000000..13b87e24e --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/BillingTemplateRequestHandler.java @@ -0,0 +1,78 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.command.ykc.IssueQRCodeCommand; +import com.jsowell.netty.factory.YKCOperateFactory; +import com.jsowell.netty.service.yunkuaichong.YKCPushCommandService; +import com.jsowell.pile.service.IPileBillingTemplateService; +import com.jsowell.pile.vo.web.BillingTemplateVO; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.concurrent.CompletableFuture; + +/** + * 计费模板请求 Handler + * + * @author JS-ZZA + * @date 2022/9/17 15:59 + */ +@Slf4j +@Component +public class BillingTemplateRequestHandler extends AbstractHandler{ + + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.BILLING_TEMPLATE_CODE.getBytes()); + + @Autowired + private IPileBillingTemplateService pileBillingTemplateService; + + @Autowired + private YKCPushCommandService ykcPushCommandService; + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===执行计费模板请求逻辑===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 获取消息体(此请求消息体只有桩编码) + byte[] pileSnByte = ykcDataProtocol.getMsgBody(); + String pileSn = BytesUtil.binary(pileSnByte, 16); + // log.info("桩号:{}", pileSn); + + // 保存时间 + saveLastTime(pileSn); + + // 根据桩号查询计费模板 + BillingTemplateVO billingTemplateVO = pileBillingTemplateService.selectBillingTemplateDetailByPileSn(pileSn); + if (billingTemplateVO == null) { + log.warn("根据桩号:{},查询计费模板为null", pileSn); + return null; + } + + // log.info("下面进行下发二维码 pileSn:{}, thread:{}", pileSn, Thread.currentThread().getName()); + // CompletableFuture.runAsync(() -> { + // try { + // Thread.sleep(200); + // } catch (InterruptedException e) { + // e.printStackTrace(); + // } + // // 下发二维码 + // IssueQRCodeCommand issueQRCodeCommand = IssueQRCodeCommand.builder().pileSn(pileSn).build(); + // ykcPushCommandService.pushIssueQRCodeCommand(issueQRCodeCommand); + // }); + + byte[] messageBody = pileBillingTemplateService.generateBillingTemplateMsgBody(pileSn, billingTemplateVO); + + return getResult(ykcDataProtocol, messageBody); + } + +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/BillingTemplateResponseHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/BillingTemplateResponseHandler.java new file mode 100644 index 000000000..ab3094c70 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/BillingTemplateResponseHandler.java @@ -0,0 +1,48 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 计费模型设置应答 + * + * @author JS-ZZA + * @date 2022/9/27 11:31 + */ +@Slf4j +@Component +public class BillingTemplateResponseHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.BILLING_TEMPLATE_SETTING_ANSWER_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===执行计费模型设置应答逻辑===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + // 桩编号 + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, 0, 7); + String pileSn = BytesUtil.bcd2Str(pileSnByteArr); + + // 保存时间 + saveLastTime(pileSn); + + // 设置结果 0x00 失败 0x01 成功 + byte[] settingResultByteArr = BytesUtil.copyBytes(msgBody, 7, 1); + + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/BillingTemplateSettingHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/BillingTemplateSettingHandler.java new file mode 100644 index 000000000..b83c0d9a5 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/BillingTemplateSettingHandler.java @@ -0,0 +1,61 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 计费模型设置 + * + * 用户充电费用计算,每半小时为一个费率段,共 48 段,每段对应尖峰平谷其中一个费率 充电时桩屏幕按此费率分别显示已充电费和服务费 + * + * @author JS-ZZA + * @date 2022/9/19 15:17 + */ +@Slf4j +@Component +public class BillingTemplateSettingHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.BILLING_TEMPLATE_SETTING_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===执行计费模型设置逻辑===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 下发 + // 桩编号 + + // 计费模型编码 + + // 尖电费费率 精确到五位小数 + + // 尖服务费费率 + + // 峰电费费率 + + // 峰服务费费率 + + // 平电费费率 + + // 平服务费费率 + + // 谷电费费率 + + // 谷服务费费率 + + // 计损比例 + + // 48个时段费率号 0:尖费率 1:峰费率 2:平费率 3:谷费率 + + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/BillingTemplateValidateRequestHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/BillingTemplateValidateRequestHandler.java new file mode 100644 index 000000000..d00bef543 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/BillingTemplateValidateRequestHandler.java @@ -0,0 +1,89 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.google.common.primitives.Bytes; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import com.jsowell.netty.service.yunkuaichong.YKCPushCommandService; +import com.jsowell.pile.service.IPileBillingTemplateService; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 计费模板验证请求Handler + * + * @author JS-ZZA + * @date 2022/9/17 14:10 + */ +@Slf4j +@Component +public class BillingTemplateValidateRequestHandler extends AbstractHandler{ + + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.BILLING_TEMPLATE_VALIDATE_CODE.getBytes()); + + @Autowired + private IPileBillingTemplateService pileBillingTemplateService; + + @Autowired + private YKCPushCommandService ykcPushCommandService; + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + + log.info("[===执行计费模板验证请求逻辑===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 获取消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + int startIndex = 0; + int length = 7; + + // 桩号 + byte[] pileSnByte = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.binary(pileSnByte, 16); + + // 保存时间 + saveLastTime(pileSn); + + // 计费模型编码 + startIndex += length; + length = 2; + byte[] billingTemplateCodeByte = BytesUtil.copyBytes(msgBody, startIndex, length); + String billingTemplateCode = BytesUtil.binary(billingTemplateCodeByte, 16); + + // 根据桩号查询计费模板 + // BillingTemplateVO billingTemplateVO = pileBillingTemplateService.selectBillingTemplateDetailByPileSn(pileSn); + // String templateCode = null; + // if (billingTemplateVO != null) { + // templateCode = billingTemplateVO.getTemplateCode(); + // } + + + // log.info("桩传的计费模型编码:{}, 根据桩号:{} 查询到的计费模型编码:{}", billingTemplateCode, pileSn, templateCode); + // log.info("桩传的计费模型编码:{}", billingTemplateCode); + + /** + * 应答 0x00 桩计费模型与平台一致 0x01 桩计费模型与平台不一致 + */ + // byte[] flag; + // if (StringUtils.equals(billingTemplateCode, "100")){ + // flag = Constants.zeroByteArray; + // }else { + // } + byte[] flag = Constants.oneByteArray; + // 消息体 + byte[] messageBody = Bytes.concat(pileSnByte, billingTemplateCodeByte, flag); + return getResult(ykcDataProtocol, messageBody); + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/ChargeEndHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/ChargeEndHandler.java new file mode 100644 index 000000000..778528ef6 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/ChargeEndHandler.java @@ -0,0 +1,121 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.enums.ykc.OrderStatusEnum; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import com.jsowell.pile.domain.OrderBasicInfo; +import com.jsowell.pile.service.IOrderBasicInfoService; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.Objects; + +/** + * 充电结束Handler + * + * GBT-27930 充电桩与 BMS 充电结束阶段报文 + * @author JS-ZZA + * @date 2022/9/19 13:27 + */ +@Slf4j +@Component +public class ChargeEndHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.CHARGE_END_CODE.getBytes()); + + @Autowired + private IOrderBasicInfoService orderBasicInfoService; + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===执行充电结束逻辑===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + int startIndex = 0; + int length = 16; + + // 交易流水号 + byte[] serialNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String orderCode = BytesUtil.bcd2Str(serialNumByteArr); + + // 桩编码 + startIndex += length; + length = 7; + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.bcd2Str(pileSnByteArr); + + // 保存时间 + saveLastTime(pileSn); + + // 枪号 + startIndex += length; + length = 1; + byte[] connectorCodeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String connectorCode = BytesUtil.bcd2Str(connectorCodeByteArr); + + // BMS中止荷电状态 SOC 1%/位, 0%偏移量; 数据范围: 0~100% + startIndex += length; + byte[] BMSStopChargingSOC = BytesUtil.copyBytes(msgBody, startIndex, length); + String stopSoc = BytesUtil.binary(BMSStopChargingSOC, 10); + + // BMS 动力蓄电池单体最低电压 0.01 V/位, 0 V 偏移量;数据范围: 0 ~24 V + startIndex += length; + length = 2; + byte[] BMSBatteryMinVoltageByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 动力蓄电池单体最高电压 0.01 V/位, 0 V 偏移量;数据范围: 0 ~24 V + startIndex += length; + byte[] BMSBatteryMaxVoltageByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 动力蓄电池最低温度 1ºC/位, -50 ºC 偏移量;数据范围: -50 ºC ~+200 ºC + startIndex += length; + length = 1; + byte[] BMSBatteryMinTemperatureByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 动力蓄电池最高温度 1ºC/位, -50 ºC 偏移量;数据范围: -50 ºC ~+200 ºC + startIndex += length; + byte[] BMSBatteryMaxTemperatureByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 电桩累计充电时间 1 min/位, 0 min 偏移量; 数据范围: 0~600 min + startIndex += length; + length = 2; + byte[] pileSumChargingTimeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 电桩输出能量 0.1 kWh/位, 0 kWh 偏移量; 数据范围: 0~1000 kWh + startIndex += length; + byte[] pileOutputEnergyByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 电桩充电机编号 充电机编号, 1/位, 1偏移量 ,数据范围 : 0 ~ 0xFFFFFFFF + startIndex += length; + length = 4; + byte[] pileChargedCodeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 查询订单,改为待结算 将结束soc传入 + OrderBasicInfo orderInfo = orderBasicInfoService.getOrderInfoByOrderCode(orderCode); + if (Objects.nonNull(orderInfo)) { + if (StringUtils.equals(OrderStatusEnum.IN_THE_CHARGING.getValue(), orderInfo.getOrderStatus())) { + orderInfo.setOrderStatus(OrderStatusEnum.STAY_SETTLEMENT.getValue()); + } + orderInfo.setEndSOC(stopSoc); + if (orderInfo.getChargeEndTime() == null) { + orderInfo.setChargeEndTime(new Date()); // 结束充电时间 + } + orderBasicInfoService.updateOrderBasicInfo(orderInfo); + } + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/ChargerAbortedDuringChargingPhaseHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/ChargerAbortedDuringChargingPhaseHandler.java new file mode 100644 index 000000000..4e8218ed4 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/ChargerAbortedDuringChargingPhaseHandler.java @@ -0,0 +1,93 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 充电阶段充电机中止Handler 0x21 + * + * GBT-27930 充电桩与 BMS 充电阶段充电机中止报文 + * @author JS-ZZA + * @date 2022/9/19 13:35 + */ +@Slf4j +@Component +public class ChargerAbortedDuringChargingPhaseHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.THE_CHARGER_IS_ABORTED_DURING_THE_CHARGING_PHASE_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===充电阶段充电机中止===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 获取消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + int startIndex = 0; + int length = 16; + + // 交易流水号 + byte[] serialNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 桩编码 + startIndex += length; + length = 7; + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.bcd2Str(pileSnByteArr); + + // 保存时间 + saveLastTime(pileSn); + + // 枪号 + startIndex += length; + length = 1; + byte[] pileConnectorNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + /** + * BMS 中止充电原因 + * 1-2 位——达到充电机设定 的条件中止 + * 3-4 位——人工中止 + * 5-6 位——异常中止 + * 7-8 位——BMS 主动中止 + */ + startIndex += length; + byte[] BMSStopChargingReasonByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + /** + * BMS 中止充电故障原因 + * 1-2 位——充电机过温故障 + * 3-4 位——充电连接器故障 + * 5-6 位——充电机内部过温故障 + * 7-8 位——所需电量不 能传送 + * 9-10 位——充电机急停故障 + * 11-12 位——其他故障 + * 13-16 位——预留位 + */ + startIndex += length; + length = 2; + byte[] BMSStopChargingFaultReasonByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + /** + * BMS 中止充电错误原因 + * 1-2 位——电流不匹配 + * 3-4 位——电压异常 + * 5-8 位——预留位 + */ + startIndex += length; + length = 1; + byte[] BMSStopChargingErrorReasonByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/ChargingHandshakeHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/ChargingHandshakeHandler.java new file mode 100644 index 000000000..f989baa33 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/ChargingHandshakeHandler.java @@ -0,0 +1,125 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 充电握手 + * + * GBT-27930 充电桩与 BMS 充电握手阶段报文 + * @author JS-ZZA + * @date 2022/9/19 13:20 + */ +@Slf4j +@Component +public class ChargingHandshakeHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.CHARGING_HANDSHAKE_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===执行充电握手逻辑===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + int startIndex = 0; + int length = 16; + + // 交易流水号 + byte[] serialNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 桩编码 + startIndex += length; + length = 7; + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.bcd2Str(pileSnByteArr); + + // 保存时间 + saveLastTime(pileSn); + + // 枪号 + startIndex += length; + length = 1; + byte[] pileConnectorNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 通信协议版本号 当前版本为 V1.1, 表示为: byte3, byte2—0001H;byte1—01H + startIndex += length; + length = 3; + byte[] BMSCommunicationProtocolVersionByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 电池类型 01H:铅酸电池;02H:氢 电池;03H:磷酸铁锂电池;04H:锰 酸锂电池;05H:钴酸锂电池 ;06H: 三元材料电池;07H:聚合物锂离子 电池;08H:钛酸锂电池;FFH:其他 + startIndex += length; + length = 1; + byte[] BMSBatteryTypeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 整车动力蓄电池系统额定容量 0.1 Ah /位, 0 Ah 偏移量 + startIndex += length; + length = 2; + byte[] BMSBatteryCapacityByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 整车动力蓄电池系统额定总电压 0.1V/位, 0V 偏移量 + startIndex += length; + byte[] BMSBatteryVoltageByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 电池生产厂商名称 + startIndex += length; + length = 4; + byte[] BMSBatteryFactoryByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 电池组序号 + startIndex += length; + byte[] BMSBatteryNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 电池组生产日期年 1985 年偏移量, 数据范围: 1985~ 2235 年 + startIndex += length; + length = 1; + byte[] BMSProductionDateYearByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 电池组生产日期月 0 月偏移量, 数据范围: 1~12 月 + startIndex += length; + byte[] BMSProductionDateMonthByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 电池组生产日期日 0 日偏移量, 数据范围: 1~31 日 + startIndex += length; + byte[] BMSProductionDateDayByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 电池组充电次数 1 次/位, 0 次偏移量, 以 BMS 统 计为准 + startIndex += length; + length = 3; + byte[] BMSChargingTimesByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 电池组产权标识 (<0>: =租赁; <1>: =车自有) + startIndex += length; + length = 1; + byte[] BMSPropertyIdentificationByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 预留位 + startIndex += length; + byte[] BMSReservePosition = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 车辆识别码 + startIndex += length; + length = 17; + byte[] BMSCarIdentifyCodeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 软件版本号 + startIndex += length; + length = 8; + byte[] BMSSoftwareVersionByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/ConfirmStartChargingRequestHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/ConfirmStartChargingRequestHandler.java new file mode 100644 index 000000000..bc33fe23e --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/ConfirmStartChargingRequestHandler.java @@ -0,0 +1,135 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.google.common.primitives.Bytes; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.common.util.id.IdUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import com.jsowell.pile.service.impl.MemberBasicInfoServiceImpl; +import com.jsowell.pile.vo.uniapp.MemberVO; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; + +/** + * 充电桩主动申请启动充电 0x31 + * + * 启动充电鉴权结果 + * @author JS-ZZA + * @date 2022/9/19 14:29 + */ +@Slf4j +@Component +public class ConfirmStartChargingRequestHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.REQUEST_START_CHARGING_CODE.getBytes()); + + @Autowired + private MemberBasicInfoServiceImpl memberBasicInfoService; + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===充电桩主动申请启动充电===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 获取消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + int startIndex = 0; + int length = 16; + + // 桩编码 + startIndex += length; + length = 7; + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.bcd2Str(pileSnByteArr); + + // 保存时间 + saveLastTime(pileSn); + + // 枪号 + startIndex += length; + length = 1; + byte[] pileConnectorNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String connectorCode = BytesUtil.bcd2Str(pileConnectorNumByteArr); + + // 启动方式 + // 0x01 表示通过刷卡启动充电 + // 0x02 表求通过帐号启动充电 (暂不支持) + // 0x03 表示vin码启动充电 + startIndex += length; + length = 1; + byte[] startModeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 是否需要密码 0x00 不需要 0x01 需要 + startIndex += length; + byte[] needPasswordFlagByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 物理卡号 不足 8 位补 0 + startIndex += length; + length = 8; + byte[] cardNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String physicsCardNum = BytesUtil.bcd2Str(cardNumByteArr); + + // 输入密码 对用户输入的密码进行16 位MD5 加密,采用小写上传 + startIndex += length; + length = 16; + byte[] inputPasswordByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // VIN码 + startIndex += length; + length = 17; + byte[] vinCodeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + + // 应答 + // 交易流水号 + String serialNum = IdUtils.generateOrderCode(pileSn, connectorCode); + byte[] serialNumByteArr = BytesUtil.str2Bcd(serialNum); + + // 逻辑卡号 + String logicCardNum = "00000000"; + byte[] logicCardNumByteArr = BytesUtil.str2Bcd(logicCardNum); // [0, 0, 0, 0] + + // 账户余额 保留两位小数 + MemberVO memberVO = memberBasicInfoService.selectInfoByPhysicsCard(physicsCardNum); + BigDecimal principalBalance = memberVO.getPrincipalBalance(); // 本金金额 + double accountBalance = principalBalance.add(memberVO.getGiftBalance()).setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue(); + byte[] accountBalanceByteArr = BytesUtil.str2Bcd(String.valueOf(accountBalance)); + + // 鉴权成功标识 0x00 失败 0x01 成功 + byte[] authenticationFlagByteArr= Constants.oneByteArray; + + /** + * 失败原因 + * 0x01 账户不存在 + * 0x02 账户冻结 + * 0x03 账户余额不足 + * 0x04 该卡存在未结账记录 + * 0x05 桩停用 + * 0x06 该账户不能在此桩上充电 + * 0x07 密码错误 + * 0x08 电站电容不足 + * 0x09 系统中vin 码不存在 + * 0x0A 该桩存在未结账记录 + * 0x0B 该桩不支持刷卡 + */ + byte[] defeatReasonByteArr = Constants.zeroByteArray; + + // 拼装消息体 + byte[] msgBodyByteArr = Bytes.concat(serialNumByteArr, logicCardNumByteArr, accountBalanceByteArr, + authenticationFlagByteArr, defeatReasonByteArr); + + return getResult(ykcDataProtocol, msgBodyByteArr); + + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/ErrorMessageHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/ErrorMessageHandler.java new file mode 100644 index 000000000..97e038d20 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/ErrorMessageHandler.java @@ -0,0 +1,93 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 错误报文Handler + * + * GBT-27930 充电桩与 BMS 充电错误报文 + * @author JS-ZZA + * @date 2022/9/19 13:29 + */ +@Slf4j +@Component +public class ErrorMessageHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.ERROR_MESSAGE_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===错误报文===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 获取消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + int startIndex = 0; + int length = 16; + + // 交易流水号 + + byte[] serialNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 桩编码 + startIndex += length; + length = 7; + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.bcd2Str(pileSnByteArr); + + // 保存时间 + saveLastTime(pileSn); + + // 枪号 + startIndex += length; + length = 1; + byte[] pileConnectorNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 序号4-6 <00>: =正常; <01>: =超时; <10>: =不可信状态 + startIndex += length; + byte[] NumFourToSix = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 序号7-9 + startIndex += length; + byte[] NumSevenToNine = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 序号10-12 + startIndex += length; + byte[] NumTenToTwelve = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 序号13-14 + startIndex += length; + byte[] NumThirteenToFourteen = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 序号15-16 + startIndex += length; + byte[] NumFifteenToSixteen = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 序号17-19 + startIndex += length; + byte[] NumSeventeenToNineteen = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 序号20-23 + startIndex += length; + byte[] NumTwentyToTwentyThree = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 序号24-25 + startIndex += length; + byte[] NumTwentyFourToTwentyFive = BytesUtil.copyBytes(msgBody, startIndex, length); + + + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/GroundLockDataUploadHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/GroundLockDataUploadHandler.java new file mode 100644 index 000000000..adff001ab --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/GroundLockDataUploadHandler.java @@ -0,0 +1,58 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 地锁数据上送 + * + * 地锁状态/报警信息变化时,桩立刻上送变位/报警信息;地锁状态不变化时,每隔 5 分钟周期 性上送地锁状态。若无报警信息,不上送。 + * + * @author JS-ZZA + * @date 2022/9/19 15:21 + */ +@Slf4j +@Component +public class GroundLockDataUploadHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.GROUND_LOCK_DATA_UPLOAD_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===地锁数据上送===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + // 桩编码 + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, 0, 7); + // 枪号 + byte[] connectorByteArr = BytesUtil.copyBytes(msgBody, 7, 1); + /** + * 车位锁状态 + * 0x00:未到位状态 + * 0x55:升锁到位状态 + * 0xFF:降锁到位状态 + */ + byte[] parkingLockStatusByteArr = BytesUtil.copyBytes(msgBody, 8, 1); + // 车位状态 0x00:无车辆 0xFF:停放车辆 + byte[] parkingStatusByteArr = BytesUtil.copyBytes(msgBody, 9, 1); + // 地锁电量状态 百分比值0~100 + byte[] groundLockElectricByteArr = BytesUtil.copyBytes(msgBody, 10, 1); + // 报警状态 0x00:正常无报警 0xFF:待机状态摇臂破坏 0x55:摇臂升降异常(未到位) + byte[] alarmStatusByteArr = BytesUtil.copyBytes(msgBody, 11, 1); + // 预留位 全部置0 + byte[] waitingUseByteArr = BytesUtil.copyBytes(msgBody, 12, 4); + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/HeartbeatRequestHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/HeartbeatRequestHandler.java new file mode 100644 index 000000000..ba6ebae9f --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/HeartbeatRequestHandler.java @@ -0,0 +1,83 @@ +package com.jsowell.netty.handler; + +import com.google.common.primitives.Bytes; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.core.redis.RedisCache; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import com.jsowell.pile.service.IPileBasicInfoService; +import com.jsowell.pile.service.IPileConnectorInfoService; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 充电桩心跳包 + */ +@Slf4j +@Component +public class HeartbeatRequestHandler extends AbstractHandler { + + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.HEART_BEAT_CODE.getBytes()); + + @Autowired + private RedisCache redisCache; + + @Autowired + private IPileBasicInfoService pileBasicInfoService; + + @Autowired + private IPileConnectorInfoService pileConnectorInfoService; + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + // log.info("[===充电桩心跳包===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 获取消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + int startIndex = 0; + int length = 7; + + // 桩号 + byte[] pileSnByte = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.binary(pileSnByte, 16); + + // 保存时间 + saveLastTime(pileSn); + + // 枪号 + startIndex += length; + length = 1; + byte[] pileConnectorNumByte = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileConnectorNum = String.format("%02d", Integer.parseInt(BytesUtil.binary(pileConnectorNumByte, 16))); + + //枪状态(不回复) + startIndex += length; + length = 1; + byte[] connectorStatusByte = BytesUtil.copyBytes(msgBody, startIndex, length); + String connectorStatus = BytesUtil.binary(connectorStatusByte, 16); + // log.info("桩号:{}, 枪号:{}, 枪状态:{}", pileSn, pileConnectorNum, connectorStatus); + + // updateStatus(pileSn, pileConnectorNum, connectorStatus); + + // 公共方法修改状态 + pileBasicInfoService.updateStatus(BytesUtil.bcd2Str(ykcDataProtocol.getFrameType()), pileSn, pileConnectorNum, connectorStatus, null); + + // 心跳应答(置0) + byte[] flag = Constants.zeroByteArray; + + // 消息体 + byte[] messageBody = Bytes.concat(pileSnByte, pileConnectorNumByte, flag); + return getResult(ykcDataProtocol, messageBody); + } + +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/LoginRequestHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/LoginRequestHandler.java new file mode 100644 index 000000000..33817202b --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/LoginRequestHandler.java @@ -0,0 +1,189 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.google.common.collect.Lists; +import com.google.common.primitives.Bytes; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.domain.ykc.LoginRequestData; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.core.redis.RedisCache; +import com.jsowell.common.enums.ykc.PileChannelEntity; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.command.ykc.IssueQRCodeCommand; +import com.jsowell.netty.command.ykc.ProofreadTimeCommand; +import com.jsowell.netty.factory.YKCOperateFactory; +import com.jsowell.netty.service.yunkuaichong.YKCPushCommandService; +import com.jsowell.pile.service.IPileBasicInfoService; +import com.jsowell.pile.service.IPileMsgRecordService; +import com.jsowell.pile.vo.base.PileInfoVO; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +@Slf4j +@Component +public class LoginRequestHandler extends AbstractHandler{ + + @Autowired + private IPileBasicInfoService pileBasicInfoService; + + @Autowired + private YKCPushCommandService ykcPushCommandService; + + @Autowired + private IPileMsgRecordService pileMsgRecordService; + + @Autowired + private RedisCache redisCache; + + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.LOGIN_CODE.getBytes()); + + private List newProgramVersionList = Lists.newArrayList("c6-30"); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===执行登录逻辑===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 获取消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + int startIndex = 0; + int length = 7; + + // 桩编码 + byte[] pileSnByte = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.binary(pileSnByte, 16); + // log.info("桩号:{}", pileSn); + + // 保存时间 + saveLastTime(pileSn); + + // 桩类型 0 表示直流桩, 1 表示交流桩 + startIndex += length; + length = 1; + byte[] pileTypeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileType = BytesUtil.bcd2Str(pileTypeByteArr); + + // 充电枪数量 + startIndex += length; + byte[] connectorNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String connectorNum = BytesUtil.bcd2Str(connectorNumByteArr); + + // 通信协议版本 版本号乘 10,v1.0 表示 0x0A + startIndex += length; + byte[] communicationVersionByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + // int i = Integer.parseInt(BytesUtil.bcd2Str(communicationVersionByteArr)); // 0F --> 15 + BigDecimal bigDecimal = new BigDecimal(BytesUtil.bcd2Str(communicationVersionByteArr)); + BigDecimal communicationVersionTemp = bigDecimal.divide(new BigDecimal(10)); + String communicationVersion = "v" + communicationVersionTemp; + + // 程序版本 + startIndex += length; + length = 8; + byte[] programVersionByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String programVersion = BytesUtil.ascii2Str(programVersionByteArr); + // log.info("程序版本:{} length:{}", programVersion, programVersion.length()); + + // 网络连接类型 0x00 SIM 卡 0x01 LAN 0x02 WAN 0x03 其他 + startIndex += length; + length = 1; + byte[] internetConnectionTypeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String internetConnection = BytesUtil.bcd2Str(internetConnectionTypeByteArr); + + // sim卡 + startIndex += length; + length = 10; + byte[] simCardNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String iccid = BytesUtil.bin2HexStr(simCardNumByteArr); + + // 运营商 0x00 移动 0x02 电信 0x03 联通 0x04 其他 + startIndex += length; + length = 1; + byte[] businessTypeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String business = BytesUtil.bcd2Str(businessTypeByteArr); + + LoginRequestData loginRequestData = LoginRequestData.builder() + .pileSn(pileSn) + .pileType(pileType) + .connectorNum(connectorNum) + .communicationVersion(communicationVersion) + .programVersion(programVersion) + .internetConnection(internetConnection) + .iccid(iccid) + .business(business) + .build(); + + // 结果(默认 0x01:登录失败) + byte[] flag = Constants.oneByteArray; + + // 通过桩编码SN查询数据库,如果有数据,则登录成功,否则登录失败 + PileInfoVO pileInfoVO = null; + try { + pileInfoVO = pileBasicInfoService.selectPileInfoBySn(pileSn); + } catch (Exception e) { + log.error("selectPileInfoBySn发生异常", e); + } + + if (pileInfoVO != null) { + flag = Constants.zeroByteArray; + + // 登录成功,保存桩号和channel的关系 + PileChannelEntity.put(pileSn, channel); + + // 更改桩和该桩下的枪口状态分别为 在线、空闲 公共方法修改状态 + pileBasicInfoService.updateStatus(BytesUtil.bcd2Str(ykcDataProtocol.getFrameType()), pileSn, null, null, null); + } + + // 充电桩使用的sim卡,把信息存库 + if (StringUtils.equals("00", internetConnection)) { + try { + pileBasicInfoService.updatePileSimInfo(pileSn, iccid); + } catch (Exception e) { + log.error("更新充电桩sim卡信息失败", e); + } + } + + // 保存报文 + String jsonMsg = JSONObject.toJSONString(loginRequestData); + pileMsgRecordService.save(pileSn, pileSn, type, jsonMsg, ykcDataProtocol.getHEXString()); + + CompletableFuture.runAsync(() -> { + try { + Thread.sleep(200); + } catch (InterruptedException e) { + e.printStackTrace(); + } + // 对时 + ProofreadTimeCommand command = ProofreadTimeCommand.builder().pileSn(pileSn).build(); + ykcPushCommandService.pushProofreadTimeCommand(command); + }); + + // log.info("下面进行下发二维码 pileSn:{}, thread:{}", pileSn, Thread.currentThread().getName()); + CompletableFuture.runAsync(() -> { + try { + Thread.sleep(600); + } catch (InterruptedException e) { + e.printStackTrace(); + } + // 下发二维码 + IssueQRCodeCommand issueQRCodeCommand = IssueQRCodeCommand.builder().pileSn(pileSn).build(); + ykcPushCommandService.pushIssueQRCodeCommand(issueQRCodeCommand); + }); + + // 消息体 + byte[] messageBody = Bytes.concat(pileSnByte, flag); + return getResult(ykcDataProtocol, messageBody); + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/OfflineCardDataCleaningHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/OfflineCardDataCleaningHandler.java new file mode 100644 index 000000000..7e27da875 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/OfflineCardDataCleaningHandler.java @@ -0,0 +1,45 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 离线卡数据清除 + * + * 离线卡清除是平台主动下发的操作,平台在充电桩在线时会下发此数据帧到充电桩,充电桩接收到离线卡数据清除报文后清除到桩本地对应的离线卡数据 + * + * @author JS-ZZA + * @date 2022/9/19 14:54 + */ +@Slf4j +@Component +public class OfflineCardDataCleaningHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.OFFLINE_CARD_DATA_CLEANING_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===离线卡数据清除===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 下发 + // 桩编号 + + // 清除离线卡的个数 + + // 第 1 个卡物理卡号 + + // 第 N 个卡物理卡号 + + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/OfflineCardDataCleaningResponseHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/OfflineCardDataCleaningResponseHandler.java new file mode 100644 index 000000000..597e31ba6 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/OfflineCardDataCleaningResponseHandler.java @@ -0,0 +1,69 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 离线卡数据清除应答 + * + * @author JS-ZZA + * @date 2022/9/27 9:59 + */ +@Slf4j +@Component +public class OfflineCardDataCleaningResponseHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.OFFLINE_CARD_DATA_CLEANING_ANSWER_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===离线卡数据清除应答===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + int startIndex = 0; + int length = 7; + + // 桩编码 + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.bcd2Str(pileSnByteArr); + + // 保存时间 + saveLastTime(pileSn); + + // 第 1 个卡物理卡号 + startIndex += length; + length = 8; + byte[] firstCardNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 清除标记 0x00 清除失败 0x01 清除成功 + startIndex += length; + length = 1; + byte[] cleanFlagByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 失败原因 0x01 卡号格式错误 0x02 清除成功 + startIndex += length; + byte[] failedReasonByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 第N个物理卡号 + + // 清除标记 + + // 失败原因 + + + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/OfflineCardDataQueryHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/OfflineCardDataQueryHandler.java new file mode 100644 index 000000000..0b8a1bc56 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/OfflineCardDataQueryHandler.java @@ -0,0 +1,46 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 离线卡数据查询 + * + * 离线卡数据查询由平台主动向桩发起的查询请求, 平台在充电桩在线时会按需下发此数据帧到充电桩, + * 桩接收到该报文后进行查询桩本地是否存在对应的离线卡 + * + * @author JS-ZZA + * @date 2022/9/27 10:18 + */ +@Slf4j +@Component +public class OfflineCardDataQueryHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.OFFLINE_CARD_DATA_QUERY_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===离线卡数据查询===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 下发 + // 桩编码 + + // 查询的离线卡个数 + + // 第一个物理卡号 + + // 第N个物理卡号 + + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/OfflineCardDataQueryResponseHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/OfflineCardDataQueryResponseHandler.java new file mode 100644 index 000000000..81d7d1972 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/OfflineCardDataQueryResponseHandler.java @@ -0,0 +1,60 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 离线卡数据查询应答 + * + * @author JS-ZZA + * @date 2022/9/27 10:20 + */ +@Slf4j +@Component +public class OfflineCardDataQueryResponseHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.OFFLINE_CARD_DATA_QUERY_ANSWER_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===离线卡数据查询应答===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + int startIndex = 0; + int length = 7; + + // 桩编号 + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.bcd2Str(pileSnByteArr); + + // 保存时间 + saveLastTime(pileSn); + + // 第1个卡物理卡号 + startIndex += length; + length = 8; + byte[] firstCardNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 查询结果 + startIndex += length; + length = 1; + byte[] firstCardResultByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 第N+1个卡物理卡号 + // 查询结果 + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/OfflineCardDataSynchronizationHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/OfflineCardDataSynchronizationHandler.java new file mode 100644 index 000000000..6acb5846b --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/OfflineCardDataSynchronizationHandler.java @@ -0,0 +1,46 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 离线卡数据同步 + * + * 离线卡适用于桩离线充电模式, 平台在充电桩在线时会下发此数据帧到充电桩, 充电桩接收到后储存离线卡信息到桩本地 + * (如果已存在离线卡则用最新的数据覆盖本地数据, 不存在则插入), 若用户刷卡充电时桩处理离线模式,则刷鉴权走桩本地进行判断 + * + * @author JS-ZZA + * @date 2022/9/19 14:51 + */ +@Slf4j +@Component +public class OfflineCardDataSynchronizationHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.OFFLINE_CARD_DATA_SYNCHRONIZATION_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===离线卡数据同步===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 下发 + // 桩编号 + + // 下发卡个数 + + // 第一个卡逻辑卡号 + + // 第N个卡物理卡号 + + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/OfflineCardDataSynchronizationResponseHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/OfflineCardDataSynchronizationResponseHandler.java new file mode 100644 index 000000000..1e7384c08 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/OfflineCardDataSynchronizationResponseHandler.java @@ -0,0 +1,57 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 离线卡数据同步应答 + * + * @author JS-ZZA + * @date 2022/9/27 9:31 + */ +@Slf4j +@Component +public class OfflineCardDataSynchronizationResponseHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.OFFLINE_CARD_DATA_SYNCHRONIZATION_ANSWER_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===离线卡数据同步应答===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + //消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + int startIndex = 0; + int length = 7; + + // 桩编码 + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.bcd2Str(pileSnByteArr); + + // 保存时间 + saveLastTime(pileSn); + + // 保存结果 0x00 失败 0x01 成功 + startIndex += length; + length = 1; + byte[] resultByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 失败原因 0x01 卡号格式错误 0x02 储存空间不足 + startIndex += length; + byte[] failedReasonByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/ParameterConfigurationHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/ParameterConfigurationHandler.java new file mode 100644 index 000000000..e7677499f --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/ParameterConfigurationHandler.java @@ -0,0 +1,129 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import com.jsowell.pile.domain.OrderBasicInfo; +import com.jsowell.pile.service.IOrderBasicInfoService; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Objects; + +/** + * 参数配置 Handler + * + * GBT-27930 充电桩与 BMS 参数配置阶段报文 + * @author JS-ZZA + * @date 2022/9/19 13:24 + */ +@Slf4j +@Component +public class ParameterConfigurationHandler extends AbstractHandler{ + @Autowired + private IOrderBasicInfoService orderBasicInfoService; + + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.PARAMETER_CONFIGURATION_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===参数配置===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 获取消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + int startIndex = 0; + int length = 16; + + // 交易流水号 + byte[] serialNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String orderCode = BytesUtil.bcd2Str(serialNumByteArr); + + // 桩编码 + startIndex += length; + length = 7; + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.bcd2Str(pileSnByteArr); + + // 保存时间 + saveLastTime(pileSn); + + // 枪号 + startIndex += length; + length = 1; + byte[] pileConnectorNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 单体动力蓄电池最高允许充电电压 0.01 V/位, 0 V 偏移量; 数据范围: 0~24 V + startIndex += length; + length = 2; + byte[] BMSMaxVoltageByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 最高允许充电电流 0.1 A/位, -400A 偏移量 + startIndex += length; + byte[] BMSMaxCurrentByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 动力蓄电池标称总能量 0.1 kWh/位, 0 kWh 偏移量; 数据范围: 0~1000 kWh + startIndex += length; + byte[] BMSSumEnergyByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 最高允许充电总电压 0.1 V/位, 0 V 偏移量 + startIndex += length; + byte[] BMSMaxChargingVoltageByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 最高允许温度 1ºC/位, -50 ºC 偏移量;数据范 围: -50 ºC ~+200 ºC + startIndex += length; + length = 1; + byte[] BMSMaxTemperatureByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // BMS 整车动力 蓄电池荷电状态(soc) 0.1%/位, 0%偏移量;数据范围: 0~100% + startIndex += length; + length = 2; + byte[] BMSSocByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String soc = YKCUtils.convertVoltageCurrent(BMSSocByteArr); + + + // BMS 整车动力蓄电池当前电池电压 整车动力蓄电池总电压 + startIndex += length; + byte[] BMSRealTimeVoltageByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 电桩最高输出电压 0.1 V /位, 0 V 偏移量 + startIndex += length; + byte[] pileMaxOutputVoltageByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 电桩最低输出电压 0.1 V /位, 0 V 偏移量 + startIndex += length; + byte[] pileMinOutputVoltageByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 电桩最大输出电流 0.1 A/位, -400 A 偏移量 + startIndex += length; + byte[] pileMaxOutputCurrentByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 电桩最小输出电流 0.1 A/位, -400 A 偏移量 + startIndex += length; + byte[] pileMinOutputCurrentByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + log.info("参数配置, 起始SOC:{}", soc); + + // 查询该订单下信息,将起始soc传入 + OrderBasicInfo orderInfo = orderBasicInfoService.getOrderInfoByOrderCode(orderCode); + if (Objects.nonNull(orderInfo)) { + OrderBasicInfo orderBasicInfo = OrderBasicInfo.builder() + .id(orderInfo.getId()) + .startSOC(soc) + .build(); + + orderBasicInfoService.updateOrderBasicInfo(orderBasicInfo); + } + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/PileWorkingParameterSettingHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/PileWorkingParameterSettingHandler.java new file mode 100644 index 000000000..ca4cb328e --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/PileWorkingParameterSettingHandler.java @@ -0,0 +1,45 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 充电桩工作参数设置. + * + * 远程设置充电桩是否停用;设置充电桩允许输出功率,以实现电网功率的调节 + * + * @author JS-ZZA + * @date 2022/9/19 15:06 + */ +@Slf4j +@Component +public class PileWorkingParameterSettingHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.CHARGING_PILE_WORKING_PARAMETER_SETTING_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===充电桩工作参数设置===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 下发 + // 桩编号 + + // 是否允许工作 + + // 充电桩最大允许输出功率 + + + // return super.supplyProcess(ykcDataTemplate, channel); + return null; + } + +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/PileWorkingParameterSettingResponseHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/PileWorkingParameterSettingResponseHandler.java new file mode 100644 index 000000000..f063fe40b --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/PileWorkingParameterSettingResponseHandler.java @@ -0,0 +1,52 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 充电桩工作参数设置应答 + * + * @author JS-ZZA + * @date 2022/9/27 10:40 + */ +@Slf4j +@Component +public class PileWorkingParameterSettingResponseHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.CHARGING_PILE_WORKING_PARAMETER_SETTING_ANSWER_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===充电桩工作参数设置应答===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + int startIndex = 0; + int length = 7; + + // 桩编号 + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.bcd2Str(pileSnByteArr); + + // 保存时间 + saveLastTime(pileSn); + + // 设置结果 + startIndex += length; + length = 1; + byte[] settingResultByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/ReadRealTimeMonitorDataHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/ReadRealTimeMonitorDataHandler.java new file mode 100644 index 000000000..d89324807 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/ReadRealTimeMonitorDataHandler.java @@ -0,0 +1,57 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.google.common.primitives.Bytes; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import com.jsowell.netty.service.yunkuaichong.impl.YKCPushCommandServiceImpl; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 读取实时监测数据(服务器端向桩发送消息 0x12) + * + * @deprecated 桩不会发送这个指令,由平台主动发送 + * @author JS-ZZA + * @date 2022/9/19 8:43 + */ +@Slf4j +@Component +public class ReadRealTimeMonitorDataHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.READ_REAL_TIME_MONITOR_DATA_CODE.getBytes()); + + @Autowired + private YKCPushCommandServiceImpl ykcPushBusinessServiceImpl; + + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===读取实时监测数据===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 下发 + // 桩编号 + byte[] pileSnByteArr = new byte[]{}; + String pileSn = BytesUtil.bcd2Str(pileSnByteArr); + // 枪号 + byte[] pileConnectorByteArr = Constants.oneByteArray; + + // 拼接消息体 + byte[] msg = Bytes.concat(pileSnByteArr, pileConnectorByteArr); + // push消息 + // boolean result = ykcPushBusinessServiceImpl.push(msg, pileSn, YKCFrameTypeCode.READ_REAL_TIME_MONITOR_DATA_CODE); + + // log.info(String.valueOf(result)); + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteAccountBalanceUpdateRequestHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteAccountBalanceUpdateRequestHandler.java new file mode 100644 index 000000000..d2b129295 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteAccountBalanceUpdateRequestHandler.java @@ -0,0 +1,74 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 余额更新应答 + * + * @author JS-ZZA + * @date 2022/9/19 14:47 + */ +@Slf4j +@Component +public class RemoteAccountBalanceUpdateRequestHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.REMOTE_ACCOUNT_BALANCE_UPDATE_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===余额更新应答===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + int startIndex = 0; + int length = 7; + + // 桩编码 + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.bcd2Str(pileSnByteArr); + + // 保存时间 + saveLastTime(pileSn); + + // 枪号 + startIndex += length; + length = 1; + byte[] pileConnectorNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 物理卡号 不足 8 位补零 + //如果不为零, 需要校验本次充电是 否为此卡充电 + //如果为零, 则不校验, 直接更新桩 当前充电用户余额 + startIndex += length; + length = 8; + byte[] cardNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 修改后账户金额 保留两位小数 + startIndex += length; + length = 4; + byte[] accountBalance = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 应答 + // 帧类型 + byte[] remoteAccountBalanceUpdateAnswerCodeBytes = YKCFrameTypeCode.REMOTE_ACCOUNT_BALANCE_UPDATE_ANSWER_CODE.getBytes(); + + // 修改结果 0x00-修改成功 + //0x01-设备编号错误 + //0x02-卡号错误 + + + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteControlGroundLockHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteControlGroundLockHandler.java new file mode 100644 index 000000000..0a08e7076 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteControlGroundLockHandler.java @@ -0,0 +1,45 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 遥控地锁升锁与降锁命令 + * + * 服务器下发命令给地锁,地锁执行动作 + * + * @author JS-ZZA + * @date 2022/9/19 15:41 + */ +@Slf4j +@Component +public class RemoteControlGroundLockHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.REMOTE_CONTROL_GROUND_LOCK_LIFTING_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===遥控地锁升锁与降锁命令===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 下发 + // 桩编码 + + // 枪号 + + // 升/降地锁 + + // 预留位 + + // return super.supplyProcess(ykcDataTemplate, channel); + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteControlGroundLockResponseHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteControlGroundLockResponseHandler.java new file mode 100644 index 000000000..93ad4518e --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteControlGroundLockResponseHandler.java @@ -0,0 +1,57 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 充电桩返回遥控地锁升锁与降锁数据(上行) + * + * @author JS-ZZA + * @date 2022/9/27 13:21 + */ +@Slf4j +@Component +public class RemoteControlGroundLockResponseHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.CHARGING_PILE_RESPOND_GROUND_LOCK_LIFTING_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===充电桩返回遥控地锁升锁与降锁数据(上行)===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + int startIndex = 0; + int length = 7; + + // 桩编号 + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 枪号 + startIndex += length; + length = 1; + byte[] connectorCodeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 地锁控制返回标志 布尔型( 1, 鉴权成功; 0, 鉴权失败) + startIndex += length; + byte[] controlResultByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + // 预留位 + startIndex += length; + length = 4; + byte[] waitingUseByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteIssuedQrCodeHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteIssuedQrCodeHandler.java new file mode 100644 index 000000000..36a4e0a6b --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteIssuedQrCodeHandler.java @@ -0,0 +1,75 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.google.common.primitives.Bytes; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import com.jsowell.netty.service.yunkuaichong.impl.YKCPushCommandServiceImpl; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 后台远程下发二维码前缀指令 + * + * 二维码下发时,只需下发前缀,同时选择是第 0 种格式,还是第 1 种格式即可,如果二维码格式为 0 种,桩自动补充桩编号。 + * 如果二维码格式为 1 种,桩自动补充桩编号+2位枪编号。 + * 注册通过后,后台即可立即下发二维码。推荐每次注册通过后,均下发一次二维码。每个桩下发一次前缀即可。无须按照枪个数下发。 + * + * @deprecated 桩不会发送这个指令,由平台主动发送 + * @author JS-ZZA + * @date 2022/9/29 14:05 + */ +@Slf4j +@Component +public class RemoteIssuedQrCodeHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.REMOTE_ISSUE_QRCODE_CODE.getBytes()); + + @Autowired + private YKCPushCommandServiceImpl ykcPushBusinessService; + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===后台远程下发二维码前缀指令===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 下发 + // 桩编码 + String pileSn = "88000000000001"; + byte[] a = new byte[]{88}; + byte[] b = Constants.zeroByteArray; + byte[] c = Constants.zeroByteArray; + byte[] d = Constants.zeroByteArray; + byte[] e = Constants.zeroByteArray; + byte[] f = Constants.zeroByteArray; + byte[] g = new byte[]{0x02}; + byte[] pileSnByteArr = Bytes.concat(a, b, c, d, e, f, g); + + // 二维码格式 0x00:第一种 前缀+桩编号 0x01:第二种 前缀+桩编号+枪编号 + byte[] qrCodeTypeByteArr = Constants.zeroByteArray; + // 二维码前缀 如:“www.baidu.com?No=” + String qrCodePrefix = "https://wx.charging.shbochong.cn/prepare_charge?code="; + byte[] qrCodePrefixByteArr = BytesUtil.str2Asc(qrCodePrefix); + // 二维码前缀长度 二维码前缀长度长度最大不超过200 字节 + int length = qrCodePrefix.length(); + byte[] qrCodePrefixLengthByteArr = BytesUtil.intToBytes(length); + + // 拼接消息体 + byte[] msg = Bytes.concat(pileSnByteArr, qrCodeTypeByteArr, qrCodePrefixLengthByteArr, qrCodePrefixByteArr); + + // push消息 + // boolean result = ykcPushBusinessService.push(msg, pileSn, YKCFrameTypeCode.REMOTE_ISSUE_QRCODE_CODE); + + // log.info(String.valueOf(result)); + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteIssuedQrCodeResponseHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteIssuedQrCodeResponseHandler.java new file mode 100644 index 000000000..efffd4b54 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteIssuedQrCodeResponseHandler.java @@ -0,0 +1,52 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 桩应答远程下发二维码前缀指令 + * + * @author JS-ZZA + * @date 2022/9/29 14:10 + */ +@Slf4j +@Component +public class RemoteIssuedQrCodeResponseHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.REMOTE_ISSUE_QRCODE_ANSWER_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===桩应答远程下发二维码前缀指令===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + int startIndex = 0; + int length = 7; + + // 桩编码 + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.bcd2Str(pileSnByteArr); + + // 保存时间 + saveLastTime(pileSn); + + // 下发结果 0x00:成功 0x01:失败 + startIndex += length; + length = 1; + byte[] issuedResultByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteRestartHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteRestartHandler.java new file mode 100644 index 000000000..33f952924 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteRestartHandler.java @@ -0,0 +1,41 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 远程重启 + * + * 重启充电桩,应对部分问题,如卡死 + * 这个属于平台主动下发的指令 + * @author JS-ZZA + * @date 2022/9/19 15:49 + */ +@Slf4j +@Component +@Deprecated +public class RemoteRestartHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.REMOTE_RESTART_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===远程重启===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 下发 + // 桩编号 + + // 执行控制 0x01:立即执行 0x02:空闲执行 + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteRestartResponseHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteRestartResponseHandler.java new file mode 100644 index 000000000..8d24061b1 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteRestartResponseHandler.java @@ -0,0 +1,61 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import com.jsowell.pile.service.IPileMsgRecordService; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 远程重启应答 + * + * @author JS-ZZA + * @date 2022/9/27 13:27 + */ +@Slf4j +@Component +public class RemoteRestartResponseHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.REMOTE_RESTART_ANSWER_CODE.getBytes()); + + @Autowired + private IPileMsgRecordService pileMsgRecordService; + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===远程重启应答===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + int startIndex = 0; + int length = 7; + + // 桩编号 + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.bcd2Str(pileSnByteArr); + + // 保存时间 + saveLastTime(pileSn); + + // 保存报文 + String jsonMsg = JSONObject.toJSONString(ykcDataProtocol); + pileMsgRecordService.save(pileSn, null, type, jsonMsg, ykcDataProtocol.getHEXString()); + + // 设置结果 + startIndex += length; + length = 1; + byte[] settingResultByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteStartChargingRequestHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteStartChargingRequestHandler.java new file mode 100644 index 000000000..681c5b0e1 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteStartChargingRequestHandler.java @@ -0,0 +1,98 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.enums.ykc.ChargingFailedReasonEnum; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import com.jsowell.pile.service.IOrderBasicInfoService; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 远程启动充电命令回复 0x33, 0x34 + * + * @author JS-ZZA + * @date 2022/9/19 14:35 + */ +@Slf4j +@Component +public class RemoteStartChargingRequestHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.REMOTE_START_CHARGING_ANSWER_CODE.getBytes()); + + @Autowired + private IOrderBasicInfoService orderBasicInfoService; + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===远程启动充电命令回复===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + int startIndex = 0; + int length = 16; + + // 交易流水号 + byte[] orderCodeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String orderCode = BytesUtil.bcd2Str(orderCodeByteArr); + + // 桩编码 + startIndex += length; + length = 7; + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.bcd2Str(pileSnByteArr); + + // 保存时间 + saveLastTime(pileSn); + + // 枪号 + startIndex += length; + length = 1; + byte[] connectorCodeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String connectorCode = BytesUtil.bcd2Str(connectorCodeByteArr); + + // 启动结果 0x00失败 0x01成功 + startIndex += length; + byte[] startResultByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String startResult = BytesUtil.bcd2Str(startResultByteArr); + + /** + * 失败原因 + * + * 桩在收到启充命令后,检测到未插枪则发送 0x33 报文回复充电失败。 + * 若在 60 秒(以收到 0x34 时间开始计算)内检测到枪重新连接,则补送 0x33 成功报文;超时或者离线等其他异常,桩不启充、不补发 0x33 报文 + * 0x00 无 + * 0x01 设备编号不匹配 + * 0x02 枪已在充电 + * 0x03 设备故障 + * 0x04 设备离线 + * 0x05 未插枪 + */ + startIndex += length; + byte[] failedReasonByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String failedReason = BytesUtil.bin2HexStr(failedReasonByteArr); + String failedReasonMsg = ChargingFailedReasonEnum.getMsgByCode(Integer.parseInt(failedReason, 16)); + + if (StringUtils.equals(startResult, Constants.DOUBLE_ZERO)) { + // 启动失败 + orderBasicInfoService.chargingPileFailedToStart(orderCode, failedReasonMsg); + } else { + // 启动成功 + orderBasicInfoService.chargingPileStartedSuccessfully(orderCode); + } + // orderBasicInfoService.updateOrderBasicInfo(orderInfo); + log.info("交易流水号:{}, 桩编码:{}, 枪号:{}, 启动结果:{}, 失败原因:{}", orderCode, pileSn, connectorCode, startResult, failedReasonMsg); + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteStopChargingRequestHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteStopChargingRequestHandler.java new file mode 100644 index 000000000..b389437f8 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteStopChargingRequestHandler.java @@ -0,0 +1,99 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.enums.ykc.OrderStatusEnum; +import com.jsowell.common.enums.ykc.StopChargingFailedReasonEnum; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import com.jsowell.pile.domain.OrderBasicInfo; +import com.jsowell.pile.service.IOrderBasicInfoService; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Date; + +/** + * 远程停机命令回复 + * + * @author JS-ZZA + * @date 2022/9/19 14:37 + */ +@Slf4j +@Component +public class RemoteStopChargingRequestHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.REMOTE_STOP_CHARGING_ANSWER_CODE.getBytes()); + + @Autowired + private IOrderBasicInfoService orderBasicInfoService; + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===远程停机命令回复===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 获取消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + int startIndex = 0; + int length = 7; + + // 桩编号 + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.bcd2Str(pileSnByteArr); + + // 保存时间 + saveLastTime(pileSn); + + // 枪号 + startIndex += length; + length = 1; + byte[] connectorCodeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String connectorCode = BytesUtil.bcd2Str(connectorCodeByteArr); + + // 停止结果 0x00失败 0x01成功 + startIndex += length; + byte[] stopResultByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String stopResult = BytesUtil.bcd2Str(stopResultByteArr); + + /** + * 失败原因 + * 0x00 无 + * 0x01 设备编号不匹配 + * 0x02 枪未处于充电状态 + * 0x03 其他 + */ + startIndex += length; + byte[] reasonByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String reasonCode = BytesUtil.bcd2Str(reasonByteArr); + String reason = StopChargingFailedReasonEnum.getMsgByCode(reasonCode); + + // 通过桩编号+枪口号 查出订单 + OrderBasicInfo order = orderBasicInfoService.queryChargingByPileSnAndConnectorCode(pileSn, connectorCode); + if (order != null) { + // 收到停机回复后,修改订单状态 + if (StringUtils.equals(stopResult, "01")) { + // 停机成功,修改订单状态为 待结算 + order.setOrderStatus(OrderStatusEnum.STAY_SETTLEMENT.getValue()); + if (order.getChargeEndTime() == null) { + order.setChargeEndTime(new Date()); // 结束充电时间 + } + } else { + // 停机失败,修改订单状态为 异常 + order.setOrderStatus(OrderStatusEnum.ABNORMAL.getValue()); + order.setReason(reason); + } + orderBasicInfoService.updateOrderBasicInfo(order); + } + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteUpdateHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteUpdateHandler.java new file mode 100644 index 000000000..04c797261 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteUpdateHandler.java @@ -0,0 +1,58 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 远程更新 + * + * 对桩进行软件升级,平台升级模式为 ftp 文件升级,由桩企提供升级需要的更新文件(特定文件名, 由桩企定义) , + * 平台在数据帧中提供访问更新文件相关服务器地址及下载路径信息, 桩下载完更新程序后对文件进行较验,并对桩进行升级 + * + * @author JS-ZZA + * @date 2022/9/19 15:56 + */ +@Slf4j +@Component +public class RemoteUpdateHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.REMOTE_UPDATE_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===远程更新===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 下发 + // 桩编号 + + // 桩型号 0x01:直流 0x02:交流 + + // 桩功率 不足 2 位补零 + + // 升级服务器地址 不足 16 位补零 + + // 升级服务器端口 不足 2 位补零 + + // 用户名 不足 16 位补零 + + // 密码 不足 16 位补零 + + // 文件路径 不足 32 位补零,文件路径名由平 台定义 + + // 执行控制 0x01:立即执行 0x02:空闲执行 + + // 下载超时时间 单位: min + + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteUpdateResponseHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteUpdateResponseHandler.java new file mode 100644 index 000000000..2291d071f --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/RemoteUpdateResponseHandler.java @@ -0,0 +1,52 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 远程更新应答 + * + * @author JS-ZZA + * @date 2022/9/27 13:32 + */ +@Slf4j +@Component +public class RemoteUpdateResponseHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.REMOTE_UPDATE_ANSWER_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[====远程更新应答====] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + int startIndex = 0; + int length = 7; + + // 桩编号 + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, 0, 7); + String pileSn = BytesUtil.bcd2Str(pileSnByteArr); + + // 保存时间 + saveLastTime(pileSn); + + // 升级状态 0x00-成功 0x01-编号错误 0x02-程序与桩型号不符 0x03-下载更新文件超时 + startIndex += length; + length = 1; + byte[] updateStatusByteArr = BytesUtil.copyBytes(msgBody, 7, 1); + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/TimeCheckSettingHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/TimeCheckSettingHandler.java new file mode 100644 index 000000000..803b44b72 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/TimeCheckSettingHandler.java @@ -0,0 +1,41 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 对时设置 + * + * 运营平台同步充电桩时钟,以保证充电桩与运营平台的时钟一致 + * + * @author JS-ZZA + * @date 2022/9/19 15:11 + */ +@Slf4j +@Component +public class TimeCheckSettingHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.TIME_CHECK_SETTING_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===对时设置===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 下发 + // 桩编号 + + // 当前时间 CP56Time2a 格式 + + // return super.supplyProcess(ykcDataTemplate, channel); + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/TimeCheckSettingResponseHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/TimeCheckSettingResponseHandler.java new file mode 100644 index 000000000..75e5c53af --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/TimeCheckSettingResponseHandler.java @@ -0,0 +1,52 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 对时设置应答 + * + * @author JS-ZZA + * @date 2022/9/27 11:09 + */ +@Slf4j +@Component +public class TimeCheckSettingResponseHandler extends AbstractHandler{ + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.TIME_CHECK_SETTING_ANSWER_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + } + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===对时设置应答===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + int startIndex = 0; + int length = 7; + + // 桩编号 + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.bcd2Str(pileSnByteArr); + + // 保存时间 + saveLastTime(pileSn); + + // 当前时间 + startIndex += length; + length = 7; + byte[] currentTimeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + + return null; + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/TransactionRecordsRequestHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/TransactionRecordsRequestHandler.java new file mode 100644 index 000000000..9a20c33f0 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/TransactionRecordsRequestHandler.java @@ -0,0 +1,559 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.google.common.primitives.Bytes; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.domain.ykc.TransactionRecordsData; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.core.redis.RedisCache; +import com.jsowell.common.enums.ykc.OrderStatusEnum; +import com.jsowell.common.enums.ykc.YKCChargingStopReasonEnum; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.Cp56Time2a.Cp56Time2aUtil; +import com.jsowell.common.util.DateUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.common.util.id.IdUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import com.jsowell.pile.domain.OrderBasicInfo; +import com.jsowell.pile.service.IOrderBasicInfoService; +import com.jsowell.pile.service.IPileMsgRecordService; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.Objects; + +/** + * 交易记录确认 + * 这一帧仅是报文交互使用, 意指平台成功接收到交易记录报文,并不代表交易订单成功结算 + * 运营平台接收到结算账单上传后,都需回复此确认信息。若桩未收到回复帧,则 5 分钟后继续 上送一次交易记录, + * 此情况下无论平台是否成功回复都停止上送。 + * + * @author JS-ZZA + * @date 2022/9/19 14:40 + */ +@Slf4j +@Component +public class TransactionRecordsRequestHandler extends AbstractHandler { + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.TRANSACTION_RECORDS_CODE.getBytes()); + private final String oldVersionType = YKCUtils.frameType2Str(YKCFrameTypeCode.TRANSACTION_RECORDS_OLD_VERSION_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + YKCOperateFactory.register(oldVersionType, this); + } + + @Autowired + private RedisCache redisCache; + + @Autowired + private IPileMsgRecordService pileMsgRecordService; + + @Autowired + private IOrderBasicInfoService orderBasicInfoService; + + /*public static void main(String[] args) { + String msgBodyStr = "880000000000040122121516483531998800000000000401000030100f0c16a8003b011a0368100f0400000000000000000000000000c891050000000000000000000000000080140700a406000000000000d01e000090170d0000000000000000000000000010b0390b0078f2390b00a406000000000000781e0000ffffffffffffffffffffffffffffffffff01a8003b011a0368830000000000000000" ; + byte[] msgBody = BytesUtil.str2Bcd(msgBodyStr); + int startIndex = 0; + int length = 16; + + // 交易流水号 + byte[] orderCodeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String orderCode = BytesUtil.bcd2Str(orderCodeByteArr); + + // 桩编码 + startIndex += length; + length = 7; + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.bcd2Str(pileSnByteArr); + + // 枪号 + startIndex += length; + length = 1; + byte[] connectorCodeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String connectorCode = BytesUtil.bcd2Str(connectorCodeByteArr); + // 根据不同程序版本获取工具类 + String programVersion = redisCache.getCacheMapValue(CacheConstants.PILE_PROGRAM_VERSION, pileSn); + AbsCp56Time2aUtil cp56Time2aUtil = Cp56Time2aFactory.getInvokeStrategy(programVersion); + // 开始时间 CP56Time2a 格式 + startIndex += length; + length = 7; + byte[] startTimeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String startTime = cp56Time2aUtil.toDateString(startTimeByteArr); + + // 结束时间 CP56Time2a 格式 + startIndex += length; + byte[] endTimeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String endTime = cp56Time2aUtil.toDateString(endTimeByteArr); + + // 尖单价 精确到小数点后五位(尖电费+尖服务费,见费率帧) + startIndex += length; + length = 4; + byte[] sharpPriceByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String sharpPrice = YKCUtils.convertDecimalPoint(sharpPriceByteArr, 5); + + // 尖电量 精确到小数点后四位 + startIndex += length; + length = 4; + byte[] sharpUsedElectricityByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String sharpUsedElectricity = YKCUtils.convertDecimalPoint(sharpUsedElectricityByteArr, 4); + + // 计损尖电量 + startIndex += length; + byte[] sharpPlanLossElectricityByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String sharpPlanLossElectric = YKCUtils.convertDecimalPoint(sharpPlanLossElectricityByteArr, 4); + + // 尖金额 + startIndex += length; + byte[] sharpAmountByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String sharpAmount = YKCUtils.convertDecimalPoint(sharpAmountByteArr, 4); + + // 峰单价 精确到小数点后五位(峰电费+峰服务费) + startIndex += length; + byte[] peakPriceByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String peakPrice = YKCUtils.convertDecimalPoint(peakPriceByteArr, 5); + + // 峰电量 + startIndex += length; + byte[] peakUsedElectricityByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String peakUsedElectricity = YKCUtils.convertDecimalPoint(peakUsedElectricityByteArr, 4); + + // 计损峰电量 + startIndex += length; + byte[] peakPlanLossElectricityByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String peakPlanLossElectricity = YKCUtils.convertDecimalPoint(peakPlanLossElectricityByteArr, 4); + + // 峰金额 + startIndex += length; + byte[] peakAmountByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String peakAmount = YKCUtils.convertDecimalPoint(peakAmountByteArr, 4); + + // 平单价 精确到小数点后五位(平电费+平服务费) + startIndex += length; + byte[] flatPriceByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String flatPrice = YKCUtils.convertDecimalPoint(flatPriceByteArr, 5); + + // 平电量 + startIndex += length; + byte[] flatUsedElectricityByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String flatUsedElectricity = YKCUtils.convertDecimalPoint(flatUsedElectricityByteArr, 4); + + // 计损平电量 + startIndex += length; + byte[] flatPlanLossElectricityByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String flatPlanLossElectricity = YKCUtils.convertDecimalPoint(flatPlanLossElectricityByteArr, 4); + + // 平金额 + startIndex += length; + byte[] flatAmountByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String flatAmount = YKCUtils.convertDecimalPoint(flatAmountByteArr, 4); + + // 谷单价 精确到小数点后五位(谷电费+谷 服务费) + startIndex += length; + byte[] valleyPriceByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String valleyPrice = YKCUtils.convertDecimalPoint(valleyPriceByteArr, 5); + + // 谷电量 + startIndex += length; + byte[] valleyUsedElectricityByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String valleyUsedElectricity = YKCUtils.convertDecimalPoint(valleyUsedElectricityByteArr, 4); + + // 计损谷电量 + startIndex += length; + byte[] valleyPlanLossElectricityByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String valleyPlanLossElectricity = YKCUtils.convertDecimalPoint(valleyPlanLossElectricityByteArr, 4); + + // 谷金额 + startIndex += length; + byte[] valleyAmountByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String valleyAmount = YKCUtils.convertDecimalPoint(valleyAmountByteArr, 4); + + // 电表总起值 + startIndex += length; + length = 5; + byte[] ammeterTotalStartByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String ammeterTotalStart = YKCUtils.convertDecimalPoint(ammeterTotalStartByteArr, 4); + + // 电表总止值 + startIndex += length; + byte[] ammeterTotalEndByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String ammeterTotalEnd = YKCUtils.convertDecimalPoint(ammeterTotalEndByteArr, 4); + + // 总电量 + startIndex += length; + length = 4; + byte[] totalElectricityByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String totalElectricity = YKCUtils.convertDecimalPoint(totalElectricityByteArr, 4); + + // 计损总电量 + startIndex += length; + byte[] planLossTotalElectricityByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String planLossTotalElectricity = YKCUtils.convertDecimalPoint(planLossTotalElectricityByteArr, 4); + + // 消费金额 精确到小数点后四位,包含电费、 服务费 + startIndex += length; + byte[] consumptionAmountByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String consumptionAmount = YKCUtils.convertDecimalPoint(consumptionAmountByteArr, 4); + + // VIN 码 VIN 码,此处 VIN 码和充电时 VIN 码不同, 正序直接上传, 无需补 0 和反序 + startIndex += length; + length = 17; + byte[] vinCodeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String vinCode = BytesUtil.ascii2Str(vinCodeByteArr); + + *//** + * 交易标识 + * 0x01: app 启动 + * 0x02:卡启动 + * 0x04:离线卡启动 + * 0x05: vin 码启动充电 + *//* + startIndex += length; + length = 1; + byte[] transactionIdentifierByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String transactionIdentifier = BytesUtil.bcd2Str(transactionIdentifierByteArr); + + // 交易时间 CP56Time2a 格式 + startIndex += length; + length = 7; + byte[] transactionTimeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String transactionTime = cp56Time2aUtil.toDateString(transactionTimeByteArr); + + // 停止原因 + startIndex += length; + length = 1; + byte[] stopReasonByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String stopReason = BytesUtil.bin2HexStr(stopReasonByteArr); + String stopReasonMsg = YKCChargingStopReasonEnum.getMsgByCode(Integer.parseInt(stopReason, 16)); + + // 物理卡号 不足 8 位补 0 + startIndex += length; + length = 8; + byte[] cardNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + byte[] logicCardNum = BytesUtil.checkLengthAndBehindAppendZero(cardNumByteArr, 16); + String logicCard = BytesUtil.binary(logicCardNum, 10); + + log.info("交易流水号:{}, 桩编号:{}, 枪号:{}, 开始时间:{}, 结束时间:{}, 尖单价:{}, 尖电量:{}, 计损尖电量:{}, 尖金额:{}, " + + "峰单价:{}, 峰电量:{}, 计损峰电量:{}, 峰金额:{}, 平单价:{}, 平电量:{}, 计损平电量:{}, 平金额:{}, " + + "谷单价:{}, 谷电量:{}, 计损谷电量:{}, 谷金额:{}, 电表总起值:{}, 电表总止值:{}, 总电量:{}, 计损总电量:{}, 消费金额:{}, " + + "电动汽车唯一标识:{}, 交易标识:{}, 交易时间:{}, 停止原因码:{}, 停止原因描述:{}, 物理卡号:{}", + orderCode, pileSn, connectorCode, startTime, endTime, sharpPrice, sharpUsedElectricity, sharpPlanLossElectric, sharpAmount, + peakPrice, peakUsedElectricity, peakPlanLossElectricity, peakAmount, flatPrice, flatUsedElectricity, flatPlanLossElectricity, flatAmount, + valleyPrice, valleyUsedElectricity, valleyPlanLossElectricity, valleyAmount, ammeterTotalStart, ammeterTotalEnd, totalElectricity, planLossTotalElectricity, + consumptionAmount, vinCode, transactionIdentifier, transactionTime, stopReason, stopReasonMsg, logicCard); + }*/ + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + // log.info("[===交易记录===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + // 获取消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + + int startIndex = 0; + int length = 16; + + // 交易流水号 + byte[] orderCodeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String orderCode = BytesUtil.bcd2Str(orderCodeByteArr); + + // 桩编码 + startIndex += length; + length = 7; + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.bcd2Str(pileSnByteArr); + + // 枪号 + startIndex += length; + length = 1; + byte[] connectorCodeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String connectorCode = BytesUtil.bcd2Str(connectorCodeByteArr); + + + // 开始时间 CP56Time2a 格式 + startIndex += length; + length = 7; + byte[] startTimeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + // String binary = BytesUtil.binary(startTimeByteArr, 16); + Date startDate = Cp56Time2aUtil.byte2Hdate(startTimeByteArr); + String startTime = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, startDate); + + + // 结束时间 CP56Time2a 格式 + startIndex += length; + byte[] endTimeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + Date endDate = Cp56Time2aUtil.byte2Hdate(endTimeByteArr); + String endTime = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, endDate); + + // 尖单价 精确到小数点后五位(尖电费+尖服务费,见费率帧) + startIndex += length; + length = 4; + byte[] sharpPriceByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String sharpPrice = YKCUtils.convertDecimalPoint(sharpPriceByteArr, 5); + + // 尖电量 精确到小数点后四位 + startIndex += length; + length = 4; + byte[] sharpUsedElectricityByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String sharpUsedElectricity = YKCUtils.convertDecimalPoint(sharpUsedElectricityByteArr, 4); + + // 计损尖电量 + startIndex += length; + byte[] sharpPlanLossElectricityByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String sharpPlanLossElectricity = YKCUtils.convertDecimalPoint(sharpPlanLossElectricityByteArr, 4); + + // 尖金额 + startIndex += length; + byte[] sharpAmountByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String sharpAmount = YKCUtils.convertDecimalPoint(sharpAmountByteArr, 4); + + // 峰单价 精确到小数点后五位(峰电费+峰服务费) + startIndex += length; + byte[] peakPriceByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String peakPrice = YKCUtils.convertDecimalPoint(peakPriceByteArr, 5); + + // 峰电量 + startIndex += length; + byte[] peakUsedElectricityByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String peakUsedElectricity = YKCUtils.convertDecimalPoint(peakUsedElectricityByteArr, 4); + + // 计损峰电量 + startIndex += length; + byte[] peakPlanLossElectricityByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String peakPlanLossElectricity = YKCUtils.convertDecimalPoint(peakPlanLossElectricityByteArr, 4); + + // 峰金额 + startIndex += length; + byte[] peakAmountByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String peakAmount = YKCUtils.convertDecimalPoint(peakAmountByteArr, 4); + + // 平单价 精确到小数点后五位(平电费+平服务费) + startIndex += length; + byte[] flatPriceByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String flatPrice = YKCUtils.convertDecimalPoint(flatPriceByteArr, 5); + + // 平电量 + startIndex += length; + byte[] flatUsedElectricityByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String flatUsedElectricity = YKCUtils.convertDecimalPoint(flatUsedElectricityByteArr, 4); + + // 计损平电量 + startIndex += length; + byte[] flatPlanLossElectricityByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String flatPlanLossElectricity = YKCUtils.convertDecimalPoint(flatPlanLossElectricityByteArr, 4); + + // 平金额 + startIndex += length; + byte[] flatAmountByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String flatAmount = YKCUtils.convertDecimalPoint(flatAmountByteArr, 4); + + // 谷单价 精确到小数点后五位(谷电费+谷 服务费) + startIndex += length; + byte[] valleyPriceByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String valleyPrice = YKCUtils.convertDecimalPoint(valleyPriceByteArr, 5); + + // 谷电量 + startIndex += length; + byte[] valleyUsedElectricityByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String valleyUsedElectricity = YKCUtils.convertDecimalPoint(valleyUsedElectricityByteArr, 4); + + // 计损谷电量 + startIndex += length; + byte[] valleyPlanLossElectricityByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String valleyPlanLossElectricity = YKCUtils.convertDecimalPoint(valleyPlanLossElectricityByteArr, 4); + + // 谷金额 + startIndex += length; + byte[] valleyAmountByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String valleyAmount = YKCUtils.convertDecimalPoint(valleyAmountByteArr, 4); + + // 电表总起值 + startIndex += length; + length = 5; + byte[] ammeterTotalStartByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String ammeterTotalStart = YKCUtils.convertDecimalPoint(ammeterTotalStartByteArr, 4); + + // 电表总止值 + startIndex += length; + byte[] ammeterTotalEndByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String ammeterTotalEnd = YKCUtils.convertDecimalPoint(ammeterTotalEndByteArr, 4); + + // 总电量 + startIndex += length; + length = 4; + byte[] totalElectricityByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String totalElectricity = YKCUtils.convertDecimalPoint(totalElectricityByteArr, 4); + + // 计损总电量 + startIndex += length; + byte[] planLossTotalElectricityByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String planLossTotalElectricity = YKCUtils.convertDecimalPoint(planLossTotalElectricityByteArr, 4); + + // 消费金额 精确到小数点后四位,包含电费、 服务费 + startIndex += length; + byte[] consumptionAmountByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String consumptionAmount = YKCUtils.convertDecimalPoint(consumptionAmountByteArr, 4); + + // VIN 码 VIN 码,此处 VIN 码和充电时 VIN 码不同, 正序直接上传, 无需补 0 和反序 + startIndex += length; + length = 17; + byte[] vinCodeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String vinCode = BytesUtil.ascii2Str(vinCodeByteArr); + + /** + * 交易标识 + * 0x01: app 启动 + * 0x02:卡启动 + * 0x04:离线卡启动 + * 0x05: vin 码启动充电 + */ + startIndex += length; + length = 1; + byte[] transactionIdentifierByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String transactionIdentifier = BytesUtil.bcd2Str(transactionIdentifierByteArr); + + // 交易时间 CP56Time2a 格式 + startIndex += length; + length = 7; + byte[] transactionTimeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + Date transactionDate = Cp56Time2aUtil.byte2Hdate(transactionTimeByteArr); + String transactionTime = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, transactionDate); + + // 停止原因 + startIndex += length; + length = 1; + byte[] stopReasonByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String stopReason = BytesUtil.bin2HexStr(stopReasonByteArr); + String stopReasonMsg = YKCChargingStopReasonEnum.getMsgByCode(Integer.parseInt(stopReason, 16)); + + // 物理卡号 不足 8 位补 0 + startIndex += length; + length = 8; + byte[] cardNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + byte[] logicCardNum = BytesUtil.checkLengthAndBehindAppendZero(cardNumByteArr, 16); + String logicCard = BytesUtil.binary(logicCardNum, 10); + + log.info("[===交易记录===]交易流水号:{}, 桩编号:{}, 枪号:{}, 开始时间:{}, 结束时间:{}, 尖单价:{}, 尖电量:{}, 计损尖电量:{}, 尖金额:{}, " + + "峰单价:{}, 峰电量:{}, 计损峰电量:{}, 峰金额:{}, 平单价:{}, 平电量:{}, 计损平电量:{}, 平金额:{}, " + + "谷单价:{}, 谷电量:{}, 计损谷电量:{}, 谷金额:{}, 电表总起值:{}, 电表总止值:{}, 总电量:{}, 计损总电量:{}, 消费金额:{}, " + + "电动汽车唯一标识:{}, 交易标识:{}, 交易日期、时间:{}, 停止原因码:{}, 停止原因描述:{}, 物理卡号:{}", + orderCode, pileSn, connectorCode, startTime, endTime, sharpPrice, sharpUsedElectricity, sharpPlanLossElectricity, sharpAmount, + peakPrice, peakUsedElectricity, peakPlanLossElectricity, peakAmount, flatPrice, flatUsedElectricity, flatPlanLossElectricity, flatAmount, + valleyPrice, valleyUsedElectricity, valleyPlanLossElectricity, valleyAmount, ammeterTotalStart, ammeterTotalEnd, totalElectricity, planLossTotalElectricity, + consumptionAmount, vinCode, transactionIdentifier, transactionTime, stopReason, stopReasonMsg, logicCard); + + // 交易记录封装到对象里 + TransactionRecordsData data = TransactionRecordsData.builder() + .orderCode(orderCode) + .pileSn(pileSn) + .connectorCode(connectorCode) + .startTime(startTime) + .endTime(endTime) + .sharpPrice(sharpPrice) + .sharpUsedElectricity(sharpUsedElectricity) + .sharpPlanLossElectricity(sharpPlanLossElectricity) + .sharpAmount(sharpAmount) + .peakPrice(peakPrice) + .peakUsedElectricity(peakUsedElectricity) + .peakPlanLossElectricity(peakPlanLossElectricity) + .peakAmount(peakAmount) + .flatPrice(flatPrice) + .flatUsedElectricity(flatUsedElectricity) + .flatPlanLossElectricity(flatPlanLossElectricity) + .flatAmount(flatAmount) + .valleyPrice(valleyPrice) + .valleyUsedElectricity(valleyUsedElectricity) + .valleyPlanLossElectricity(valleyPlanLossElectricity) + .valleyAmount(valleyAmount) + .ammeterTotalStart(ammeterTotalStart) + .ammeterTotalEnd(ammeterTotalEnd) + .totalElectricity(totalElectricity) + .planLossTotalElectricity(planLossTotalElectricity) + .consumptionAmount(consumptionAmount) + .vinCode(vinCode) + .transactionIdentifier(transactionIdentifier) + .transactionTime(transactionTime) + .stopReasonMsg(stopReasonMsg) + .logicCard(logicCard) + .build(); + + // 保存报文 + String jsonMsg = JSONObject.toJSONString(data); + pileMsgRecordService.save(pileSn, pileSn + connectorCode, type, jsonMsg, ykcDataProtocol.getHEXString()); + + // 处理订单加锁 + String lockKey = "settle_order_" + orderCode; + String uuid = IdUtils.fastUUID(); + try { + // redis锁 + Boolean isLock = redisCache.lock(lockKey, uuid, 1500); + if (isLock) { + processOrder(data); + } + } catch (Exception e) { + log.error("处理订单发生异常", e); + } finally { + if (uuid.equals(redisCache.getCacheObject(lockKey).toString())) { + redisCache.unLock(lockKey); + } + } + + // TODO 将开始时间和结束时间存入订单表中 + // OrderBasicInfo orderBasicInfo = OrderBasicInfo.builder() + // .orderCode(orderCode) + // .chargeStartTime() + // .chargeEndTime() + // .build() + + + /* + 应答 + 确认结果 0x00 上传成功 0x01 非法账单 + 2022年12月15日11点28分发现返回 01非法账单,充电桩会持续上传交易记录,后面产生的交易记录被阻塞 + */ + byte[] confirmResultBytes = Constants.zeroByteArray; + byte[] concatMsgBody = Bytes.concat(orderCodeByteArr, confirmResultBytes); + + return getResult(ykcDataProtocol, concatMsgBody); + } + + private void processOrder(TransactionRecordsData data) { + String orderCode = data.getOrderCode(); + // 根据订单号查询订单信息 + OrderBasicInfo orderBasicInfo = orderBasicInfoService.getOrderInfoByOrderCode(orderCode); + if (orderBasicInfo != null) { + // 平台存在订单 + orderBasicInfo.setReason(data.getStopReasonMsg()); + // 如果订单状态为 异常,则改为 待结算 + if (StringUtils.equals(OrderStatusEnum.ABNORMAL.getValue(), orderBasicInfo.getOrderStatus())) { + orderBasicInfo.setOrderStatus(OrderStatusEnum.STAY_SETTLEMENT.getValue()); + } + + // 校验一下开始时间和结束时间,防止充电中桩离线,时间不准确 + if (Objects.isNull(orderBasicInfo.getChargeStartTime())) { // 开始时间 + orderBasicInfo.setChargeStartTime(DateUtils.parseDate(data.getStartTime())); + } + if (Objects.isNull(orderBasicInfo.getChargeEndTime())) { // 结束时间 + orderBasicInfo.setChargeEndTime(DateUtils.parseDate(data.getEndTime())); + } + + orderBasicInfoService.updateOrderBasicInfo(orderBasicInfo); + + // 结算订单操作 + try { + orderBasicInfoService.settleOrder(data, orderBasicInfo); + } catch (Exception e) { + log.error("结算订单发生异常", e); + } + } else { + // 平台没有查到订单 + orderBasicInfoService.saveAbnormalOrder(data); + log.warn("充电桩传来的交易记录,根据订单号:{}查询不到订单,判定为非法账单", orderCode); + } + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/UploadRealTimeMonitorHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/UploadRealTimeMonitorHandler.java new file mode 100644 index 000000000..5e7cc98a3 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/UploadRealTimeMonitorHandler.java @@ -0,0 +1,267 @@ +package com.jsowell.netty.handler; + +import com.alibaba.fastjson2.JSONObject; +import com.jsowell.common.core.domain.ykc.RealTimeMonitorData; +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.enums.ykc.OrderStatusEnum; +import com.jsowell.common.enums.ykc.YKCPileFaultReasonEnum; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import com.jsowell.pile.domain.OrderBasicInfo; +import com.jsowell.pile.service.IOrderBasicInfoService; +import com.jsowell.pile.service.IPileBasicInfoService; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.Objects; + +/** + * 获取桩上传的实时监测数据 + * + * @author JS-ZZA + * @date 2022/9/19 9:08 + */ +@Slf4j +@Component +public class UploadRealTimeMonitorHandler extends AbstractHandler { + private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.UPLOAD_REAL_TIME_MONITOR_DATA_CODE.getBytes()); + private final String oldVersionType = YKCUtils.frameType2Str(YKCFrameTypeCode.UPLOAD_REAL_TIME_MONITOR_DATA_OLD_VERSION_CODE.getBytes()); + + @Override + public void afterPropertiesSet() throws Exception { + YKCOperateFactory.register(type, this); + YKCOperateFactory.register(oldVersionType, this); + } + + @Autowired + private IPileBasicInfoService pileBasicInfoService; + + @Autowired + private IOrderBasicInfoService orderBasicInfoService; + + @Override + public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) { + log.info("[===获取桩上传的实时监测数据===] param:{}, channel:{}", JSONObject.toJSONString(ykcDataProtocol), channel.toString()); + RealTimeMonitorData realTimeMonitorData = new RealTimeMonitorData(); + + // 获取消息体 + byte[] msgBody = ykcDataProtocol.getMsgBody(); + // log.info("上传实时数据msgBody:{}", BytesUtil.bcd2Str(msgBody)); + int startIndex = 0; + int length = 16; + + // 交易流水号 + byte[] orderCodeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String orderCode = BytesUtil.bcd2Str(orderCodeByteArr); + realTimeMonitorData.setOrderCode(orderCode); + + // 桩编码 + startIndex += length; + length = 7; + byte[] pileSnByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String pileSn = BytesUtil.bcd2Str(pileSnByteArr); + realTimeMonitorData.setPileSn(pileSn); + + // 保存时间 + saveLastTime(pileSn); + + // 枪号 + startIndex += length; + length = 1; + byte[] pileConnectorCodeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String connectorCode = BytesUtil.bcd2Str(pileConnectorCodeByteArr); + realTimeMonitorData.setConnectorCode(connectorCode); + + // 枪口状态 0x00:离线 0x01:故障 0x02:空闲 0x03:充电 + startIndex += length; + length = 1; + byte[] connectorStatusByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String connectorStatus = BytesUtil.bcd2Str(connectorStatusByteArr); + realTimeMonitorData.setConnectorStatus(connectorStatus); + + // 是否归位 0x00:否 0x01:是 0x02:未知(无法检测到枪是否插回枪座即 未知) + startIndex += length; + length = 1; + byte[] homingFlagByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String homingFlag = BytesUtil.bcd2Str(homingFlagByteArr); + realTimeMonitorData.setHomingFlag(homingFlag); + + // 是否插枪 0x00:否 0x01:是 + startIndex += length; + length = 1; + byte[] putGunTypeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String putGunType = BytesUtil.bcd2Str(putGunTypeByteArr); + realTimeMonitorData.setPutGunType(putGunType); + + // 输出电压 精确到小数点后一位;待机置零 + startIndex += length; + length = 2; + byte[] outputVoltageByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String outputVoltage = YKCUtils.convertVoltageCurrent(outputVoltageByteArr); + realTimeMonitorData.setOutputVoltage(outputVoltage); + + // 输出电流 精确到小数点后一位;待机置零 + startIndex += length; + length = 2; + byte[] outputCurrentByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String outputCurrent = YKCUtils.convertVoltageCurrent(outputCurrentByteArr); + realTimeMonitorData.setOutputCurrent(outputCurrent); + + // 枪线温度 整形, 偏移量-50;待机置零 + startIndex += length; + length = 1; + byte[] gunLineTemperatureByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String gunLineTemperature = String.valueOf(gunLineTemperatureByteArr[0]); + realTimeMonitorData.setGunLineTemperature(gunLineTemperature); + + // 枪线编码 没有置零 + startIndex += length; + length = 8; + byte[] gunLineCodeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String gunLineCode = BytesUtil.bcd2Str(gunLineCodeByteArr); + realTimeMonitorData.setGunLineCode(gunLineCode); + + // SOC 待机置零;交流桩置零 + startIndex += length; + length = 1; + byte[] SOCByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String SOC = String.valueOf(SOCByteArr[0]); + realTimeMonitorData.setSOC(SOC); + + // 电池组最高温度 整形, 偏移量-50 ºC;待机置零; 交流桩置零 + startIndex += length; + length = 1; + byte[] batteryMaxTemperatureByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String batteryMaxTemperature = String.valueOf(batteryMaxTemperatureByteArr[0]); + realTimeMonitorData.setBatteryMaxTemperature(batteryMaxTemperature); + + // 累计充电时间 单位: min;待机置零 + startIndex += length; + length = 2; + byte[] sumChargingTimeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + int sumChargingTime = BytesUtil.bytesToIntLittle(sumChargingTimeByteArr); + realTimeMonitorData.setSumChargingTime(String.valueOf(sumChargingTime)); + + // 剩余时间 单位: min;待机置零、交流桩置零 + startIndex += length; + length = 2; + byte[] timeRemainingByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + int timeRemaining = BytesUtil.bytesToIntLittle(timeRemainingByteArr); + realTimeMonitorData.setTimeRemaining(String.valueOf(timeRemaining)); + + // 充电度数 精确到小数点后四位;待机置零 + startIndex += length; + length = 4; + byte[] chargingDegreeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String chargingDegree = YKCUtils.convertDecimalPoint(chargingDegreeByteArr, 4); + realTimeMonitorData.setChargingDegree(chargingDegree); + + // 计损充电度数 精确到小数点后四位;待机置零 未设置计损比例时等于充电度数 + startIndex += length; + length = 4; + byte[] lossDegreeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String lossDegree = YKCUtils.convertDecimalPoint(lossDegreeByteArr, 4); + realTimeMonitorData.setLossDegree(lossDegree); + + // 已充金额 精确到小数点后四位;待机置零 (电费+服务费) *计损充电度数 + startIndex += length; + length = 4; + byte[] chargingAmountByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String chargingAmount = YKCUtils.convertDecimalPoint(chargingAmountByteArr, 4); + realTimeMonitorData.setChargingAmount(chargingAmount); + + /** + * 硬件故障 + * + * Bit 位表示(0 否 1 是), 低位到高位顺序 + * Bit1:急停按钮动作故障; + * Bit2:无可用整流模块; + * Bit3:出风口温度过高; + * Bit4:交流防雷故障; + * Bit5:交直流模块 DC20 通信中断; + * Bit6:绝缘检测模块 FC08 通信中断; + * Bit7:电度表通信中断; + * Bit8:读卡器通信中断; + * Bit9: RC10 通信中断; + * Bit10:风扇调速板故障; + * Bit11:直流熔断器故障; + * Bit12:高压接触器故障; + * Bit13:门打开; + */ + startIndex += length; + length = 2; + byte[] hardwareFaultTempByteArr = BytesUtil.copyBytes(msgBody, startIndex, length); + String hardwareFaultTemp = BytesUtil.bcd2Str(hardwareFaultTempByteArr); + String faultReason = "无"; + + if (!StringUtils.equals(hardwareFaultTemp, "0000")) { + // 不等于0000说明有故障 + StringBuffer sb = new StringBuffer(hardwareFaultTemp); + String lowOrder = sb.substring(0, 2); + String highOrder = sb.substring(2, 4); + + // String hardwareFault = highOrder + lowOrder; + byte[] hardwareFaultByteArr = BytesUtil.str2Bcd(highOrder + lowOrder); + String binStr = BytesUtil.bytes2BinStr(hardwareFaultByteArr); + // log.info("binStr:{}", binStr); // 0000 0000 0000 0001 + int faultCode = 0; + for (int i = 0; i < binStr.length(); i++) { + if (binStr.charAt(i) == '1') { + faultCode = 15 - i; + break; + } + } + faultReason = YKCPileFaultReasonEnum.getValueByCode(faultCode); + // log.info("故障码:{}, 故障原因:{}", faultCode, faultReason); + } + realTimeMonitorData.setHardwareFault(hardwareFaultTemp); + + if (!StringUtils.equals(connectorStatus, "02")) { + log.info("0x13上传实时监测数据==交易流水号:{}, 桩编号:{}, 枪号:{}, 状态:{}, 枪是否归位:{}, 是否插枪:{}, 输出电压:{}, 输出电流:{}, 枪线温度:{}, " + + "枪线编码:{}, SOC:{}, 电池组最高温度:{}, 累计充电时间:{}, 剩余时间:{}, 充电度数:{}, 记损充电度数:{}, 已充金额:{}, " + + "硬件故障:{}, 故障码转换结果:{}", orderCode, pileSn, connectorCode, connectorStatus, homingFlag, putGunType, outputVoltage, + outputCurrent, gunLineTemperature, gunLineCode, SOC, batteryMaxTemperature, sumChargingTime, timeRemaining, + chargingDegree, lossDegree, chargingAmount, hardwareFaultTemp, faultReason + ); + } + + // 公共方法修改状态 + pileBasicInfoService.updateStatus(BytesUtil.bcd2Str(ykcDataProtocol.getFrameType()), pileSn, connectorCode, connectorStatus, putGunType); + + // 03表示充电中 + if (StringUtils.equals(connectorStatus, "03")) { + // 充电时保存实时数据到redis + pileBasicInfoService.saveRealTimeMonitorData2Redis(realTimeMonitorData); + + // 查询数据库中该订单当前信息 + OrderBasicInfo orderInfo = orderBasicInfoService.getOrderInfoByOrderCode(orderCode); + if (Objects.nonNull(orderInfo)) { + boolean updateFlag = false; + if (StringUtils.equals(orderInfo.getOrderStatus(), OrderStatusEnum.NOT_START.getValue()) + || StringUtils.equals(orderInfo.getOrderStatus(), OrderStatusEnum.ABNORMAL.getValue())) { + updateFlag = true; + // 如果是未启动状态或者异常状态, 修改这个订单状态为充电中 + orderInfo.setOrderStatus(OrderStatusEnum.IN_THE_CHARGING.getValue()); + } + + // 如果原来没有开始充电时间就保存当前时间为开始充电时间 + if (orderInfo.getChargeStartTime() == null) { + updateFlag = true; + orderInfo.setChargeStartTime(new Date()); + } + + if (updateFlag) { + orderBasicInfoService.updateOrderBasicInfo(orderInfo); + } + } + } + return null; + } + +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/server/yunkuaichong/NettyServer.java b/jsowell-netty/src/main/java/com/jsowell/netty/server/yunkuaichong/NettyServer.java new file mode 100644 index 000000000..15305e30b --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/server/yunkuaichong/NettyServer.java @@ -0,0 +1,77 @@ +package com.jsowell.netty.server.yunkuaichong; + +import com.jsowell.common.constant.Constants; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.net.InetSocketAddress; + +@Slf4j +@Component +public class NettyServer implements CommandLineRunner { + @Resource + private NettyServerChannelInitializer nettyServerChannelInitializer; + + @Order(value = 1) + @Override + public void run(String... args) throws Exception { + InetSocketAddress address = new InetSocketAddress(Constants.SOCKET_IP, Constants.SOCKET_PORT); + this.start(address); + } + + public void start(InetSocketAddress address) { + // log.info("========NettyServer.start order 1"); + //配置服务端的NIO线程组 + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + + try { + ServerBootstrap bootstrap = new ServerBootstrap() // //启动NIO服务的辅助启动类 + .group(bossGroup, workerGroup) // 绑定线程池 + .channel(NioServerSocketChannel.class) // 启动服务时, 通过反射创建一个NioServerSocketChannel对象 + + /* + ===> 服务器初始化时执行, 属于AbstracBootstrap的方法 + */ + .handler(new LoggingHandler(LogLevel.DEBUG)) // handler在初始化时就会执行,可以设置打印日志级别 + // 设置tcp缓冲区, 可连接队列大小 + .option(ChannelOption.SO_BACKLOG, 128) //服务端接受连接的队列长度,如果队列已满,客户端连接将被拒绝 + .option(ChannelOption.SO_REUSEADDR, true) //允许重复使用本地地址和端口 + + /* + ===> 客户端连接成功之后执行, 属于ServerBootstrap的方法,继承自AbstractBootstrap + */ + .childOption(ChannelOption.SO_KEEPALIVE, true) //两小时没有数据通信时, 启用心跳保活机制探测客户端是否连接有效 + .childOption(ChannelOption.SO_REUSEADDR, true) + .childHandler(nettyServerChannelInitializer)//编码解码 + + // 地址 + .localAddress(address); + + // 绑定端口,开始接收进来的连接 + ChannelFuture future = bootstrap.bind(address.getPort()).sync(); + if (future.isSuccess()) { + log.info("NettyServer启动成功, 开始监听端口:{}", address.getPort()); + } else { + log.error("NettyServer启动失败", future.cause()); + } + //关闭channel和块,直到它被关闭 + future.channel().closeFuture().sync(); + } catch (Exception e) { + log.error("NettyServer.start error", e); + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/server/yunkuaichong/NettyServerChannelInitializer.java b/jsowell-netty/src/main/java/com/jsowell/netty/server/yunkuaichong/NettyServerChannelInitializer.java new file mode 100644 index 000000000..ad6efd175 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/server/yunkuaichong/NettyServerChannelInitializer.java @@ -0,0 +1,32 @@ +package com.jsowell.netty.server.yunkuaichong; + +import com.jsowell.netty.decoder.StartAndLengthFieldFrameDecoder; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.bytes.ByteArrayDecoder; +import io.netty.handler.timeout.IdleStateHandler; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.concurrent.TimeUnit; + +@Component +public class NettyServerChannelInitializer extends ChannelInitializer { + + @Resource + NettyServerHandler nettyServerHandler; + + @Override + protected void initChannel(SocketChannel channel) throws Exception { + ChannelPipeline pipeline = channel.pipeline(); + // pipeline.addLast("decoder",new CustomDecoder()); + pipeline.addLast("frameDecoder", new StartAndLengthFieldFrameDecoder(0x68)); + pipeline.addLast("decoder", new ByteArrayDecoder()); + pipeline.addLast("encoder", new ByteArrayDecoder()); + //读超时时间设置为10s,0表示不监控 + pipeline.addLast(new IdleStateHandler(60, 0, 0, TimeUnit.SECONDS)); + pipeline.addLast("handler", nettyServerHandler); + } + +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/server/yunkuaichong/NettyServerHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/server/yunkuaichong/NettyServerHandler.java new file mode 100644 index 000000000..c02e23769 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/server/yunkuaichong/NettyServerHandler.java @@ -0,0 +1,203 @@ +package com.jsowell.netty.server.yunkuaichong; + +import com.google.common.collect.Lists; +import com.jsowell.common.enums.ykc.PileChannelEntity; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.service.yunkuaichong.YKCBusinessService; +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelId; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.timeout.IdleState; +import io.netty.handler.timeout.IdleStateEvent; +import io.netty.handler.timeout.ReadTimeoutException; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.xml.bind.DatatypeConverter; +import java.net.InetSocketAddress; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * netty服务端处理类 + */ +@ChannelHandler.Sharable +@Slf4j +@Component +public class NettyServerHandler extends ChannelInboundHandlerAdapter { + + @Autowired + private YKCBusinessService ykcService; + + /** + * 管理一个全局map,保存连接进服务端的通道数量 + */ + private static final ConcurrentHashMap CHANNEL_MAP = new ConcurrentHashMap<>(); + + private final List notPrintFrameTypeList = Lists.newArrayList("0x03"); + + /** + * 有客户端连接服务器会触发此函数 + * 连接被建立并且准备进行通信时被调用 + */ + @Override + public void channelActive(ChannelHandlerContext ctx) { + InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress(); + String clientIp = insocket.getAddress().getHostAddress(); + int clientPort = insocket.getPort(); + //获取连接通道唯一标识 + ChannelId channelId = ctx.channel().id(); + //如果map中不包含此连接,就保存连接 + if (CHANNEL_MAP.containsKey(channelId)) { + log.info("客户端【{}】是连接状态,连接通道数量: {}", channelId, CHANNEL_MAP.size()); + } else { + //保存连接 + CHANNEL_MAP.put(channelId, ctx); + log.info("客户端【{}】, 连接netty服务器[IP:{}--->PORT:{}], 连接通道数量: {}", channelId, clientIp, clientPort, CHANNEL_MAP.size()); + } + } + + /** + * 有客户端发消息会触发此函数 + */ + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + // log.info("加载客户端报文=== channelId:" + ctx.channel().id() + ", msg:" + msg); + // 下面可以解析数据,保存数据,生成返回报文,将需要返回报文写入write函数 + byte[] arr = (byte[]) msg; + // 获取帧类型 + String frameType = YKCUtils.frameType2Str(BytesUtil.copyBytes(arr, 5, 1)); + // 获取序列号域 + int serialNumber = BytesUtil.bytesToIntLittle(BytesUtil.copyBytes(arr, 2, 2)); + + // new + String hexString = DatatypeConverter.printHexBinary(arr); + + // 心跳包0x03日志太多,造成日志文件过大,改为不打印 + if (!CollectionUtils.containsAny(notPrintFrameTypeList, frameType)) { + log.info("【<<<<<平台收到消息<<<<<】channel:{}, 帧类型:{}, 序列号域:{}, 报文:{}, new报文:{}, msg:{}", + ctx.channel().id(), frameType, serialNumber, BytesUtil.binary(arr, 16), hexString, Arrays.toString(arr)); + } + + // 处理数据 + byte[] result = ykcService.process(arr, ctx.channel()); + if (Objects.nonNull(result)) { + // 响应客户端 + ByteBuf buffer = ctx.alloc().buffer().writeBytes(result); + this.channelWrite(ctx.channel().id(), buffer); + if (!CollectionUtils.containsAny(notPrintFrameTypeList, frameType)) { + log.info("【>>>>>平台响应消息>>>>>】:{}", BytesUtil.binary(result, 16)); + } + } + } + + /** + * 有客户端终止连接服务器会触发此函数 + */ + @Override + public void channelInactive(ChannelHandlerContext ctx) { + InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress(); + String clientIp = insocket.getAddress().getHostAddress(); + ChannelId channelId = ctx.channel().id(); + //包含此客户端才去删除 + if (CHANNEL_MAP.containsKey(channelId)) { + ykcService.exit(channelId); + //删除连接 + CHANNEL_MAP.remove(channelId); + log.info("客户端【{}】, 退出netty服务器[IP:{}--->PORT:{}], 连接通道数量: {}", channelId, clientIp, insocket.getPort(), CHANNEL_MAP.size()); + } + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { // (2) + // Channel incoming = ctx.channel(); + // log.info("handlerAdded: handler被添加到channel的pipeline connect:" + incoming.remoteAddress()); + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { // (3) + // Channel incoming = ctx.channel(); + // log.info("handlerRemoved: handler从channel的pipeline中移除 connect:" + incoming.remoteAddress()); + // ChannelMapByEntity.removeChannel(incoming); + // ChannelMap.removeChannel(incoming); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + Channel channel = ctx.channel(); + // log.info("channel:【{}】读数据完成", channel.id()); + super.channelReadComplete(ctx); + } + + /** + * 服务端给客户端发送消息 + * + * @param channelId 连接通道唯一id + * @param msg 需要发送的消息内容 + */ + public void channelWrite(ChannelId channelId, Object msg) throws Exception { + ChannelHandlerContext ctx = CHANNEL_MAP.get(channelId); + if (ctx == null) { + log.info("通道【{}】不存在", channelId); + return; + } + if (msg == null || msg == "") { + log.info("服务端响应空的消息"); + return; + } + //将客户端的信息直接返回写入ctx + ctx.write(msg); + //刷新缓存区 + ctx.flush(); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + String socketString = ctx.channel().remoteAddress().toString(); + ChannelId channelId = ctx.channel().id(); + String pileSn = PileChannelEntity.getPileSnByChannelId(channelId.asLongText()); + if (evt instanceof IdleStateEvent) { // 超时事件 + IdleStateEvent event = (IdleStateEvent) evt; + boolean flag = false; + if (event.state() == IdleState.READER_IDLE) { // 读 + flag = true; + log.info("Client-IP:【{}】, channelId:【{}】, pileSn:【{}】, READER_IDLE 读超时", socketString, channelId, pileSn); + } else if (event.state() == IdleState.WRITER_IDLE) { // 写 + flag = true; + log.info("Client-IP:【{}】, channelId:【{}】, pileSn:【{}】, WRITER_IDLE 写超时", socketString, channelId, pileSn); + } else if (event.state() == IdleState.ALL_IDLE) { // 全部 + flag = true; + log.info("Client-IP:【{}】, channelId:【{}】, pileSn:【{}】, ALL_IDLE 总超时", socketString, channelId, pileSn); + } + // if (flag) { + // ctx.channel().close(); + // } + } + } + + /** + * 发生异常会触发此函数 + */ + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + ChannelId channelId = ctx.channel().id(); + log.error("发生异常 channelId:{}", channelId.asShortText(), cause); + cause.printStackTrace(); + // 如果桩连到平台,在1分钟内没有发送数据过来,会报ReadTimeoutException异常 + if (cause instanceof ReadTimeoutException) { + if (log.isTraceEnabled()) { + log.trace("Connection timeout 【{}】", ctx.channel().remoteAddress()); + } + log.info("【{}】发生了错误, 此连接被关闭, 此时连通数量: {}", channelId, CHANNEL_MAP.size()); + ctx.channel().close(); + } + } +} \ No newline at end of file diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/service/yunkuaichong/YKCBusinessService.java b/jsowell-netty/src/main/java/com/jsowell/netty/service/yunkuaichong/YKCBusinessService.java new file mode 100644 index 000000000..f3d8102dd --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/service/yunkuaichong/YKCBusinessService.java @@ -0,0 +1,25 @@ +package com.jsowell.netty.service.yunkuaichong; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelId; + +/** + * 云快充处理service + */ + +public interface YKCBusinessService { + /** + * 处理桩发来的请求 + * 不需要应答的返回null + * @param msg 请求报文 + * @param channel 通道信息 + * @return 结果 + */ + byte[] process(byte[] msg, Channel channel); + + /** + * 桩退出 + * @param channelId channelId + */ + void exit(ChannelId channelId); +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/service/yunkuaichong/YKCPushCommandService.java b/jsowell-netty/src/main/java/com/jsowell/netty/service/yunkuaichong/YKCPushCommandService.java new file mode 100644 index 000000000..6d59df3c2 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/service/yunkuaichong/YKCPushCommandService.java @@ -0,0 +1,64 @@ +package com.jsowell.netty.service.yunkuaichong; + +import com.jsowell.netty.command.ykc.GetRealTimeMonitorDataCommand; +import com.jsowell.netty.command.ykc.IssueQRCodeCommand; +import com.jsowell.netty.command.ykc.ProofreadTimeCommand; +import com.jsowell.netty.command.ykc.PublishPileBillingTemplateCommand; +import com.jsowell.netty.command.ykc.RebootCommand; +import com.jsowell.netty.command.ykc.StartChargingCommand; +import com.jsowell.netty.command.ykc.StopChargingCommand; +import com.jsowell.netty.command.ykc.UpdateFileCommand; + +/** + * 云快充协议,向充电桩发送命令service + */ +public interface YKCPushCommandService { + + /** + * 发送启动充电指令 + * @param startChargingCommand + */ + void pushStartChargingCommand(StartChargingCommand startChargingCommand); + + /** + * 发送停止充电指令 + * @param stopChargingCommand + */ + void pushStopChargingCommand(StopChargingCommand stopChargingCommand); + + /** + * 读取实时监测数据命令 + * @param command + */ + void pushGetRealTimeMonitorDataCommand(GetRealTimeMonitorDataCommand command); + + /** + * 发送重启指令 + * @param command + */ + void pushRebootCommand(RebootCommand command); + + /** + * 发送下发二维码命令 + * @param command + */ + void pushIssueQRCodeCommand(IssueQRCodeCommand command); + + /** + * 发送对时命令 + * @param command + */ + void pushProofreadTimeCommand(ProofreadTimeCommand command); + + /** + * 下发计费模板命令 + * @param command + */ + void pushPublishPileBillingTemplate(PublishPileBillingTemplateCommand command); + + /** + * 发送远程更新命令 + * @param command + */ + void pushUpdateFileCommand(UpdateFileCommand command); +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/service/yunkuaichong/impl/YKCBusinessServiceImpl.java b/jsowell-netty/src/main/java/com/jsowell/netty/service/yunkuaichong/impl/YKCBusinessServiceImpl.java new file mode 100644 index 000000000..26e41e063 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/service/yunkuaichong/impl/YKCBusinessServiceImpl.java @@ -0,0 +1,100 @@ +package com.jsowell.netty.service.yunkuaichong.impl; + +import com.jsowell.common.core.domain.ykc.YKCDataProtocol; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.enums.ykc.OrderStatusEnum; +import com.jsowell.common.enums.ykc.PileChannelEntity; +import com.jsowell.common.enums.ykc.PileConnectorDataBaseStatusEnum; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.factory.YKCOperateFactory; +import com.jsowell.netty.handler.AbstractHandler; +import com.jsowell.netty.service.yunkuaichong.YKCBusinessService; +import com.jsowell.netty.service.yunkuaichong.YKCPushCommandService; +import com.jsowell.pile.domain.OrderBasicInfo; +import com.jsowell.pile.service.IOrderBasicInfoService; +import com.jsowell.pile.service.IPileConnectorInfoService; +import com.jsowell.pile.service.IPileMsgRecordService; +import com.jsowell.pile.vo.web.OrderListVO; +import io.netty.channel.Channel; +import io.netty.channel.ChannelId; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service +public class YKCBusinessServiceImpl implements YKCBusinessService { + + @Autowired + private IPileMsgRecordService pileMsgRecordService; + + @Autowired + private IPileConnectorInfoService pileConnectorInfoService; + + @Autowired + private IOrderBasicInfoService orderBasicInfoService; + + @Autowired + private YKCPushCommandService ykcPushCommandService; + + @Override + public byte[] process(byte[] msg, Channel channel) { + if (!YKCUtils.checkMsg(msg)) { + // 校验不通过,丢弃消息 + return null; + } + YKCDataProtocol ykcDataProtocol = new YKCDataProtocol(msg); + // 获取帧类型 + String frameType = YKCUtils.frameType2Str(ykcDataProtocol.getFrameType()); + // 获取业务处理handler + AbstractHandler invokeStrategy = YKCOperateFactory.getInvokeStrategy(frameType); + return invokeStrategy.supplyProcess(ykcDataProtocol, channel); + } + + @Override + public void exit(ChannelId channelId) { + // 获取桩编号 + String pileSn = PileChannelEntity.getPileSnByChannelId(channelId.asLongText()); + if (StringUtils.isBlank(pileSn)) { + return; + } + log.info("充电桩退出:{}, channelId:{}", pileSn, PileChannelEntity.getChannelByPileSn(pileSn).id()); + + // 充电桩断开连接,所有枪口都设置为【离线】 + pileConnectorInfoService.updateConnectorStatusByPileSn(pileSn, PileConnectorDataBaseStatusEnum.OFF_NETWORK.getValue()); + + // 将此桩正在进行充电的订单状态改为 异常 + List orderListVOS = orderBasicInfoService.selectChargingOrder(pileSn); + if (CollectionUtils.isNotEmpty(orderListVOS)) { + for (OrderListVO orderListVO : orderListVOS) { + if (StringUtils.equals(orderListVO.getOrderStatus(), OrderStatusEnum.IN_THE_CHARGING.getValue())) { + // 修改数据库订单状态 + OrderBasicInfo info = OrderBasicInfo.builder() + .id(Long.parseLong(orderListVO.getId())) + .orderStatus(OrderStatusEnum.ABNORMAL.getValue()) + .build(); + orderBasicInfoService.updateOrderBasicInfo(info); + log.info("充电桩:{}退出, 修改充电桩正在充电的订单状态为异常, orderCode: {}", pileSn, orderListVO.getOrderCode()); + } + } + } + + // 记录充电桩退出msg + // 保存报文 + String type = YKCFrameTypeCode.PILE_LOG_OUT.getCode() + ""; + String jsonMsg = YKCFrameTypeCode.PILE_LOG_OUT.getValue(); + pileMsgRecordService.save(pileSn, pileSn, type, jsonMsg, ""); + + // 自动重启 发送重启指令 + // RebootCommand command = RebootCommand.builder().pileSn(pileSn).build(); + // ykcPushCommandService.pushRebootCommand(command); + + // 删除桩编号和channel的关系 + PileChannelEntity.removeByPileSn(pileSn); + } + +} diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/service/yunkuaichong/impl/YKCPushCommandServiceImpl.java b/jsowell-netty/src/main/java/com/jsowell/netty/service/yunkuaichong/impl/YKCPushCommandServiceImpl.java new file mode 100644 index 000000000..8de5bf249 --- /dev/null +++ b/jsowell-netty/src/main/java/com/jsowell/netty/service/yunkuaichong/impl/YKCPushCommandServiceImpl.java @@ -0,0 +1,341 @@ +package com.jsowell.netty.service.yunkuaichong.impl; + +import com.google.common.collect.Lists; +import com.google.common.primitives.Bytes; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.core.redis.RedisCache; +import com.jsowell.common.enums.ykc.PileChannelEntity; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.CRC16Util; +import com.jsowell.common.util.Cp56Time2a.Cp56Time2aUtil; +import com.jsowell.common.util.DateUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.netty.command.ykc.GetRealTimeMonitorDataCommand; +import com.jsowell.netty.command.ykc.IssueQRCodeCommand; +import com.jsowell.netty.command.ykc.ProofreadTimeCommand; +import com.jsowell.netty.command.ykc.PublishPileBillingTemplateCommand; +import com.jsowell.netty.command.ykc.RebootCommand; +import com.jsowell.netty.command.ykc.StartChargingCommand; +import com.jsowell.netty.command.ykc.StopChargingCommand; +import com.jsowell.netty.command.ykc.UpdateFileCommand; +import com.jsowell.netty.service.yunkuaichong.YKCPushCommandService; +import com.jsowell.pile.service.IPileBasicInfoService; +import com.jsowell.pile.service.IPileBillingTemplateService; +import com.jsowell.pile.service.IPileConnectorInfoService; +import com.jsowell.pile.service.IPileModelInfoService; +import com.jsowell.pile.service.IPileMsgRecordService; +import com.jsowell.pile.vo.web.BillingTemplateVO; +import com.jsowell.pile.vo.web.PileModelInfoVO; +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; +import java.util.Objects; + +@Slf4j +@Service +public class YKCPushCommandServiceImpl implements YKCPushCommandService { + @Autowired + private IPileBillingTemplateService pileBillingTemplateService; + + @Autowired + private IPileModelInfoService pileModelInfoService; + + @Autowired + private IPileBasicInfoService pileBasicInfoService; + + @Autowired + private RedisCache redisCache; + + @Autowired + private IPileMsgRecordService pileMsgRecordService; + + @Autowired + private IPileConnectorInfoService pileConnectorInfoService; + + // 需要记录报文的数据帧类型 + private final List frameTypeList = Lists.newArrayList( + YKCUtils.frameType2Str(YKCFrameTypeCode.REMOTE_RESTART_CODE.getBytes()), + YKCUtils.frameType2Str(YKCFrameTypeCode.REMOTE_CONTROL_START_CODE.getBytes()), + YKCUtils.frameType2Str(YKCFrameTypeCode.REMOTE_STOP_CHARGING_CODE.getBytes()) + ); + + public boolean push(byte[] msg, String pileSn, Enum frameTypeCode) { + // 通过桩编号获取channel + Channel channel = PileChannelEntity.getChannelByPileSn(pileSn); + if (Objects.isNull(channel)) { + log.error("push命令失败, 桩号:{}无法获取到长连接, 请检查充电桩连接状态!", pileSn); + return false; + } + /** + * 拼接报文 + */ + // 起始标志 + byte[] head = new byte[]{0x68}; + + // 序列号域 + byte[] serialNumber = new byte[]{0x00, 0x00}; + + // 加密标志 + byte[] encryptFlag = new byte[]{0x00}; + + // 帧类型标志 + byte[] frameType = new byte[]{(byte) ((YKCFrameTypeCode) frameTypeCode).getCode()}; + + // 序列号域+加密标志+帧类型标志+消息体 + byte[] temp = Bytes.concat(serialNumber, encryptFlag, frameType, msg); + + // 数据长度 + byte[] length = BytesUtil.intToBytes(temp.length, 1); + + // 帧校验域 + byte[] crc = BytesUtil.intToBytes(CRC16Util.calcCrc16(temp)); + // 返回报文 + byte[] writeMsg = Bytes.concat(head, length, temp, crc); + // 返回完整的报文 string类型 + String wholeMsg= BytesUtil.binary(writeMsg, 16); + log.info("[" + channel.remoteAddress() + "] 主动发送push请求信息:{}", wholeMsg); + ByteBuf byteBuf = channel.alloc().buffer().writeBytes(writeMsg); + ChannelFuture channelFuture = channel.writeAndFlush(byteBuf); + channelFuture.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture channelFuture) throws Exception { + // 检查操作的状态 + if (channelFuture.isSuccess()) { + log.info("push结果【成功】, remoteAddress:{}, channelId:{}, 报文:{}, ", channel.remoteAddress(), channel.id(), wholeMsg); + } else { + // 如果发生错误,则访问描述原因的Throwable + Throwable cause = channelFuture.cause(); + cause.printStackTrace(); + log.info("push结果【失败】, remoteAddress:{}, channelId:{}, 报文:{}", channel.remoteAddress(), channel.id(), wholeMsg); + log.error("push发送命令失败", cause); + } + } + }); + + // 保存报文 + String frameTypeStr = YKCUtils.frameType2Str(((YKCFrameTypeCode) frameTypeCode).getBytes()); + if (frameTypeList.contains(frameTypeStr)) { + pileMsgRecordService.save(pileSn, null, frameTypeStr, null, wholeMsg); + } + return true; + } + + /** + * 发送启动充电指令 + */ + @Override + public void pushStartChargingCommand(StartChargingCommand command) { + String pileSn = command.getPileSn(); + String connectorCode = command.getConnectorCode(); + String orderCode = command.getOrderCode(); + if (StringUtils.isEmpty(pileSn) || StringUtils.isEmpty(connectorCode) ) { + log.warn("远程启动充电, 充电桩编号和枪口号不能为空"); + return; + } + if (StringUtils.isEmpty(orderCode)) { + log.warn("远程启动充电, 交易流水号不能为空"); + return; + } + if (command.getChargeAmount() == null || BigDecimal.ZERO.equals(command.getChargeAmount())) { + log.warn("远程启动充电, 充电金额不能为0"); + return; + } + + // 枪口号 + byte[] connectorCodeByteArr = BytesUtil.str2Bcd(connectorCode); + + // 交易流水号 + byte[] orderIdByteArr = BytesUtil.str2Bcd(orderCode); + + // 桩编号 + byte[] pileSnByteArr = BytesUtil.str2Bcd(pileSn); + + // 逻辑卡号 + String logicCardNum = StringUtils.isBlank(command.getLogicCardNum()) + ? Constants.ZERO + : command.getLogicCardNum(); + byte[] logicCardNumByteArr = BytesUtil.checkLengthAndFrontAppendZero(BytesUtil.str2Bcd(logicCardNum), 16); + + // 物理卡号 + String physicsCardNum = StringUtils.isBlank(command.getPhysicsCardNum()) + ? Constants.ZERO + : command.getPhysicsCardNum(); + byte[] physicsCardNumByteArr = BytesUtil.checkLengthAndFrontAppendZero(BytesUtil.str2Bcd(physicsCardNum), 16); + + // 账户余额 + BigDecimal chargeAmount = command.getChargeAmount(); + byte[] accountBalanceByteArr = YKCUtils.getPriceByte(chargeAmount.toString(), 2); + + byte[] msgBody = Bytes.concat(orderIdByteArr, pileSnByteArr, connectorCodeByteArr, logicCardNumByteArr, physicsCardNumByteArr, accountBalanceByteArr); + this.push(msgBody, pileSn, YKCFrameTypeCode.REMOTE_CONTROL_START_CODE); + log.info("【=====平台下发充电指令=====】:订单id:{}, 桩号:{}, 枪口号:{}, 逻辑卡号:{}, 物理卡号:{}, 账户余额:{}", + orderCode, pileSn, BytesUtil.bcd2Str(connectorCodeByteArr), logicCardNum, physicsCardNum, chargeAmount); + } + + @Override + public void pushStopChargingCommand(StopChargingCommand command) { + String pileSn = command.getPileSn(); + String connectorCode = command.getConnectorCode(); + // 远程停机 + byte[] msgBody = Bytes.concat(BytesUtil.str2Bcd(pileSn), BytesUtil.str2Bcd(connectorCode)); + this.push(msgBody, pileSn, YKCFrameTypeCode.REMOTE_STOP_CHARGING_CODE); + log.info("【=====平台下发指令=====】:远程停止充电,桩号:{},枪口号:{}", pileSn, connectorCode); + } + + @Override + public void pushGetRealTimeMonitorDataCommand(GetRealTimeMonitorDataCommand command) { + String pileSn = command.getPileSn(); + String connectorCode = command.getConnectorCode(); + byte[] msg = BytesUtil.str2Bcd(pileSn + connectorCode); + this.push(msg, pileSn, YKCFrameTypeCode.READ_REAL_TIME_MONITOR_DATA_CODE); + log.info("【=====平台下发指令=====】:获取充电桩:{} 的 {} 枪口实时数据信息", pileSn, connectorCode); + } + + @Override + public void pushRebootCommand(RebootCommand command) { + String pileSn = command.getPileSn(); + byte[] msg = BytesUtil.str2Bcd(pileSn + Constants.ZERO_ONE); + log.info("【=====平台下发指令=====】:重启充电桩:,{}", pileSn); + this.push(msg, pileSn, YKCFrameTypeCode.REMOTE_RESTART_CODE); + + } + + // 下发二维码 + @Override + public void pushIssueQRCodeCommand(IssueQRCodeCommand command) { + log.info("异步下发二维码 thread:{}", Thread.currentThread().getName()); + String pileSn = command.getPileSn(); + // 桩编码 + byte[] pileSnByteArr = BytesUtil.str2Bcd(pileSn); + + // 二维码格式 0x00:第一种 前缀+桩编号 0x01:第二种 前缀+桩编号+枪编号 + byte[] qrCodeTypeByteArr = Constants.oneByteArray; + + // 二维码前缀 如:“www.baidu.com?No=” + // String qrCodePrefix = "https://wx.charging.shbochong.cn/prepare_charge?code="; + // String qrCodePrefix = pileBasicInfoService.getPileQrCodeUrl(null); + String qrCodePrefix = pileConnectorInfoService.getPileConnectorQrCodeUrl(null); + byte[] qrCodePrefixByteArr = BytesUtil.str2Asc(qrCodePrefix); + + // 二维码前缀长度 二维码前缀长度长度最大不超过200 字节 + int length = qrCodePrefix.length(); + byte[] qrCodePrefixLengthByteArr = BytesUtil.intToBytes(length, 1); + + // 拼接消息体 + byte[] msg = Bytes.concat(pileSnByteArr, qrCodeTypeByteArr, qrCodePrefixLengthByteArr, qrCodePrefixByteArr); + + // push消息 + boolean result = this.push(msg, pileSn, YKCFrameTypeCode.REMOTE_ISSUE_QRCODE_CODE); + log.info("=====平台下发指令===== :下发二维码,地址为:{}", qrCodePrefix); + } + + /** + * 0x56 对时设置 + * @param command + */ + @Override + public void pushProofreadTimeCommand(ProofreadTimeCommand command) { + log.info("充电桩对时,thread:{}", Thread.currentThread().getName()); + String pileSn = command.getPileSn(); + // Date date = new Date(); + // Date parseDate = DateUtils.parseDate("2023-02-28 16:45:20"); + Date date = DateUtils.parseDate(DateUtils.getTime()); + // 根据不同程序版本获取工具类 + // String programVersion = redisCache.getCacheMapValue(CacheConstants.PILE_PROGRAM_VERSION, pileSn); + // AbsCp56Time2aUtil cp56Time2aUtil = Cp56Time2aFactory.getInvokeStrategy(programVersion); + // String encodeCP56Time2a = cp56Time2aUtil.date2HexStr(date); + byte[] bytes = Cp56Time2aUtil.date2Hbyte(date); + byte[] pileSnByteArr = BytesUtil.str2Bcd(pileSn); + byte[] msg = Bytes.concat(pileSnByteArr, bytes); + + this.push(msg, pileSn, YKCFrameTypeCode.TIME_CHECK_SETTING_CODE); + log.info("[充电桩:{}对时, 时间:{}, CP56Time2a:{}]", pileSn, DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, date), BytesUtil.binary(bytes, 16)); + } + + @Override + public void pushPublishPileBillingTemplate(PublishPileBillingTemplateCommand command) { + BillingTemplateVO billingTemplateVO = command.getBillingTemplateVO(); + String pileSn = command.getPileSn(); + // 转换 + byte[] messageBody = pileBillingTemplateService.generateBillingTemplateMsgBody(pileSn, billingTemplateVO); + // 发送 + if (messageBody != null) { + this.push(messageBody, pileSn, YKCFrameTypeCode.BILLING_TEMPLATE_SETTING_CODE); + } + } + + @Override + public void pushUpdateFileCommand(UpdateFileCommand command) { + List pileSns = command.getPileSnList(); + if (CollectionUtils.isEmpty(pileSns)) { + return; + } + + List list = pileModelInfoService.getPileModelInfoByPileSnList(pileSns); + if (CollectionUtils.isEmpty(list)) { + return; + } + // 获取桩型号 01:直流 02:交流 + byte[] pileModelType; + for (PileModelInfoVO pileModelInfoVO : list) { + byte[] pileSnByteArr = BytesUtil.str2Bcd(pileModelInfoVO.getPileSn()); + + // 数据库 1- 快充(直流) 2-慢充(交流) + /*if (StringUtils.equals(pileModelInfoVO.getSpeedType(), Constants.ONE)) { + pileModelType = Constants.oneByteArray; + } else { + pileModelType = Constants.twoByteArray; + }*/ + pileModelType = Constants.zeroByteArray; + + // 额定功率 + String ratedPower = pileModelInfoVO.getRatedPower(); + int i = Integer.parseInt(ratedPower); + + // byte[] ratedPowerByteArr = Base64.getDecoder().decode(ratedPower); + byte[] ratedPowerByteArr = BytesUtil.checkLengthAndBehindAppendZero(Constants.zeroByteArray, 4); + + // 升级服务器地址 + byte[] updateServerAddressByteArr = BytesUtil.checkLengthAndBehindAppendZero(BytesUtil.str2Asc(Constants.updateServerIP), 32); + + // 升级服务器端口 + byte[] updateServerPortByteArr = BytesUtil.checkLengthAndBehindAppendZero(Constants.updateServerPort, 4); + // byte[] updateServerPortByteArr = BytesUtil.checkLengthAndBehindAppendZero(BytesUtil.str2Bcd("15"), 4); + + // 用户名 + byte[] userNameByteArr = BytesUtil.checkLengthAndBehindAppendZero(BytesUtil.str2Asc(Constants.updateServerUserName), 32); + + // 密码 + byte[] passwordByteArr = BytesUtil.checkLengthAndBehindAppendZero(BytesUtil.str2Asc(Constants.updateServerPassword), 32); + + // 文件路径 + byte[] filePathByteArr = BytesUtil.checkLengthAndBehindAppendZero(BytesUtil.str2Asc(Constants.filePath), 64); + + // 执行控制 01:立即执行 02:空闲执行 + byte[] performTypeByteArr = Constants.oneByteArray; + + // 下载超时时间 单位:min + byte[] overTimeByteArr = new byte[]{0x05}; + + byte[] msgBody = Bytes.concat(pileSnByteArr, pileModelType, ratedPowerByteArr, updateServerAddressByteArr, + updateServerPortByteArr, userNameByteArr, passwordByteArr, filePathByteArr, performTypeByteArr, overTimeByteArr); + + this.push(msgBody, pileModelInfoVO.getPileSn(), YKCFrameTypeCode.REMOTE_UPDATE_CODE); + log.info("【=====平台下发指令=====】:远程更新, 桩号:{}, 类型:{}, 额定功率:{}, 服务器地址:{}, 端口号:{}, 用户名:{}, 密码:{}, 文件路径:{}", + pileModelInfoVO.getPileSn(), pileModelType, BytesUtil.bcd2Str(ratedPowerByteArr), BytesUtil.binary(updateServerAddressByteArr, 16), + BytesUtil.binary(updateServerPortByteArr, 16), BytesUtil.binary(userNameByteArr, 16), BytesUtil.binary(passwordByteArr, 16), + BytesUtil.binary(filePathByteArr, 16)); + } + } +} diff --git a/jsowell-pile/pom.xml b/jsowell-pile/pom.xml new file mode 100644 index 000000000..161983f02 --- /dev/null +++ b/jsowell-pile/pom.xml @@ -0,0 +1,106 @@ + + + + jsowell-charger-web + com.jsowell + 1.0.0 + + 4.0.0 + + jsowell-pile + + + + + + + + + com.jsowell + jsowell-framework + + + + org.projectlombok + lombok + + + junit + junit + + + org.springframework + spring-test + + + io.swagger + swagger-annotations + 1.6.2 + compile + + + + + org.apache.commons + commons-collections4 + 4.4 + + + + com.github.wxpay + wxpay-sdk + + + + org.jdom + jdom + + + + com.google.zxing + core + + + + + com.baomidou + mybatis-plus-boot-starter + 3.4.0 + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-avro + + + + + 8 + 8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.8 + 1.8 + + /src/test/** + + utf-8 + + + + + + + \ No newline at end of file diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/domain/MemberBasicInfo.java b/jsowell-pile/src/main/java/com/jsowell/pile/domain/MemberBasicInfo.java new file mode 100644 index 000000000..e1a597936 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/domain/MemberBasicInfo.java @@ -0,0 +1,107 @@ +package com.jsowell.pile.domain; + +import com.jsowell.common.annotation.Excel; +import com.jsowell.common.core.domain.BaseEntity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.math.BigDecimal; + +/** + * 会员基础信息对象 member_basic_info + * + * @author jsowell + * @date 2022-10-12 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class MemberBasicInfo extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * 主键id + */ + private Integer id; + + /** + * 会员id + */ + @Excel(name = "会员id") + private String memberId; + + /** + * 微信用户身份码openid + */ + private String openId; + + /** + * 昵称 + */ + @Excel(name = "昵称") + private String nickName; + + /** + * 逻辑卡号 + */ + @Excel(name = "逻辑卡号") + private String logicCard; + + /** + * 物理卡号 + */ + @Excel(name = "物理卡号") + private String physicsCard; + + /** + * 状态 + */ + @Excel(name = "状态") + private String status; + + /** + * 头像url + */ + @Excel(name = "头像url") + private String avatarUrl; + + /** + * 手机号 + */ + @Excel(name = "手机号") + private String mobileNumber; + + /** + * 所属运营商 + */ + @Excel(name = "所属运营商") + private Long merchantId; + + /** + * 删除标识(0-正常;1-删除) + */ + private String delFlag; + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("id", id) + .append("memberId", memberId) + .append("nickName", nickName) + .append("logicCard", logicCard) + .append("physicsCard", physicsCard) + .append("status", status) + .append("avatarUrl", avatarUrl) + .append("mobileNumber", mobileNumber) + .append("merchantId", merchantId) + .append("delFlag", delFlag) + .toString(); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/domain/MemberTransactionRecord.java b/jsowell-pile/src/main/java/com/jsowell/pile/domain/MemberTransactionRecord.java new file mode 100644 index 000000000..460e74047 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/domain/MemberTransactionRecord.java @@ -0,0 +1,78 @@ +package com.jsowell.pile.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 会员交易记录表 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class MemberTransactionRecord { + /** + * 主键 + */ + private Integer id; + + /** + * 充电订单号 + */ + private String orderCode; + + /** + * 场景类型(order, balance) + */ + private String scenarioType; + + /** + * 会员id + */ + private String memberId; + + /** + * 操作类型(forward-正向, reverse-逆向) + */ + private String actionType; + + /** + * 支付类型(wx, balance) + */ + private String payMode; + + /** + * 金额 + */ + private BigDecimal amount; + + /** + * 外部商户订单号 + */ + private String outTradeNo; + + /** + * 外部支付订单号 + */ + private String transactionId; + + /** + * 外部退款单号 + */ + private String outRefundNo; + + /** + * 外部支付退款单号 + */ + private String refundId; + + /** + * 创建时间 + */ + private LocalDateTime createTime; +} \ No newline at end of file diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/domain/MemberWalletInfo.java b/jsowell-pile/src/main/java/com/jsowell/pile/domain/MemberWalletInfo.java new file mode 100644 index 000000000..1441ec242 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/domain/MemberWalletInfo.java @@ -0,0 +1,56 @@ +package com.jsowell.pile.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 会员钱包信息表 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class MemberWalletInfo { + /** + * 主键 + */ + private Integer id; + + /** + * 会员id + */ + private String memberId; + + /** + * 本金余额 + */ + private BigDecimal principalBalance; + + /** + * 赠送余额 + */ + private BigDecimal giftBalance; + + /** + * 版本号 + */ + private Integer version; + + private String createBy; + + private LocalDateTime createTime; + + private String updateBy; + + private LocalDateTime updateTime; + + /** + * 删除标识(0-正常;1-删除) + */ + private String delFlag; +} \ No newline at end of file diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/domain/MemberWalletLog.java b/jsowell-pile/src/main/java/com/jsowell/pile/domain/MemberWalletLog.java new file mode 100644 index 000000000..a0487e88e --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/domain/MemberWalletLog.java @@ -0,0 +1,63 @@ +package com.jsowell.pile.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 会员钱包流水表 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class MemberWalletLog { + /** + * 主键 + */ + private Integer id; + + /** + * 会员id + */ + private String memberId; + + /** + * 类型(1-进账;2-出账) + */ + private String type; + + /** + * 子类型(10-充值, 11-赠送, 12-订单结算退款,20-后管扣款, 21-订单付款, 22-用户退款) + */ + private String subType; + + /** + * 金额 + */ + private BigDecimal amount; + + /** + * 余额类型(1-本金,2-赠送) + */ + private String category; + + /** + * 关联订单编号 + */ + private String relatedOrderCode; + + /** + * 创建人 + */ + private String createBy; + + /** + * 创建时间 + */ + private LocalDateTime createTime; +} \ No newline at end of file diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/domain/OrderAbnormalRecord.java b/jsowell-pile/src/main/java/com/jsowell/pile/domain/OrderAbnormalRecord.java new file mode 100644 index 000000000..2eda57291 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/domain/OrderAbnormalRecord.java @@ -0,0 +1,502 @@ +package com.jsowell.pile.domain; + +import com.jsowell.common.annotation.Excel; +import com.jsowell.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 订单异常记录对象 order_abnormal_record + * + * @author jsowell + * @date 2023-02-13 + */ +public class OrderAbnormalRecord extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + private Integer id; + + /** + * 订单编号/交易流水号 + */ + @Excel(name = "订单编号") + private String orderCode; + + /** + * 桩编码 + */ + @Excel(name = "桩编码") + private String pileSn; + + /** + * 枪号 + */ + @Excel(name = "枪号") + private String connectorCode; + + /** + * 开始时间 + */ + @Excel(name = "开始时间") + private String startTime; + + /** + * 结束时间 + */ + @Excel(name = "结束时间") + private String endTime; + + /** + * 尖单价 精确到小数点后五位(尖电费+尖服务费,见费率帧) + */ + @Excel(name = "尖单价", readConverterExp = "尖=电费+尖服务费,见费率帧") + private String sharpPrice; + + /** + * 尖电量 精确到小数点后四位 + */ + @Excel(name = "尖电量") + private String sharpUsedElectricity; + + /** + * 计损尖电量 + */ + @Excel(name = "计损尖电量") + private String sharpPlanLossElectricity; + + /** + * 尖金额 + */ + @Excel(name = "尖金额") + private String sharpAmount; + + /** + * 峰单价 精确到小数点后五位(峰电费+峰服务费) + */ + @Excel(name = "峰单价", readConverterExp = "峰=电费+峰服务费") + private String peakPrice; + + /** + * 峰电量 + */ + @Excel(name = "峰电量") + private String peakUsedElectricity; + + /** + * 计损峰电量 + */ + @Excel(name = "计损峰电量") + private String peakPlanLossElectricity; + + /** + * 峰金额 + */ + @Excel(name = "峰金额") + private String peakAmount; + + /** + * 平单价 精确到小数点后五位(平电费+平服务费) + */ + @Excel(name = "平单价", readConverterExp = "平=电费+平服务费") + private String flatPrice; + + /** + * 平电量 + */ + @Excel(name = "平电量") + private String flatUsedElectricity; + + /** + * 计损平电量 + */ + @Excel(name = "计损平电量") + private String flatPlanLossElectricity; + + /** + * 平金额 + */ + @Excel(name = "平金额") + private String flatAmount; + + /** + * 谷单价 精确到小数点后五位(谷电费+谷 服务费) + */ + @Excel(name = "谷单价", readConverterExp = "谷=电费+谷,服=务费") + private String valleyPrice; + + /** + * 谷电量 + */ + @Excel(name = "谷电量") + private String valleyUsedElectricity; + + /** + * 计损谷电量 + */ + @Excel(name = "计损谷电量") + private String valleyPlanLossElectricity; + + /** + * 谷金额 + */ + @Excel(name = "谷金额") + private String valleyAmount; + + /** + * 电表总起值 + */ + @Excel(name = "电表总起值") + private String ammeterTotalStart; + + /** + * 电表总止值 + */ + @Excel(name = "电表总止值") + private String ammeterTotalEnd; + + /** + * 总电量 + */ + @Excel(name = "总电量") + private String totalElectricity; + + /** + * 计损总电量 + */ + @Excel(name = "计损总电量") + private String planLossTotalElectricity; + + /** + * 消费金额 精确到小数点后四位,包含电费、 服务费 + */ + @Excel(name = "消费金额") + private String consumptionAmount; + + /** + * VIN 码 VIN 码,此处 VIN 码和充电时 VIN 码不同, 正序直接上传, 无需补 0 和反序 + */ + @Excel(name = "VIN码") + private String vinCode; + + /** + * 交易标识(0x01app启动; 0x02卡启动; 0x04离线卡启动; 0x05vin码启动充电) + */ + @Excel(name = "交易标识(0x01app启动; 0x02卡启动; 0x04离线卡启动; 0x05vin码启动充电)") + private String transactionIdentifier; + + /** + * 交易时间 CP56Time2a 格式 + */ + @Excel(name = "交易时间") + private String transactionTime; + + /** + * 停止原因 + */ + @Excel(name = "停止原因") + private String stopReasonMsg; + + /** + * 物理卡号 不足 8 位补 0 + */ + @Excel(name = "物理卡号") + private String logicCard; + + public void setId(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public void setOrderCode(String orderCode) { + this.orderCode = orderCode; + } + + public String getOrderCode() { + return orderCode; + } + + public void setPileSn(String pileSn) { + this.pileSn = pileSn; + } + + public String getPileSn() { + return pileSn; + } + + public void setConnectorCode(String connectorCode) { + this.connectorCode = connectorCode; + } + + public String getConnectorCode() { + return connectorCode; + } + + public void setStartTime(String startTime) { + this.startTime = startTime; + } + + public String getStartTime() { + return startTime; + } + + public void setEndTime(String endTime) { + this.endTime = endTime; + } + + public String getEndTime() { + return endTime; + } + + public void setSharpPrice(String sharpPrice) { + this.sharpPrice = sharpPrice; + } + + public String getSharpPrice() { + return sharpPrice; + } + + public void setSharpUsedElectricity(String sharpUsedElectricity) { + this.sharpUsedElectricity = sharpUsedElectricity; + } + + public String getSharpUsedElectricity() { + return sharpUsedElectricity; + } + + public void setSharpPlanLossElectricity(String sharpPlanLossElectricity) { + this.sharpPlanLossElectricity = sharpPlanLossElectricity; + } + + public String getSharpPlanLossElectricity() { + return sharpPlanLossElectricity; + } + + public void setSharpAmount(String sharpAmount) { + this.sharpAmount = sharpAmount; + } + + public String getSharpAmount() { + return sharpAmount; + } + + public void setPeakPrice(String peakPrice) { + this.peakPrice = peakPrice; + } + + public String getPeakPrice() { + return peakPrice; + } + + public void setPeakUsedElectricity(String peakUsedElectricity) { + this.peakUsedElectricity = peakUsedElectricity; + } + + public String getPeakUsedElectricity() { + return peakUsedElectricity; + } + + public void setPeakPlanLossElectricity(String peakPlanLossElectricity) { + this.peakPlanLossElectricity = peakPlanLossElectricity; + } + + public String getPeakPlanLossElectricity() { + return peakPlanLossElectricity; + } + + public void setPeakAmount(String peakAmount) { + this.peakAmount = peakAmount; + } + + public String getPeakAmount() { + return peakAmount; + } + + public void setFlatPrice(String flatPrice) { + this.flatPrice = flatPrice; + } + + public String getFlatPrice() { + return flatPrice; + } + + public void setFlatUsedElectricity(String flatUsedElectricity) { + this.flatUsedElectricity = flatUsedElectricity; + } + + public String getFlatUsedElectricity() { + return flatUsedElectricity; + } + + public void setFlatPlanLossElectricity(String flatPlanLossElectricity) { + this.flatPlanLossElectricity = flatPlanLossElectricity; + } + + public String getFlatPlanLossElectricity() { + return flatPlanLossElectricity; + } + + public void setFlatAmount(String flatAmount) { + this.flatAmount = flatAmount; + } + + public String getFlatAmount() { + return flatAmount; + } + + public void setValleyPrice(String valleyPrice) { + this.valleyPrice = valleyPrice; + } + + public String getValleyPrice() { + return valleyPrice; + } + + public void setValleyUsedElectricity(String valleyUsedElectricity) { + this.valleyUsedElectricity = valleyUsedElectricity; + } + + public String getValleyUsedElectricity() { + return valleyUsedElectricity; + } + + public void setValleyPlanLossElectricity(String valleyPlanLossElectricity) { + this.valleyPlanLossElectricity = valleyPlanLossElectricity; + } + + public String getValleyPlanLossElectricity() { + return valleyPlanLossElectricity; + } + + public void setValleyAmount(String valleyAmount) { + this.valleyAmount = valleyAmount; + } + + public String getValleyAmount() { + return valleyAmount; + } + + public void setAmmeterTotalStart(String ammeterTotalStart) { + this.ammeterTotalStart = ammeterTotalStart; + } + + public String getAmmeterTotalStart() { + return ammeterTotalStart; + } + + public void setAmmeterTotalEnd(String ammeterTotalEnd) { + this.ammeterTotalEnd = ammeterTotalEnd; + } + + public String getAmmeterTotalEnd() { + return ammeterTotalEnd; + } + + public void setTotalElectricity(String totalElectricity) { + this.totalElectricity = totalElectricity; + } + + public String getTotalElectricity() { + return totalElectricity; + } + + public void setPlanLossTotalElectricity(String planLossTotalElectricity) { + this.planLossTotalElectricity = planLossTotalElectricity; + } + + public String getPlanLossTotalElectricity() { + return planLossTotalElectricity; + } + + public void setConsumptionAmount(String consumptionAmount) { + this.consumptionAmount = consumptionAmount; + } + + public String getConsumptionAmount() { + return consumptionAmount; + } + + public void setVinCode(String vinCode) { + this.vinCode = vinCode; + } + + public String getVinCode() { + return vinCode; + } + + public void setTransactionIdentifier(String transactionIdentifier) { + this.transactionIdentifier = transactionIdentifier; + } + + public String getTransactionIdentifier() { + return transactionIdentifier; + } + + public void setTransactionTime(String transactionTime) { + this.transactionTime = transactionTime; + } + + public String getTransactionTime() { + return transactionTime; + } + + public void setStopReasonMsg(String stopReasonMsg) { + this.stopReasonMsg = stopReasonMsg; + } + + public String getStopReasonMsg() { + return stopReasonMsg; + } + + public void setLogicCard(String logicCard) { + this.logicCard = logicCard; + } + + public String getLogicCard() { + return logicCard; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("id", getId()) + .append("orderCode", getOrderCode()) + .append("pileSn", getPileSn()) + .append("connectorCode", getConnectorCode()) + .append("startTime", getStartTime()) + .append("endTime", getEndTime()) + .append("sharpPrice", getSharpPrice()) + .append("sharpUsedElectricity", getSharpUsedElectricity()) + .append("sharpPlanLossElectricity", getSharpPlanLossElectricity()) + .append("sharpAmount", getSharpAmount()) + .append("peakPrice", getPeakPrice()) + .append("peakUsedElectricity", getPeakUsedElectricity()) + .append("peakPlanLossElectricity", getPeakPlanLossElectricity()) + .append("peakAmount", getPeakAmount()) + .append("flatPrice", getFlatPrice()) + .append("flatUsedElectricity", getFlatUsedElectricity()) + .append("flatPlanLossElectricity", getFlatPlanLossElectricity()) + .append("flatAmount", getFlatAmount()) + .append("valleyPrice", getValleyPrice()) + .append("valleyUsedElectricity", getValleyUsedElectricity()) + .append("valleyPlanLossElectricity", getValleyPlanLossElectricity()) + .append("valleyAmount", getValleyAmount()) + .append("ammeterTotalStart", getAmmeterTotalStart()) + .append("ammeterTotalEnd", getAmmeterTotalEnd()) + .append("totalElectricity", getTotalElectricity()) + .append("planLossTotalElectricity", getPlanLossTotalElectricity()) + .append("consumptionAmount", getConsumptionAmount()) + .append("vinCode", getVinCode()) + .append("transactionIdentifier", getTransactionIdentifier()) + .append("transactionTime", getTransactionTime()) + .append("stopReasonMsg", getStopReasonMsg()) + .append("logicCard", getLogicCard()) + .append("createTime", getCreateTime()) + .toString(); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/domain/OrderBasicInfo.java b/jsowell-pile/src/main/java/com/jsowell/pile/domain/OrderBasicInfo.java new file mode 100644 index 000000000..01121bfe0 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/domain/OrderBasicInfo.java @@ -0,0 +1,185 @@ +package com.jsowell.pile.domain; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.jsowell.common.annotation.Excel; +import com.jsowell.common.core.domain.BaseEntity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +/** + * 订单对象 order_basic_info + * + * @author jsowell + * @date 2022-09-30 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class OrderBasicInfo extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + private Long id; + + /** + * 订单编号 + */ + @Excel(name = "订单编号") + private String orderCode; + + /** + * 订单状态(0-待支付;1-充电中;2-待结算;3-待补缴;4-异常;5-可疑;6-订单完成) + */ + @Excel(name = "订单状态") + private String orderStatus; + + /** + * 会员id + */ + @Excel(name = "会员id") + private String memberId; + + /** + * 站点id + */ + @Excel(name = "站点id") + private String stationId; + + /** + * 充电桩sn号 + */ + @Excel(name = "充电桩sn号") + private String pileSn; + + /** + * 充电桩枪口号 + */ + @Excel(name = "充电桩枪口号") + private String connectorCode; + + /** + * 充电桩枪口编号 + */ + @Excel(name = "充电桩枪口编号") + private String pileConnectorCode; + + /** + * 启动方式 + * 0-后管启动;1-用户app启动 + */ + @Excel(name = "启动方式") + private String startMode; + + /** + * 支付方式 + * 1-余额支付;3-白名单支付; 4-微信支付;5-支付宝支付; + */ + @Excel(name = "支付方式") + private String payMode; + + /** + * 支付状态 + * 0-待支付;1-支付完成 + */ + @Excel(name = "支付状态") + private String payStatus; + + /** + * 支付金额 + * 指用户本次需要充电的金额 + */ + @Excel(name = "支付金额") + private BigDecimal payAmount; + + /** + * 支付时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "支付时间", width = 30, dateFormat = "yyyy-MM-dd") + private Date payTime; + + /** + * 订单总金额 = 电费总金额 + 服务费总金额 + */ + @Excel(name = "订单总金额 = 电费总金额 + 服务费总金额") + private BigDecimal orderAmount; + + /** + * 充电开始时间 + */ + private Date chargeStartTime; + + /** + * 充电结束时间 + */ + private Date chargeEndTime; + + /** + * 开始SOC + */ + private String startSOC; + + /** + * 结束SOC + */ + private String endSOC; + + /** + * 异常原因 + */ + private String reason; + + /** + * 结算时间 + */ + private Date settlementTime; + + /** + * 退款金额 + */ + private BigDecimal refundAmount; + + /** + * 删除标识(0-正常;1-删除) + */ + private String delFlag; + + private List orderDetailList; + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("id", id) + .append("orderCode", orderCode) + .append("orderStatus", orderStatus) + .append("memberId", memberId) + .append("stationId", stationId) + .append("pileSn", pileSn) + .append("connectorCode", connectorCode) + .append("startMode", startMode) + .append("payMode", payMode) + .append("payStatus", payStatus) + .append("payAmount", payAmount) + .append("payTime", payTime) + .append("orderAmount", orderAmount) + .append("chargeStartTime", chargeStartTime) + .append("chargeEndTime", chargeEndTime) + .append("startSOC", startSOC) + .append("endSOC", endSOC) + .append("reason", reason) + .append("delFlag", delFlag) + .append("orderDetailList", orderDetailList) + .toString(); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/domain/OrderDetail.java b/jsowell-pile/src/main/java/com/jsowell/pile/domain/OrderDetail.java new file mode 100644 index 000000000..dfa20a9b8 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/domain/OrderDetail.java @@ -0,0 +1,319 @@ +package com.jsowell.pile.domain; + +import com.jsowell.common.annotation.Excel; +import com.jsowell.common.core.domain.BaseEntity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.math.BigDecimal; + +/** + * 订单详情对象 order_detail + * + * @author jsowell + * @date 2022-09-30 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class OrderDetail extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + private Integer id; + + /** + * 订单编号 + */ + @Excel(name = "订单编号") + private String orderCode; + + /** + * 总用电量 + */ + @Excel(name = "总用电量") + private BigDecimal totalUsedElectricity; + + /** + * 订单总金额(电费总额+服务费总额) + */ + @Excel(name = "订单总金额", readConverterExp = "电=费总额+服务费总额") + private BigDecimal totalOrderAmount; + + /** + * 电费总金额(各时段消耗电费总金额) + */ + @Excel(name = "电费总金额", readConverterExp = "各=时段消耗电费总金额") + private BigDecimal totalElectricityAmount; + + /** + * 服务费总金额(各时段服务费总金额) + */ + @Excel(name = "服务费总金额", readConverterExp = "各=时段服务费总金额") + private BigDecimal totalServiceAmount; + + /** + * 尖时段用电量 + */ + @Excel(name = "尖时段用电量") + private BigDecimal sharpUsedElectricity; + + /** + * 尖时段电费单价 + */ + @Excel(name = "尖时段电费单价") + private BigDecimal sharpElectricityPrice; + + /** + * 尖时段服务费单价 + */ + @Excel(name = "尖时段服务费单价") + private BigDecimal sharpServicePrice; + + /** + * 峰时段用电量 + */ + @Excel(name = "峰时段用电量") + private BigDecimal peakUsedElectricity; + + /** + * 峰时段电费单价 + */ + @Excel(name = "峰时段电费单价") + private BigDecimal peakElectricityPrice; + + /** + * 峰时段服务费单价 + */ + @Excel(name = "峰时段服务费单价") + private BigDecimal peakServicePrice; + + /** + * 平时段用电量 + */ + @Excel(name = "平时段用电量") + private BigDecimal flatUsedElectricity; + + /** + * 平时段电费单价 + */ + @Excel(name = "平时段电费单价") + private BigDecimal flatElectricityPrice; + + /** + * 平时段服务费单价 + */ + @Excel(name = "平时段服务费单价") + private BigDecimal flatServicePrice; + + /** + * 谷时段用电量 + */ + @Excel(name = "谷时段用电量") + private BigDecimal valleyUsedElectricity; + + /** + * 谷时段电费单价 + */ + @Excel(name = "谷时段电费单价") + private BigDecimal valleyElectricityPrice; + + /** + * 谷时段服务费单价 + */ + @Excel(name = "谷时段服务费单价") + private BigDecimal valleyServicePrice; + + /** + * 删除标识(0-正常;1-删除) + */ + private String delFlag; + + public void setId(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public void setOrderCode(String orderCode) { + this.orderCode = orderCode; + } + + public String getOrderCode() { + return orderCode; + } + + public void setTotalUsedElectricity(BigDecimal totalUsedElectricity) { + this.totalUsedElectricity = totalUsedElectricity; + } + + public BigDecimal getTotalUsedElectricity() { + return totalUsedElectricity; + } + + public void setTotalOrderAmount(BigDecimal totalOrderAmount) { + this.totalOrderAmount = totalOrderAmount; + } + + public BigDecimal getTotalOrderAmount() { + return totalOrderAmount; + } + + public void setTotalElectricityAmount(BigDecimal totalElectricityAmount) { + this.totalElectricityAmount = totalElectricityAmount; + } + + public BigDecimal getTotalElectricityAmount() { + return totalElectricityAmount; + } + + public void setTotalServiceAmount(BigDecimal totalServiceAmount) { + this.totalServiceAmount = totalServiceAmount; + } + + public BigDecimal getTotalServiceAnount() { + return totalServiceAmount; + } + + public void setSharpUsedElectricity(BigDecimal sharpUsedElectricity) { + this.sharpUsedElectricity = sharpUsedElectricity; + } + + public BigDecimal getSharpUsedElectricity() { + return sharpUsedElectricity; + } + + public void setSharpElectricityPrice(BigDecimal sharpElectricityPrice) { + this.sharpElectricityPrice = sharpElectricityPrice; + } + + public BigDecimal getSharpElectricityPrice() { + return sharpElectricityPrice; + } + + public void setSharpServicePrice(BigDecimal sharpServicePrice) { + this.sharpServicePrice = sharpServicePrice; + } + + public BigDecimal getSharpServicePrice() { + return sharpServicePrice; + } + + public void setPeakUsedElectricity(BigDecimal peakUsedElectricity) { + this.peakUsedElectricity = peakUsedElectricity; + } + + public BigDecimal getPeakUsedElectricity() { + return peakUsedElectricity; + } + + public void setPeakElectricityPrice(BigDecimal peakElectricityPrice) { + this.peakElectricityPrice = peakElectricityPrice; + } + + public BigDecimal getPeakElectricityPrice() { + return peakElectricityPrice; + } + + public void setPeakServicePrice(BigDecimal peakServicePrice) { + this.peakServicePrice = peakServicePrice; + } + + public BigDecimal getPeakServicePrice() { + return peakServicePrice; + } + + public void setFlatUsedElectricity(BigDecimal flatUsedElectricity) { + this.flatUsedElectricity = flatUsedElectricity; + } + + public BigDecimal getFlatUsedElectricity() { + return flatUsedElectricity; + } + + public void setFlatElectricityPrice(BigDecimal flatElectricityPrice) { + this.flatElectricityPrice = flatElectricityPrice; + } + + public BigDecimal getFlatElectricityPrice() { + return flatElectricityPrice; + } + + public void setFlatServicePrice(BigDecimal flatServicePrice) { + this.flatServicePrice = flatServicePrice; + } + + public BigDecimal getFlatServicePrice() { + return flatServicePrice; + } + + public void setValleyUsedElectricity(BigDecimal valleyUsedElectricity) { + this.valleyUsedElectricity = valleyUsedElectricity; + } + + public BigDecimal getValleyUsedElectricity() { + return valleyUsedElectricity; + } + + public void setValleyElectricityPrice(BigDecimal valleyElectricityPrice) { + this.valleyElectricityPrice = valleyElectricityPrice; + } + + public BigDecimal getValleyElectricityPrice() { + return valleyElectricityPrice; + } + + public void setValleyServicePrice(BigDecimal valleyServicePrice) { + this.valleyServicePrice = valleyServicePrice; + } + + public BigDecimal getValleyServicePrice() { + return valleyServicePrice; + } + + public void setDelFlag(String delFlag) { + this.delFlag = delFlag; + } + + public String getDelFlag() { + return delFlag; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("id", getId()) + .append("orderCode", getOrderCode()) + .append("totalUsedElectricity", getTotalUsedElectricity()) + .append("totalOrderAmount", getTotalOrderAmount()) + .append("totalElectricityAmount", getTotalElectricityAmount()) + .append("totalServiceAnount", getTotalServiceAnount()) + .append("sharpUsedElectricity", getSharpUsedElectricity()) + .append("sharpElectricityPrice", getSharpElectricityPrice()) + .append("sharpServicePrice", getSharpServicePrice()) + .append("peakUsedElectricity", getPeakUsedElectricity()) + .append("peakElectricityPrice", getPeakElectricityPrice()) + .append("peakServicePrice", getPeakServicePrice()) + .append("flatUsedElectricity", getFlatUsedElectricity()) + .append("flatElectricityPrice", getFlatElectricityPrice()) + .append("flatServicePrice", getFlatServicePrice()) + .append("valleyUsedElectricity", getValleyUsedElectricity()) + .append("valleyElectricityPrice", getValleyElectricityPrice()) + .append("valleyServicePrice", getValleyServicePrice()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("delFlag", getDelFlag()) + .toString(); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/domain/OrderPayRecord.java b/jsowell-pile/src/main/java/com/jsowell/pile/domain/OrderPayRecord.java new file mode 100644 index 000000000..8b1b8aeee --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/domain/OrderPayRecord.java @@ -0,0 +1,70 @@ +package com.jsowell.pile.domain; + +import com.jsowell.common.enums.ykc.OrderPayRecordEnum; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 订单支付记录 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class OrderPayRecord { + /** + * 主键 + */ + private Integer id; + + /** + * 订单号 + */ + private String orderCode; + + /** + * 支付方式(1-本金余额支付; 2-赠送余额支付; 3-白名单支付; 4-微信支付; 5-支付宝支付) + * @see OrderPayRecordEnum + */ + private String payMode; + + /** + * 支付金额 + */ + private BigDecimal payAmount; + + /** + * 退款金额 + */ + private BigDecimal refundAmount; + + /** + * 创建人 + */ + private String createBy; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 更新人 + */ + private String updateBy; + + /** + * 更新时间 + */ + private LocalDateTime updateTime; + + /** + * 删除标识(0-正常;1-删除) + */ + private String delFlag; +} \ No newline at end of file diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileBasicInfo.java b/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileBasicInfo.java new file mode 100644 index 000000000..b5d954a4a --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileBasicInfo.java @@ -0,0 +1,215 @@ +package com.jsowell.pile.domain; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.jsowell.common.annotation.Excel; +import com.jsowell.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.Date; + +/** + * 设备管理对象 pile_basic_info + * + * @author jsowell + * @date 2022-08-26 + */ +public class PileBasicInfo extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + private Long id; + + /** + * 桩号 + */ + @Excel(name = "桩号") + private String sn; + + /** + * 状态(0-未知;1-在线;2-离线;3-故障) + */ + // @Excel(name = "状态", readConverterExp = "0-未知;1-在线;2-离线;3-故障") + // private String status; + + /** + * 经营类型(1-运营桩;2-个人桩) + */ + @Excel(name = "经营类型", readConverterExp = "1=-运营桩;2-个人桩") + private String businessType; + + /** + * 软件协议(1-云快充;2-永联) + */ + @Excel(name = "软件协议", readConverterExp = "1=-云快充;2-永联") + private String softwareProtocol; + + /** + * 生产日期 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "生产日期", width = 30, dateFormat = "yyyy-MM-dd") + private Date productionDate; + + /** + * 证书编号 + */ + @Excel(name = "证书编号") + private Long licenceId; + + /** + * 充电桩型号 + */ + @Excel(name = "充电桩型号") + private Long modelId; + + /** + * sim卡id + */ + @Excel(name = "sim卡id") + private Long simId; + + /** + * 运营商id + */ + @Excel(name = "运营商id") + private Long merchantId; + + /** + * 充电站id + */ + @Excel(name = "充电站id") + private Long stationId; + + /** + * 故障原因 + */ + @Excel(name = "故障原因") + private String faultReason; + + /** + * 删除标识(0-正常;1-删除) + */ + private String delFlag; + + public void setId(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + public void setSn(String sn) { + this.sn = sn; + } + + public String getSn() { + return sn; + } + + public void setBusinessType(String businessType) { + this.businessType = businessType; + } + + public String getBusinessType() { + return businessType; + } + + public void setSoftwareProtocol(String softwareProtocol) { + this.softwareProtocol = softwareProtocol; + } + + public String getSoftwareProtocol() { + return softwareProtocol; + } + + public void setProductionDate(Date productionDate) { + this.productionDate = productionDate; + } + + public Date getProductionDate() { + return productionDate; + } + + public void setLicenceId(Long licenceId) { + this.licenceId = licenceId; + } + + public Long getLicenceId() { + return licenceId; + } + + public void setModelId(Long modelId) { + this.modelId = modelId; + } + + public Long getModelId() { + return modelId; + } + + public void setSimId(Long simId) { + this.simId = simId; + } + + public Long getSimId() { + return simId; + } + + public void setMerchantId(Long merchantId) { + this.merchantId = merchantId; + } + + public Long getMerchantId() { + return merchantId; + } + + public void setStationId(Long stationId) { + this.stationId = stationId; + } + + public Long getStationId() { + return stationId; + } + + public void setFaultReason(String faultReason) { + this.faultReason = faultReason; + } + + public String getFaultReason() { + return faultReason; + } + + public void setDelFlag(String delFlag) { + this.delFlag = delFlag; + } + + public String getDelFlag() { + return delFlag; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("id", getId()) + .append("sn", getSn()) + .append("businessType", getBusinessType()) + .append("softwareProtocol", getSoftwareProtocol()) + .append("productionDate", getProductionDate()) + .append("licenceId", getLicenceId()) + .append("modelId", getModelId()) + .append("simId", getSimId()) + .append("merchantId", getMerchantId()) + .append("stationId", getStationId()) + .append("faultReason", getFaultReason()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("delFlag", getDelFlag()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileBillingDetail.java b/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileBillingDetail.java new file mode 100644 index 000000000..4571e5b80 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileBillingDetail.java @@ -0,0 +1,131 @@ +package com.jsowell.pile.domain; + +import com.jsowell.common.annotation.Excel; +import com.jsowell.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.math.BigDecimal; + +/** + * 计费模板详情对象 pile_billing_detail + * + * @author jsowell + * @date 2022-09-20 + */ +public class PileBillingDetail extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + private Long id; + + /** + * 计费模板编号 + */ + @Excel(name = "计费模板编号") + private String templateCode; + + /** + * 时段类型(1-尖时;2-峰时;3-平时;4-谷时) + */ + @Excel(name = "时段类型", readConverterExp = "1=-尖时;2-峰时;3-平时;4-谷时") + private String timeType; + + /** + * 电费 + */ + @Excel(name = "电费") + private BigDecimal electricityPrice; + + /** + * 服务费 + */ + @Excel(name = "服务费") + private BigDecimal servicePrice; + + /** + * 适用时间段 + */ + @Excel(name = "适用时间段") + private String applyTime; + + /** + * 删除标识(0-正常;1-删除) + */ + private String delFlag; + + public void setId(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + public void setTemplateCode(String templateCode) { + this.templateCode = templateCode; + } + + public String getTemplateCode() { + return templateCode; + } + + public void setTimeType(String timeType) { + this.timeType = timeType; + } + + public String getTimeType() { + return timeType; + } + + public void setElectricityPrice(BigDecimal electricityPrice) { + this.electricityPrice = electricityPrice; + } + + public BigDecimal getElectricityPrice() { + return electricityPrice; + } + + public void setServicePrice(BigDecimal servicePrice) { + this.servicePrice = servicePrice; + } + + public BigDecimal getServicePrice() { + return servicePrice; + } + + public void setApplyTime(String applyTime) { + this.applyTime = applyTime; + } + + public String getApplyTime() { + return applyTime; + } + + public void setDelFlag(String delFlag) { + this.delFlag = delFlag; + } + + public String getDelFlag() { + return delFlag; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("id", getId()) + .append("templateCode", getTemplateCode()) + .append("timeType", getTimeType()) + .append("electricityPrice", getElectricityPrice()) + .append("servicePrice", getServicePrice()) + .append("applyTime", getApplyTime()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("delFlag", getDelFlag()) + .toString(); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileBillingRelation.java b/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileBillingRelation.java new file mode 100644 index 000000000..b4094d128 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileBillingRelation.java @@ -0,0 +1,35 @@ +package com.jsowell.pile.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 充电桩和计费模板关系 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PileBillingRelation { + /** + * 主键 + */ + private Long id; + + /** + * 桩编号 + */ + private String pileSn; + + /** + * 计费模板编号 + */ + private String billingTemplateCode; + + /** + * 站点id + */ + private String stationId; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileBillingTemplate.java b/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileBillingTemplate.java new file mode 100644 index 000000000..38ea662b2 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileBillingTemplate.java @@ -0,0 +1,154 @@ +package com.jsowell.pile.domain; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.jsowell.common.annotation.Excel; +import com.jsowell.common.core.domain.BaseEntity; +import lombok.Data; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.Date; +import java.util.List; + +/** + * 计费模板对象 pile_billing_template + * + * @author jsowell + * @date 2022-09-20 + */ +@Data +public class PileBillingTemplate extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + private Long id; + + /** + * 计费模板编号 + */ + private String templateCode; + + /** + * 模板名称 + */ + @Excel(name = "模板名称") + private String name; + + /** + * 车辆类型(1-电动汽车;2-电动自行车) + */ + @Excel(name = "车辆类型", readConverterExp = "1=-电动汽车;2-电动自行车") + private String type; + + /** + * 充电站id + */ + @Excel(name = "充电站id") + private Long stationId; + + /** + * 删除标识(0-正常;1-删除) + */ + private String delFlag; + + /** + * 计费模板详情信息 + */ + private List pileBillingDetailList; + + /** + * 公共模板标识(0-私有;1-公共) + */ + private String publicFlag; + + /** + * 发布时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date publishTime; + + public String getPublicFlag() { + return publicFlag; + } + + public void setPublicFlag(String publicFlag) { + this.publicFlag = publicFlag; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + public void setTemplateCode(String templateCode) { + this.templateCode = templateCode; + } + + public String getTemplateCode() { + return templateCode; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setType(String type) { + this.type = type; + } + + public String getType() { + return type; + } + + public void setStationId(Long stationId) { + this.stationId = stationId; + } + + public Long getStationId() { + return stationId; + } + + public void setDelFlag(String delFlag) { + this.delFlag = delFlag; + } + + public String getDelFlag() { + return delFlag; + } + + public List getPileBillingDetailList() { + return pileBillingDetailList; + } + + public void setPileBillingDetailList(List pileBillingDetailList) { + this.pileBillingDetailList = pileBillingDetailList; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("id", getId()) + .append("templateCode", getTemplateCode()) + .append("name", getName()) + .append("remark", getRemark()) + .append("type", getType()) + .append("stationId", getStationId()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("delFlag", getDelFlag()) + .append("pileBillingDetailList", getPileBillingDetailList()) + .append("publicFlag", getPublicFlag()) + .toString(); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileConnectorInfo.java b/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileConnectorInfo.java new file mode 100644 index 000000000..33bcf4ec3 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileConnectorInfo.java @@ -0,0 +1,121 @@ +package com.jsowell.pile.domain; + +import com.jsowell.common.annotation.Excel; +import com.jsowell.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 充电桩枪口信息对象 pile_connector_info + * + * @author jsowell + * @date 2022-08-31 + */ +public class PileConnectorInfo extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + private Integer id; + + /** + * 名称 + */ + @Excel(name = "名称") + private String name; + + /** + * 充电枪编号,由充电桩SN+01生成 + */ + @Excel(name = "充电枪编号,由充电桩SN+01生成") + private String pileConnectorCode; + + /** + * 状态 + * 0:离网 + * 1:空闲 + * 2:占用(未充电) + * 3:占用(充电中) + * 4:占用(预约锁定) + * 255:故障 + */ + @Excel(name = "状态") + private String status; + + /** + * 所属充电桩id + */ + @Excel(name = "所属充电桩id") + private String pileSn; + + /** + * 删除标识(0-正常;1-删除) + */ + private String delFlag; + + public void setId(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public String getPileConnectorCode() { + return pileConnectorCode; + } + + public void setPileConnectorCode(String pileConnectorCode) { + this.pileConnectorCode = pileConnectorCode; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getStatus() { + return status; + } + + public void setPileSn(String pileSn) { + this.pileSn = pileSn; + } + + public String getPileSn() { + return pileSn; + } + + public void setDelFlag(String delFlag) { + this.delFlag = delFlag; + } + + public String getDelFlag() { + return delFlag; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("id", getId()) + .append("name", getName()) + .append("pileConnectorCode", getPileConnectorCode()) + .append("status", getStatus()) + .append("pileId", getPileSn()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("delFlag", getDelFlag()) + .toString(); + } + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileLicenceInfo.java b/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileLicenceInfo.java new file mode 100644 index 000000000..be341692f --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileLicenceInfo.java @@ -0,0 +1,114 @@ +package com.jsowell.pile.domain; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.jsowell.common.annotation.Excel; +import com.jsowell.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.Date; + +/** + * 充电桩证书信息对象 pile_licence_info + * + * @author jsowell + * @date 2022-08-27 + */ +public class PileLicenceInfo extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 主键 */ + private Long id; + + /** 证书名称 */ + @Excel(name = "证书名称") + private String licenceName; + + /** 运营商id */ + @Excel(name = "运营商id") + private Long merchantId; + + /** 状态(0-待激活;1-正常;2-过期) */ + @Excel(name = "状态", readConverterExp = "0=-待激活;1-正常;2-过期") + private String status; + + /** 过期时间 */ + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "过期时间", width = 30, dateFormat = "yyyy-MM-dd") + private Date expireTime; + + /** 删除标识(0-正常;1-删除) */ + private String delFlag; + + public void setId(Long id) + { + this.id = id; + } + + public Long getId() + { + return id; + } + public void setLicenceName(String licenceName) + { + this.licenceName = licenceName; + } + + public String getLicenceName() + { + return licenceName; + } + public void setMerchantId(Long merchantId) + { + this.merchantId = merchantId; + } + + public Long getMerchantId() + { + return merchantId; + } + public void setStatus(String status) + { + this.status = status; + } + + public String getStatus() + { + return status; + } + public void setExpireTime(Date expireTime) + { + this.expireTime = expireTime; + } + + public Date getExpireTime() + { + return expireTime; + } + public void setDelFlag(String delFlag) + { + this.delFlag = delFlag; + } + + public String getDelFlag() + { + return delFlag; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("id", getId()) + .append("licenceName", getLicenceName()) + .append("merchantId", getMerchantId()) + .append("status", getStatus()) + .append("expireTime", getExpireTime()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("delFlag", getDelFlag()) + .toString(); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileMemberRelation.java b/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileMemberRelation.java new file mode 100644 index 000000000..2bdad4d86 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileMemberRelation.java @@ -0,0 +1,56 @@ +package com.jsowell.pile.domain; + +import com.jsowell.common.annotation.Excel; +import com.jsowell.common.core.domain.BaseEntity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 桩与用户绑定关系对象 pile_member_relation + * + * @author jsowell + * @date 2023-02-21 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class PileMemberRelation extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** $column.columnComment */ + private Integer id; + + /** 桩编码 */ + @Excel(name = "桩编码") + private String pileSn; + + /** 会员id */ + @Excel(name = "会员id") + private String memberId; + + /** 身份类型(1-管理员,2-用户) */ + @Excel(name = "身份类型", readConverterExp = "1=-管理员,2-用户") + private String type; + + public void setId(Integer id) + { + this.id = id; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("id", getId()) + .append("pileSn", getPileSn()) + .append("memberId", getMemberId()) + .append("type", getType()) + .append("createTime", getCreateTime()) + .toString(); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileMerchantInfo.java b/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileMerchantInfo.java new file mode 100644 index 000000000..4695a8d06 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileMerchantInfo.java @@ -0,0 +1,107 @@ +package com.jsowell.pile.domain; + +import com.jsowell.common.annotation.Excel; +import com.jsowell.common.core.domain.BaseEntity; +import lombok.Data; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 充电桩运营商信息对象 pile_merchant_info + * + * @author jsowell + * @date 2022-08-27 + */ +@Data +public class PileMerchantInfo extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * + */ + private Long id; + + /** + * 运营商名称 + */ + @Excel(name = "运营商名称") + private String merchantName; + + /** + * 地址 + */ + @Excel(name = "地址") + private String address; + + /** + * 状态 + * 0-失效;1-生效 + */ + @Excel(name = "状态") + private String status; + + /** + * 组织机构代码 + */ + @Excel(name = "组织机构代码") + private String organizationCode; + + /** + * 负责人姓名 + */ + @Excel(name = "负责人姓名") + private String managerName; + + /** + * 负责人电话号码 + */ + @Excel(name = "负责人电话号码") + private String managerPhone; + + /** + * 客服电话号码 + */ + @Excel(name = "客服电话号码") + private String servicePhone; + + /** + * logo + */ + @Excel(name = "logo") + private String logoUrl; + + /** + * 运营商的小程序appId + */ + private String appId; + + /** + * 部门id + */ + private String deptId; + + /** + * 删除标识(0-正常;1-删除) + */ + private String delFlag; + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("id", getId()) + .append("merchantName", getMerchantName()) + .append("address", getAddress()) + .append("status", getStatus()) + .append("organizationCode", getOrganizationCode()) + .append("managerName", getManagerName()) + .append("managerPhone", getManagerPhone()) + .append("servicePhone", getServicePhone()) + .append("logoUrl", getLogoUrl()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("delFlag", getDelFlag()) + .toString(); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileModelInfo.java b/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileModelInfo.java new file mode 100644 index 000000000..2b3dad2e1 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileModelInfo.java @@ -0,0 +1,102 @@ +package com.jsowell.pile.domain; + +import com.jsowell.common.annotation.Excel; +import com.jsowell.common.core.domain.BaseEntity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 充电桩型号信息对象 pile_model_info + * + * @author jsowell + * @date 2022-09-07 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class PileModelInfo extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * + */ + private Long id; + + /** + * 型号名称 + */ + @Excel(name = "型号名称") + private String modelName; + + /** + * 额定功率,单位W + */ + @Excel(name = "额定功率,单位kW") + private String ratedPower; + + /** + * 额定电流,单位A + */ + @Excel(name = "额定电流,单位A") + private String ratedCurrent; + + /** + * 额定电压,单位V + */ + @Excel(name = "额定电压,单位V") + private String ratedVoltage; + + /** + * 充电类型(1-快充;2-慢充) + */ + @Excel(name = "充电类型", readConverterExp = "1=-快充;2-慢充") + private String speedType; + + /** + * 充电桩类型(1-汽车桩,2-电单车) + */ + @Excel(name = "充电桩类型", readConverterExp = "1=-汽车桩,2-电单车") + private String chargerPileType; + + /** + * 充电枪数量 + */ + @Excel(name = "充电枪数量") + private Long connectorNum; + + /** + * 充电接口标准 + */ + @Excel(name = "充电接口标准") + private String interfaceStandard; + + /** + * 删除标识(0-正常;1-删除) + */ + private String delFlag; + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("id", getId()) + .append("modelName", getModelName()) + .append("ratedPower", getRatedPower()) + .append("ratedCurrent", getRatedCurrent()) + .append("ratedVoltage", getRatedVoltage()) + .append("speedType", getSpeedType()) + .append("chargerPileType", getChargerPileType()) + .append("connectorNum", getConnectorNum()) + .append("interfaceStandard", getInterfaceStandard()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("delFlag", getDelFlag()) + .toString(); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileMsgRecord.java b/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileMsgRecord.java new file mode 100644 index 000000000..5f10f71db --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileMsgRecord.java @@ -0,0 +1,53 @@ +package com.jsowell.pile.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PileMsgRecord { + /** + * 主键 + */ + private Long id; + + /** + * 桩编号 + */ + private String pileSn; + + /** + * 帧类型 + */ + private String frameType; + + /** + * 枪口号 + */ + private String connectorCode; + + /** + * 枪口编号 + */ + private String pileConnectorCode; + + /** + * 原始报文 + */ + private String originalMsg; + + /** + * json格式报文 + */ + private String jsonMsg; + + /** + * 创建时间 + */ + private String createTime; + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileSimInfo.java b/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileSimInfo.java new file mode 100644 index 000000000..5ca1b7b69 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileSimInfo.java @@ -0,0 +1,66 @@ +package com.jsowell.pile.domain; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.jsowell.common.annotation.Excel; +import com.jsowell.common.core.domain.BaseEntity; +import lombok.*; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.Date; + +/** + * 充电桩SIM卡信息对象 pile_sim_info + * + * @author jsowell + * @date 2022-08-26 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PileSimInfo extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 主键 */ + private Long id; + + /** 套餐名称 */ + @Excel(name = "套餐名称") + private String name; + + /** ICCID */ + @Excel(name = "ICCID") + private String iccid; + + /** 状态(0-正常,1-强制断网,2-客户断网,3-超套停,4-服务结束,5-提请销卡,6-销卡) */ + @Excel(name = "状态", readConverterExp = "0=-正常,1-强制断网,2-客户断网,3-超套停,4-服务结束,5-提请销卡,6-销卡") + private String status; + + /** sim卡供应商 */ + @Excel(name = "sim卡供应商") + private String simSupplier; + + /** 套餐总流量 */ + @Excel(name = "套餐总流量") + private String totalData; + + /** 剩余流量 */ + @Excel(name = "剩余流量") + private String surplusData; + + /** 到期时间 */ + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "到期时间", width = 30, dateFormat = "yyyy-MM-dd") + private Date expireTime; + + /** SIM卡运营商(china_telecom - 中国电信, china_mobile - 中国移动, china_unicom - 中国联通) */ + @Excel(name = "SIM卡运营商(china_telecom - 中国电信, china_mobile - 中国移动, china_unicom - 中国联通)") + private String operator; + + /** 删除标识(0-正常;1-删除) */ + private String delFlag; + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileStationInfo.java b/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileStationInfo.java new file mode 100644 index 000000000..eeeca7a6f --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/domain/PileStationInfo.java @@ -0,0 +1,355 @@ +package com.jsowell.pile.domain; + +import com.jsowell.common.annotation.Excel; +import com.jsowell.common.core.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.math.BigDecimal; + +/** + * 充电站信息对象 pile_station_info + * + * @author jsowell + * @date 2022-08-30 + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class PileStationInfo extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * 站点id + */ + private Long id; + + /** + * 运营商id + */ + private Long merchantId; + + /** + * 站点名称 + */ + @Excel(name = "站点名称") + private String stationName; + + private String deptId; + + /** + * 是否独立报桩(0-否;1-是) + */ + @Excel(name = "是否独立报桩", readConverterExp = "0=-否;1-是") + private String aloneApply; + + /** + * 国网电费账单户号 + */ + @Excel(name = "国网电费账单户号") + private String accountNumber; + + /** + * 容量,独立电表申请的功率保留小数点后4位 + */ + @Excel(name = "容量") + private BigDecimal capacity; + + /** + * 公共停车场库(0-否;1-是) + */ + @Excel(name = "公共停车场库(0-否;1-是)") + private String publicParking; + + /** + * 停车场库编号 + */ + @Excel(name = "停车场库编号") + private String parkingNumber; + + /** + * 充电站国家代码 + */ + @Excel(name = "充电站国家代码") + private String countryCode; + + /** + * 充电站省市辖区编码 + */ + @Excel(name = "充电站省市辖区编码") + private String areaCode; + + /** + * 站点地址 + */ + @Excel(name = "站点地址") + private String address; + + /** + * 站点电话 + */ + @Excel(name = "站点电话") + private String stationTel; + + /** + * 服务电话,例如400电话 + */ + @Excel(name = "服务电话,例如400电话") + private String serviceTel; + + /** + * 站点状态【1:公共 + * 50:个人 + * 100:公交(专用) + * 101:环卫(专用) + * 102:物流(专用) + * 103:出租车(专用) + * 104:分时租赁(专用) + * 105:小区共享(专用) + * 106:单位(专用) + * 255:其他 + * 】 + */ + @Excel(name = "站点状态", readConverterExp = "专=用") + private String stationType; + + /** + * 站点状态【0:未知 + * 1:建设中 + * 5:关闭下线 + * 6:维护中 + * 50:正常使用 + * 】 + */ + @Excel(name = "站点状态") + private String stationStatus; + + /** + * 站点管理员名称 + */ + @Excel(name = "站点管理员名称") + private String stationAdminName; + + /** + * 车位数量(默认:0 未知) + */ + @Excel(name = "车位数量(默认:0 未知)") + private String parkNums; + + /** + * 经度GCJ-02坐标系保留小数点后6位 + */ + @Excel(name = "经度GCJ-02坐标系保留小数点后6位") + private String stationLng; + + /** + * 纬度GCJ-02坐标系保留小数点后6位 + */ + @Excel(name = "纬度GCJ-02坐标系保留小数点后6位") + private String stationLat; + + /** + * 站点引导,用于引导车主找到充电车位 + */ + @Excel(name = "站点引导,用于引导车主找到充电车位") + private String siteGuide; + + /** + * 建设场所(1:居民区 + * 2:公共机构 + * 3:企事业单位 + * 4:写字楼 + * 5:工业园区 + * 6:交通枢纽 + * 7:大型文体设施 + * 8:城市绿地 + * 9:大型建筑配建停车场 + * 10:路边停车位 + * 11:城际高速服务区 + * 12:风景区 + * 13:公交场站 + * 14:加油加气站 + * 15:出租车 + * 255:其他 + * ) + */ + @Excel(name = "建设场所") + private String construction; + + /** + * 站点照片 + */ + @Excel(name = "站点照片") + private String pictures; + + /** + * 使用车型描述(描述该站点接受的车大小以及类型,如大巴、物流车、私家乘用车、出租车等 ) + */ + @Excel(name = "使用车型描述(描述该站点接受的车大小以及类型,如大巴、物流车、私家乘用车、出租车等 )") + private String matchCars; + + /** + * 车位楼层及数量描述 + */ + @Excel(name = "车位楼层及数量描述") + private String parkInfo; + + /** + * 停车场产权方 + */ + @Excel(name = "停车场产权方") + private String parkOwner; + + /** + * 停车场管理人(如:XX 物业) + */ + @Excel(name = "停车场管理人", readConverterExp = "如=:XX,物=业") + private String parkManager; + + /** + * 是否全天开放 + * 0:否 + * 1:是 + */ + @Excel(name = "是否全天开放") + private String openAllDay; + + /** + * 营业时间描述 + */ + @Excel(name = "营业时间描述") + private String businessHours; + + /** + * 是否停车免费 + * 0:否 + * 1:是 + */ + @Excel(name = "是否停车免费") + private String parkFree; + + /** + * 支付方式:0-刷卡、1-线上、2-现金 + * 其中电子钱包类卡为刷卡,身份鉴权卡、微信/ 支付宝、APP为线上 + */ + @Excel(name = "支付方式") + private String payment; + + /** + * 是否支持预约 (0为不支持预约、1为支持预约。不填默认为0) + */ + @Excel(name = "是否支持预约 (0为不支持预约、1为支持预约。不填默认为0)") + private String supportOrder; + + /** + * 是否对外开放 (0-否;1-是) + */ + private String publicFlag; + + /** + * 是否营业中(0-否;1-是) + */ + private String openFlag; + + /** + * 是否靠近卫生间 (0-无;1-有) + */ + @Excel(name = "是否靠近卫生间 (0-无;1-有)") + private String toiletFlag; + + /** + * 是否靠近便利店(0-无;1-有) + */ + @Excel(name = "是否靠近便利店(0-无;1-有)") + private String storeFlag; + + /** + * 是否靠近餐厅(0-无;1-有) + */ + @Excel(name = "是否靠近餐厅(0-无;1-有)") + private String restaurantFlag; + + /** + * 是否靠近休息室(0-无;1-有) + */ + @Excel(name = "是否靠近休息室(0-无;1-有)") + private String loungeFlag; + + /** + * 是否有雨棚(0-无;1-有) + */ + @Excel(name = "是否有雨棚(0-无;1-有)") + private String canopyFlag; + + /** + * 是否有小票机 (0-无;1-有) + */ + @Excel(name = "是否有小票机 (0-无;1-有)") + private String printerFlag; + + /** + * 是否有道闸(0-无;1-有) + */ + @Excel(name = "是否有道闸(0-无;1-有)") + private String barrierFlag; + + /** + * 是否有地锁(0-无;1-有) + */ + @Excel(name = "是否有地锁(0-无;1-有)") + private String parkingLockFlag; + + /** + * 删除标识(0-正常;1-删除) + */ + private String delFlag; + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("id", id) + .append("merchantId", merchantId) + .append("stationName", stationName) + .append("aloneApply", aloneApply) + .append("accountNumber", accountNumber) + .append("capacity", capacity) + .append("publicParking", publicParking) + .append("parkingNumber", parkingNumber) + .append("countryCode", countryCode) + .append("areaCode", areaCode) + .append("address", address) + .append("stationTel", stationTel) + .append("serviceTel", serviceTel) + .append("stationType", stationType) + .append("stationStatus", stationStatus) + .append("stationAdminName", stationAdminName) + .append("parkNums", parkNums) + .append("stationLng", stationLng) + .append("stationLat", stationLat) + .append("siteGuide", siteGuide) + .append("construction", construction) + .append("pictures", pictures) + .append("matchCars", matchCars) + .append("parkInfo", parkInfo) + .append("parkOwner", parkOwner) + .append("parkManager", parkManager) + .append("openAllDay", openAllDay) + .append("businessHours", businessHours) + .append("parkFree", parkFree) + .append("payment", payment) + .append("supportOrder", supportOrder) + .append("publicFlag", publicFlag) + .append("openFlag", openFlag) + .append("toiletFlag", toiletFlag) + .append("storeFlag", storeFlag) + .append("restaurantFlag", restaurantFlag) + .append("loungeFlag", loungeFlag) + .append("canopyFlag", canopyFlag) + .append("printerFlag", printerFlag) + .append("barrierFlag", barrierFlag) + .append("parkingLockFlag", parkingLockFlag) + .append("delFlag", delFlag) + .toString(); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/domain/TransactionRecords.java b/jsowell-pile/src/main/java/com/jsowell/pile/domain/TransactionRecords.java new file mode 100644 index 000000000..a8f5bf92a --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/domain/TransactionRecords.java @@ -0,0 +1,178 @@ +package com.jsowell.pile.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 交易流水记录 0x3B + * + * @author JS-ZZA + * @date 2022/11/5 10:15 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TransactionRecords { + /** + * 交易流水号 + */ + private String orderCode; + + /** + * 桩编码 + */ + private String pileSn; + + /** + * 枪号 + */ + private String connectorCode; + + /** + * 开始时间 + */ + private String startTime; + + /** + * 结束时间 + */ + private String endTime; + + /** + * 尖单价 + */ + private String sharpPrice; + + /** + * 尖电量 + */ + private String sharpUsedElectricity; + + /** + * 计损尖电量 + */ + private String sharpPlanLossElectric; + + /** + * 尖金额 + */ + private String sharpAmount; + + /** + * 峰单价 + */ + private String peakPrice; + + /** + * 峰电量 + */ + private String peakUsedElectricity; + + /** + * 计损峰电量 + */ + private String peakPlanLossElectricity; + + /** + * 峰金额 + */ + private String peakAmount; + + /** + * 平单价 + */ + private String flatPrice; + + /** + * 平电量 + */ + private String flatUsedElectricity; + + /** + * 计损平电量 + */ + private String flatPlanLossElectricity; + + /** + * 平金额 + */ + private String flatAmount; + + /** + * 谷单价 + */ + private String valleyPrice; + + /** + * 谷电量 + */ + private String valleyUsedElectricity; + + /** + * 计损谷电量 + */ + private String valleyPlanLossElectricity; + + /** + * 谷金额 + */ + private String valleyAmount; + + /** + * 电表总起值 + */ + private String ammeterTotalStart; + + /** + * 电表总止值 + */ + private String ammeterTotalEnd; + + /** + * 总电量 + */ + private String totalElectricity; + + /** + * 计损总电量 + */ + private String planLossTotalElectricity; + + /** + * 消费金额 + */ + private String consumptionAmount; + + /** + * VIN 码 + */ + private String vinCode; + + /** + * 交易标识 + * 0x01: app 启动 + * 0x02:卡启动 + * 0x04:离线卡启动 + * 0x05: vin 码启动充电 + */ + private String transactionIdentifier; + + /** + * 交易时间 + */ + private String transactionTime; + + /** + * 停止原因 + */ + private String stopReason; + + /** + * 物理卡号 + */ + private String logicCardNum; + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/domain/WxpayCallbackRecord.java b/jsowell-pile/src/main/java/com/jsowell/pile/domain/WxpayCallbackRecord.java new file mode 100644 index 000000000..3c0630f9f --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/domain/WxpayCallbackRecord.java @@ -0,0 +1,102 @@ +package com.jsowell.pile.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 微信支付回调记录表 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class WxpayCallbackRecord { + /** + * 主键id + */ + private Integer id; + + /** + * 支付类型(1-支付订单;2-充值余额) + */ + private String payScenario; + + /** + * 会员id + */ + private String memberId; + + /** + * 充电订单号 + */ + private String orderCode; + + /** + * 微信商户订单号 + */ + private String outTradeNo; + + /** + * 微信支付订单号 + */ + private String transactionId; + + /** + * 商户号 + */ + private String mchId; + + /** + * 应用ID + */ + private String appId; + + /** + * 交易类型 + */ + private String tradeType; + + /** + * 交易状态 + */ + private String tradeState; + + /** + * 交易状态描述 + */ + private String tradeStateDesc; + + /** + * 银行类型 + */ + private String bankType; + + /** + * 附加数据 + */ + private String attach; + + /** + * 支付完成时间 + */ + private LocalDateTime successTime; + + /** + * 支付者信息openId + */ + private String payerOpenId; + + /** + * 用户支付金额(单位分) + */ + private String payerTotal; + + /** + * 创建时间 + */ + private LocalDateTime createTime; +} \ No newline at end of file diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/domain/WxpayRefundCallback.java b/jsowell-pile/src/main/java/com/jsowell/pile/domain/WxpayRefundCallback.java new file mode 100644 index 000000000..0eec11456 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/domain/WxpayRefundCallback.java @@ -0,0 +1,94 @@ +package com.jsowell.pile.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class WxpayRefundCallback { + /** + * 主键 + */ + private Integer id; + + /** + * 会员id + */ + private String memberId; + + /** + * 订单编号 + */ + private String orderCode; + + /** + * 微信商户订单号 + */ + private String outTradeNo; + + /** + * 微信商户退款单号 + */ + private String outRefundNo; + + /** + * 微信支付订单号 + */ + private String transactionId; + + /** + * 微信商户号 + */ + private String mchId; + + /** + * 微信支付退款单号 + */ + private String refundId; + + /** + * 退款状态(SUCCESS:退款成功;CLOSED:退款关闭; ABNORMAL:退款异常) + */ + private String refundStatus; + + /** + * 退款成功时间 + */ + private String successTime; + + /** + * 退款入账账户 + */ + private String userReceivedAccount; + + /** + * 用户支付金额 + */ + private String payerTotal; + + /** + * 用户退款金额 + */ + private String payerRefund; + + /** + * 订单金额 + */ + private String amountTotal; + + /** + * 退款金额 + */ + private String amountRefund; + + /** + * 创建时间 + */ + private LocalDateTime createTime; +} \ No newline at end of file diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/BaseMemberDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/BaseMemberDTO.java new file mode 100644 index 000000000..23e6a7d96 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/BaseMemberDTO.java @@ -0,0 +1,32 @@ +package com.jsowell.pile.dto; + +import lombok.Data; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * uniapp查询用户信息DTO + * + * @author JS-ZZA + * @date 2022/11/19 14:53 + */ +@Data +public class BaseMemberDTO { + /** + * 用户令牌 + */ + private String memberToken; + + /** + * 会员Id + */ + private String memberId; + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("memberToken", memberToken) + .append("memberId", memberId) + .toString(); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/BasicPileDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/BasicPileDTO.java new file mode 100644 index 000000000..57cf3472d --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/BasicPileDTO.java @@ -0,0 +1,42 @@ +package com.jsowell.pile.dto; + +import com.jsowell.common.core.domain.BaseEntity; +import lombok.Data; + +/** + * 充电桩基础DTO + */ +@Data +public class BasicPileDTO extends BaseEntity { + /** + * 站点id + */ + private String stationId; + + /** + * 充电桩id + */ + private String pileId; + + /** + * 设备sn + */ + private String pileSn; + + /** + * 枪口号 + */ + private String connectorCode; + + /** + * 枪口编号 + * 桩编码+枪口号 + */ + private String pileConnectorCode; + + /** + * 充电桩状态 + * 1-在线;2-离线;3-故障 + */ + private String status; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/BatchCreatePileDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/BatchCreatePileDTO.java new file mode 100644 index 000000000..627ecd2d6 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/BatchCreatePileDTO.java @@ -0,0 +1,54 @@ +package com.jsowell.pile.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class BatchCreatePileDTO { + /** + * 运营商id + */ + private String merchantId; + + /** + * 充电站id + */ + private String stationId; + + /** + * 型号id + */ + private String modelId; + + /** + * 软件协议(1-云快充;2-永联) + */ + private String softwareProtocol; + + /** + * 生成日期 + */ + private Date productionDate; + + /** + * 接口数量 + */ + private int connectorNum; + + /** + * 生成台数 + */ + private int num; + + /** + * 备注 + */ + private String remark; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/BillingTimeDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/BillingTimeDTO.java new file mode 100644 index 000000000..461fd825e --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/BillingTimeDTO.java @@ -0,0 +1,31 @@ +package com.jsowell.pile.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@ApiModel("计费模板时段详情") +@Data +public class BillingTimeDTO { + /** + * 时段类型(1-尖时;2-峰时;3-平时;4-谷时) + */ + @ApiModelProperty("时段类型(1-尖时;2-峰时;3-平时;4-谷时)") + private String type; + + /** + * 开始时间 + */ + private String startTime; + + /** + * 结束时间 + */ + private String endTime; + + /** + * 时段 例如:00:00-05:00 + */ + @ApiModelProperty("时段 例如:00:00-05:00") + private String timeDesc; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/CreateOrUpdateBillingTemplateDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/CreateOrUpdateBillingTemplateDTO.java new file mode 100644 index 000000000..bf96723bc --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/CreateOrUpdateBillingTemplateDTO.java @@ -0,0 +1,91 @@ +package com.jsowell.pile.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +// @ApiModel(value = "UserEntity" , description = "用户实体") +@Data +public class CreateOrUpdateBillingTemplateDTO { + /** + * 计费模板id,修改时必传 + */ + private String billingTemplateId; + + private String stationId; + + /** + * 模板名称 + */ + @ApiModelProperty("模板名称") + private String name; + + /** + * 时段类型(1-尖时;2-峰时;3-平时;4-谷时) + */ + @ApiModelProperty("时段类型") + private String type; + + /** + * 尖时段电费 + */ + @ApiModelProperty("尖时段电费") + private BigDecimal electricityPriceA; + + /** + * 尖时段服务费 + */ + @ApiModelProperty("尖时段服务费") + private BigDecimal servicePriceA; + + /** + * 峰时段电费 + */ + @ApiModelProperty("峰时段电费") + private BigDecimal electricityPriceB; + + /** + * 峰时段服务费 + */ + @ApiModelProperty("峰时段服务费") + private BigDecimal servicePriceB; + + /** + * 平时段电费 + */ + @ApiModelProperty("平时段电费") + private BigDecimal electricityPriceC; + + /** + * 平时段服务费 + */ + @ApiModelProperty("平时段服务费") + private BigDecimal servicePriceC; + + /** + * 谷时段电费 + */ + @ApiModelProperty("谷时段电费") + private BigDecimal electricityPriceD; + + /** + * 谷时段服务费 + */ + @ApiModelProperty("谷时段服务费") + private BigDecimal servicePriceD; + + /** + * 备注 + */ + @ApiModelProperty("备注") + private String remark; + + /** + * 时段清单 + */ + @ApiModelProperty("时段清单") + private List timeArray; + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/FastCreateStationDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/FastCreateStationDTO.java new file mode 100644 index 000000000..6960c9ccd --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/FastCreateStationDTO.java @@ -0,0 +1,40 @@ +package com.jsowell.pile.dto; + +import lombok.Data; + +/** + * 快速建站DTO + */ +@Data +public class FastCreateStationDTO { + /** + * 所属运营商id + */ + private String merchantId; + + /** + * 名称 + */ + private String stationName; + + /** + * 地址 + */ + private String address; + + /** + * 区域 + */ + private String areaCode; + + /** + * 站点电话 + */ + private String stationTel; + + /** + * 管理员 + */ + private String stationAdminName; + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/GenerateOrderDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/GenerateOrderDTO.java new file mode 100644 index 000000000..c654da65f --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/GenerateOrderDTO.java @@ -0,0 +1,51 @@ +package com.jsowell.pile.dto; + +import com.jsowell.pile.vo.uniapp.PileConnectorDetailVO; +import com.jsowell.pile.vo.web.BillingTemplateVO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; + +/** + * 生成订单dto + * start_pile_charge + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class GenerateOrderDTO extends BasicPileDTO{ + /** + * 会员id + */ + private String memberId; + + /** + * token + */ + private String memberToken; + + /** + * 启动方式(0-后管启动;1-用户app启动) + */ + private String startMode; + + /** + * 支付方式 1-余额支付;3-白名单支付;4-微信支付;5-支付宝支付 + */ + private String payMode; + + /** + * 充电金额 + */ + private BigDecimal chargeAmount; + + /** + * 充电桩枪口信息 + */ + private PileConnectorDetailVO pileConnector; + + /** + * 计费模板相关信息 + */ + private BillingTemplateVO billingTemplate; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/ImportBillingTemplateDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/ImportBillingTemplateDTO.java new file mode 100644 index 000000000..c4cd45ee8 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/ImportBillingTemplateDTO.java @@ -0,0 +1,15 @@ +package com.jsowell.pile.dto; + +import lombok.Data; + +/** + * 站点导入计费模板dto + */ +@Data +public class ImportBillingTemplateDTO { + // 站点id + private String stationId; + + // 计费模板id + private String billingTemplateId; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/IndexQueryDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/IndexQueryDTO.java new file mode 100644 index 000000000..6f647e812 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/IndexQueryDTO.java @@ -0,0 +1,17 @@ +package com.jsowell.pile.dto; + +import lombok.Data; + +/** + * 首页数据展示DTO + * + * @author JS-ZZA + * @date 2023/2/3 16:11 + */ +@Data +public class IndexQueryDTO { + /** + * 站点id + */ + private String stationId; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/MemberRegisterAndLoginDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/MemberRegisterAndLoginDTO.java new file mode 100644 index 000000000..4d3753d57 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/MemberRegisterAndLoginDTO.java @@ -0,0 +1,37 @@ +package com.jsowell.pile.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class MemberRegisterAndLoginDTO { + /** + * 手机号 + */ + private String mobileNumber; + + /** + * 验证码 + */ + private String verificationCode; + + /** + * 小程序appId + */ + private String appId; + + /** + * 运营商id + */ + private String merchantId; + + /** + * 微信用户openId + */ + private String openId; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/MemberRegisterDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/MemberRegisterDTO.java new file mode 100644 index 000000000..53ba073ae --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/MemberRegisterDTO.java @@ -0,0 +1,38 @@ +package com.jsowell.pile.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 用户注册前台参数 + * + * @author JS-ZZA + * @date 2022/10/27 14:55 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class MemberRegisterDTO { + /** + * 昵称 + */ + private String nickName; + + /** + * 状态 + */ + private String status; + + /** + * 头像url + */ + private String avatarUrl; + + /** + * 手机号码 + */ + private String mobileNumber; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/PayOrderDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/PayOrderDTO.java new file mode 100644 index 000000000..83f301573 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/PayOrderDTO.java @@ -0,0 +1,55 @@ +package com.jsowell.pile.dto; + +import lombok.Data; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.math.BigDecimal; + +/** + * 支付订单DTO + */ +@Data +public class PayOrderDTO { + /** + * 会员id + */ + private String memberId; + + /** + * 订单编号 + */ + private String orderCode; + + /** + * 支付方式 + * @see com.jsowell.common.enums.ykc.OrderPayModeEnum + */ + private String payMode; + + /** + * 支付金额 + */ + private BigDecimal payAmount; + + /** + * 微信支付需要用的code + */ + private String code; + + /** + * redis锁的值 + */ + private String lockValue; + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("memberId", memberId) + .append("orderCode", orderCode) + .append("payMode", payMode) + .append("payAmount", payAmount) + .append("code", code) + .toString(); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/PayOrderSuccessCallbackDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/PayOrderSuccessCallbackDTO.java new file mode 100644 index 000000000..8f6c2167d --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/PayOrderSuccessCallbackDTO.java @@ -0,0 +1,32 @@ +package com.jsowell.pile.dto; + +import com.jsowell.common.enums.ykc.OrderPayModeEnum; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PayOrderSuccessCallbackDTO { + /** + * 订单编号 + */ + private String orderCode; + + /** + * 支付金额 单位: 元 + */ + private BigDecimal payAmount; + + /** + * 支付方式 + * 1-余额支付;2-微信支付;3-支付宝支付 + * @see OrderPayModeEnum + */ + private String payMode; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/PaymentScenarioDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/PaymentScenarioDTO.java new file mode 100644 index 000000000..c4ad3f4da --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/PaymentScenarioDTO.java @@ -0,0 +1,29 @@ +package com.jsowell.pile.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PaymentScenarioDTO { + /** + * 支付场景类型 1-订单支付;2-充值余额 + */ + private String type; + + /** + * 订单编号 + * 当type==1时,orderCode不能为空 + */ + private String orderCode; + + /** + * 会员id + * 当type==2时,memberId不能为空 + */ + private String memberId; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/PileMemberBindingDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/PileMemberBindingDTO.java new file mode 100644 index 000000000..6b498570a --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/PileMemberBindingDTO.java @@ -0,0 +1,32 @@ +package com.jsowell.pile.dto; + +import lombok.Data; + +/** + * 个人桩绑定DTO + * + * @author JS-ZZA + * @date 2023/2/20 16:46 + */ +@Data +public class PileMemberBindingDTO { + /** + * 桩编码 + */ + private String pileSn; + + /** + * 手机号 + */ + private String phoneNumber; + + /** + * 验证码 + */ + private String verificationCode; + + /** + * 用户memberId + */ + private String memberId; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/PublishBillingTemplateDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/PublishBillingTemplateDTO.java new file mode 100644 index 000000000..9bd42c679 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/PublishBillingTemplateDTO.java @@ -0,0 +1,22 @@ +package com.jsowell.pile.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PublishBillingTemplateDTO { + /** + * 站点id + */ + private String stationId; + + /** + * 计费模板id + */ + private String templateId; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/QueryConnectorDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/QueryConnectorDTO.java new file mode 100644 index 000000000..1e0322bc3 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/QueryConnectorDTO.java @@ -0,0 +1,32 @@ +package com.jsowell.pile.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 查询充电枪接收前台参数 + * + * @author JS-ZZA + * @date 2022/8/31 16:41 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class QueryConnectorDTO { + /** + * 充电枪编号 + */ + private String connectorCode; + + /** + * 站点id + */ + private Long stationId; + + private int pageNum; + + private int pageSize; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/QueryConnectorListDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/QueryConnectorListDTO.java new file mode 100644 index 000000000..5db718411 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/QueryConnectorListDTO.java @@ -0,0 +1,50 @@ +package com.jsowell.pile.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class QueryConnectorListDTO { + + /** + * 页码 + */ + private int pageNum; + + /** + * 每页数量 + */ + private int pageSize; + + /** + * 运营商id + */ + private String merchantId; + + /** + * 站点 + */ + private List stationIdList; + + /** + * 充电桩id列表 + */ + private List pileIds; + + /** + * 接口id + */ + private List connectorIdList; + + /** + * 接口编号 + */ + private List connectorCodeList; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/QueryOrderDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/QueryOrderDTO.java new file mode 100644 index 000000000..7d915ef33 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/QueryOrderDTO.java @@ -0,0 +1,58 @@ +package com.jsowell.pile.dto; + +import com.jsowell.common.core.domain.BaseEntity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class QueryOrderDTO extends BaseEntity { + /** + * 充电桩编号 + */ + private String pileSn; + + /** + * 枪口号 + */ + private String connectorCode; + + /** + * 会员id + */ + private String memberId; + + /** + * 订单状态 + */ + private String orderStatus; + + /** + * 订单编号 + */ + private String orderCode; + + /** + * 手机号 + */ + private String mobileNumber; + + /** + * 站点Id + */ + private String stationId; + + /** + * 开始时间 + */ + private String startTime; + + /** + * 结束时间 + */ + private String endTime; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/QueryPersonPileDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/QueryPersonPileDTO.java new file mode 100644 index 000000000..fb067cbda --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/QueryPersonPileDTO.java @@ -0,0 +1,37 @@ +package com.jsowell.pile.dto; + +import lombok.Data; + +/** + * 查询个人桩相关信息DTO + * + * @author JS-ZZA + * @date 2023/2/23 15:27 + */ +@Data +public class QueryPersonPileDTO { + /** + * 会员id + */ + private String memberId; + + /** + * 桩枪口号 + */ + private String pileConnectorCode; + + /** + * 开始时间 + */ + private String startTime; + + /** + * 结束时间 + */ + private String endTime; + + private int pageSize; + + private int pageNum; + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/QueryPileDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/QueryPileDTO.java new file mode 100644 index 000000000..66a8a45bd --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/QueryPileDTO.java @@ -0,0 +1,37 @@ +package com.jsowell.pile.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 接收前端参数 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class QueryPileDTO extends BasicPileDTO{ + + private int pageNum; + + private int pageSize; + + /** + * 订单号 + */ + private String orderCode; + + /** + * 桩编码+枪口号 + */ + private String pileConnectorCode; + + /** + * 桩编码List + */ + private List pileSns; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/QuerySimInfoDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/QuerySimInfoDTO.java new file mode 100644 index 000000000..dd86fd349 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/QuerySimInfoDTO.java @@ -0,0 +1,32 @@ +package com.jsowell.pile.dto; + +import lombok.Data; + +/** + * 后管查询sim卡信息DTO + * + * @author JS-ZZA + * @date 2023/2/17 11:24 + */ +@Data +public class QuerySimInfoDTO { + /** + * 桩编码 + */ + private String pileSn; + + /** + * iccId + */ + private String iccId; + + /** + * 到期时间 + */ + private String expiredTime; + + /** + * sim卡商 + */ + private String simSupplier; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/QueryStationDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/QueryStationDTO.java new file mode 100644 index 000000000..b648682f2 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/QueryStationDTO.java @@ -0,0 +1,59 @@ +package com.jsowell.pile.dto; + +import com.jsowell.common.core.domain.BaseEntity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 站点管理前台参数 + * + * @author JS-ZZA + * @date 2022/9/1 13:25 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class QueryStationDTO extends BaseEntity { + /** + * 站点名称 + */ + private String stationName; + + /** + * 运营商名称 + */ + private String merchantName; + + /** + * 运营商id + */ + private String merchantId; + + /** + * 站点经度 + */ + private String stationLng; + + /** + * 站点纬度 + */ + private String stationLat; + + /** + * 每页数量 + */ + private int pageSize; + + /** + * 页码 + */ + private int pageNum; + + /** + * 是否对外开放(0-否;1-是) + */ + private String publicFlag; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/RemoteUpdatePileFileDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/RemoteUpdatePileFileDTO.java new file mode 100644 index 000000000..878b9e7b7 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/RemoteUpdatePileFileDTO.java @@ -0,0 +1,26 @@ +package com.jsowell.pile.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 远程更新桩 + * + * @author JS-ZZA + * @date 2022/10/21 11:45 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class RemoteUpdatePileFileDTO { + + /** + * 桩编号集合 + */ + private List pileSnList; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/ReplaceMerchantStationDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/ReplaceMerchantStationDTO.java new file mode 100644 index 000000000..743ab30a7 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/ReplaceMerchantStationDTO.java @@ -0,0 +1,42 @@ +package com.jsowell.pile.dto; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import java.util.Date; +import java.util.List; + +@Data +public class ReplaceMerchantStationDTO { + /** + * 运营商id + */ + private String merchantId; + + /** + * 站点id + */ + @NotBlank(message = "站点不能为空") + private String stationId; + + /** + * 充电桩idList + */ + @NotEmpty(message = "充电桩不能为空") + private List pileIdList; + + /** + * 充电桩编号list + */ + private List pileSnList; + + // 型号id + private String modelId; + + // 枪口数量 + private Integer connectorNum; + + private String updateBy; + private Date updateTime; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/SettleOrderDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/SettleOrderDTO.java new file mode 100644 index 000000000..4840edf98 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/SettleOrderDTO.java @@ -0,0 +1,27 @@ +package com.jsowell.pile.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * 结算订单DTO + * + * @author JS-ZZA + * @date 2022/11/15 9:23 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SettleOrderDTO extends BasicPileDTO{ + + /** + * 订单号 + */ + private String orderCode; + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/SimRenewDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/SimRenewDTO.java new file mode 100644 index 000000000..b2771295c --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/SimRenewDTO.java @@ -0,0 +1,24 @@ +package com.jsowell.pile.dto; + +import lombok.Data; + +import java.util.List; + +/** + * sim卡续费DTO + * + * @author JS-ZZA + * @date 2022/12/19 10:36 + */ +@Data +public class SimRenewDTO { + /** + * 卡号集合 + */ + private List iccIds; + + /** + * 续费周期 + */ + private int cycleNumber; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/StartChargingDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/StartChargingDTO.java new file mode 100644 index 000000000..197d044ac --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/StartChargingDTO.java @@ -0,0 +1,29 @@ +package com.jsowell.pile.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +/** + * 启动充电DTO + */ +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class StartChargingDTO extends BasicPileDTO{ + /** + * 会员token + */ + private String memberToken; + + /** + * 充电金额 + */ + private BigDecimal chargeAmount; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/StopChargingDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/StopChargingDTO.java new file mode 100644 index 000000000..2ad98f661 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/StopChargingDTO.java @@ -0,0 +1,26 @@ +package com.jsowell.pile.dto; + +import lombok.Data; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +@Data +public class StopChargingDTO { + /** + * 会员id + */ + private String memberId; + + /** + * 订单编号 + */ + private String orderCode; + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("memberId", memberId) + .append("orderCode", orderCode) + .toString(); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/UniAppQueryMemberBalanceDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/UniAppQueryMemberBalanceDTO.java new file mode 100644 index 000000000..c58b0a66a --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/UniAppQueryMemberBalanceDTO.java @@ -0,0 +1,33 @@ +package com.jsowell.pile.dto; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 小程序查询用户账户余额变动信息 + * + * @author JS-ZZA + * @date 2022/11/26 9:41 + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class UniAppQueryMemberBalanceDTO extends BaseMemberDTO{ + private int pageSize; + private int pageNum; + + /** + * 变动类型 1-进账;2-出账 + */ + private String type; + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("pageSize", pageSize) + .append("pageNum", pageNum) + .append("type", type) + .toString(); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/UniAppQueryOrderDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/UniAppQueryOrderDTO.java new file mode 100644 index 000000000..37932ab24 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/UniAppQueryOrderDTO.java @@ -0,0 +1,24 @@ +package com.jsowell.pile.dto; + +import lombok.Data; + +/** + * 小程序查询订单列表DTO + * + * @author JS-ZZA + * @date 2022/11/25 15:16 + */ +@Data +public class UniAppQueryOrderDTO { + private int pageSize; + private int pageNum; + /** + * 订单状态 1-全部 2-未完成 3-已完成 + */ + private String orderStatus; + + /** + * 订单编号 + */ + private String orderCode; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/WechatLoginDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/WechatLoginDTO.java new file mode 100644 index 000000000..7c3481e4c --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/WechatLoginDTO.java @@ -0,0 +1,18 @@ +package com.jsowell.pile.dto; + +import lombok.Data; + +/** + * 微信登录注册dto + */ +@Data +public class WechatLoginDTO { + private String code; + + private String appId; + + /** + * 用来获取openId的Code + */ + private String openIdCode; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/WeixinPayDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/WeixinPayDTO.java new file mode 100644 index 000000000..cd3e177b4 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/WeixinPayDTO.java @@ -0,0 +1,32 @@ +package com.jsowell.pile.dto; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +@EqualsAndHashCode(callSuper = true) +@Data +public class WeixinPayDTO extends BaseMemberDTO{ + private String openId; + + private String code; + + private String amount; + + private String description; // 微信商品详情 + + /** + * 附加参数 + * json格式,支付回调取出来使用 + */ + private String attach; + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("openId", openId) + .append("code", code) + .toString(); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/MemberBasicInfoMapper.java b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/MemberBasicInfoMapper.java new file mode 100644 index 000000000..632eac41e --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/MemberBasicInfoMapper.java @@ -0,0 +1,110 @@ +package com.jsowell.pile.mapper; + +import com.jsowell.pile.domain.MemberBasicInfo; +import com.jsowell.pile.vo.uniapp.MemberVO; +import com.jsowell.pile.vo.uniapp.MemberWalletLogVO; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Repository; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 会员基础信息Mapper接口 + * + * @author jsowell + * @date 2022-10-12 + */ +@Repository +public interface MemberBasicInfoMapper { + /** + * 查询会员基础信息 + * + * @param id 会员基础信息主键 + * @return 会员基础信息 + */ + public MemberBasicInfo selectMemberBasicInfoById(Integer id); + + /** + * 查询会员基础信息列表 + * + * @param memberBasicInfo 会员基础信息 + * @return 会员基础信息集合 + */ + public List selectMemberBasicInfoList(MemberBasicInfo memberBasicInfo); + + /** + * 新增会员基础信息 + * + * @param memberBasicInfo 会员基础信息 + * @return 结果 + */ + public int insertMemberBasicInfo(MemberBasicInfo memberBasicInfo); + + /** + * 修改会员基础信息 + * + * @param memberBasicInfo 会员基础信息 + * @return 结果 + */ + public int updateMemberBasicInfo(MemberBasicInfo memberBasicInfo); + + /** + * 批量删除会员基础信息 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteMemberBasicInfoByIds(List ids); + + /** + * 通过物理卡号查询基本信息 + * + * @param physicsCard 物理卡号 + * @return 会员基础信息 + */ + MemberVO selectInfoByPhysicsCard(@Param("physicsCard") String physicsCard); + + /** + * 通过手机号和商户id查询会员信息 + * 不同手机号可以在多个运营商下注册账号 + * + * @param mobileNumber 手机号 + * @param merchantId 运营商id + * @return 会员信息 + */ + MemberBasicInfo selectInfoByMobileNumberAndMerchantId(@Param("mobileNumber") String mobileNumber, @Param("merchantId") String merchantId); + + /** + * 通过手机号码查询会员信息 + * + * @param mobileNumber 电话号码 + * @return 会员信息 + */ + // MemberBasicInfo selectInfoByMobileNumber(@Param("mobileNumber") String mobileNumber); + + /** + * 通过会员id查询会员详情 + * @param memberId + * @return + */ + MemberBasicInfo selectInfoByMemberId(String memberId); + + /** + * 更新会员余额 + * @param memberId 会员id + * @param newPrincipalBalance new本金余额 + * @param newGiftBalance new赠送余额 + * @param version 上次查询的版本号 + * @return 更新行数 0-更新失败,1-更新成功 + */ + int updateMemberBalance(@Param("memberId") String memberId, @Param("newPrincipalBalance") BigDecimal newPrincipalBalance, + @Param("newGiftBalance") BigDecimal newGiftBalance, @Param("version") Integer version); + + MemberVO queryMemberInfoByMemberId(String memberId); + + + List selectMemberList(@Param("mobileNumber") String mobileNumber, @Param("nickName") String nickName); + + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/MemberTransactionRecordMapper.java b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/MemberTransactionRecordMapper.java new file mode 100644 index 000000000..13e7328fa --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/MemberTransactionRecordMapper.java @@ -0,0 +1,13 @@ +package com.jsowell.pile.mapper; + +import com.jsowell.pile.domain.MemberTransactionRecord; + +import java.util.List; + +public interface MemberTransactionRecordMapper { + + int insertSelective(MemberTransactionRecord record); + + List selectByMemberId(String memberId); + +} \ No newline at end of file diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/MemberWalletInfoMapper.java b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/MemberWalletInfoMapper.java new file mode 100644 index 000000000..9c714b016 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/MemberWalletInfoMapper.java @@ -0,0 +1,21 @@ +package com.jsowell.pile.mapper; + +import com.jsowell.pile.domain.MemberWalletInfo; +import org.springframework.stereotype.Repository; + +@Repository +public interface MemberWalletInfoMapper { + int deleteByPrimaryKey(Integer id); + + int insert(MemberWalletInfo record); + + int insertSelective(MemberWalletInfo record); + + MemberWalletInfo selectByPrimaryKey(Integer id); + + MemberWalletInfo selectByMemberId(String memberId); + + int updateByPrimaryKeySelective(MemberWalletInfo record); + + int updateByPrimaryKey(MemberWalletInfo record); +} \ No newline at end of file diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/MemberWalletLogMapper.java b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/MemberWalletLogMapper.java new file mode 100644 index 000000000..c085f190d --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/MemberWalletLogMapper.java @@ -0,0 +1,33 @@ +package com.jsowell.pile.mapper; + + +import com.jsowell.pile.domain.MemberWalletLog; +import com.jsowell.pile.vo.uniapp.MemberWalletLogVO; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface MemberWalletLogMapper { + // int deleteByPrimaryKey(Integer id); + + // int insert(MemberWalletLog record); + + // int insertSelective(MemberWalletLog record); + + // MemberWalletLog selectByPrimaryKey(Integer id); + + // int updateByPrimaryKeySelective(MemberWalletLog record); + + // int updateByPrimaryKey(MemberWalletLog record); + + void batchInsert(@Param("list") List logList); + + /** + * 查询用户账户余额变动信息 + * @param memberId 会员id + * @param type 1-进账;2-出账 不传查全部 + */ + List getMemberBalanceChanges(@Param("memberId") String memberId, @Param("type") String type); +} \ No newline at end of file diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/OrderAbnormalRecordMapper.java b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/OrderAbnormalRecordMapper.java new file mode 100644 index 000000000..bec9190e0 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/OrderAbnormalRecordMapper.java @@ -0,0 +1,56 @@ +package com.jsowell.pile.mapper; + +import com.jsowell.pile.domain.OrderAbnormalRecord; + +import java.util.List; + +public interface OrderAbnormalRecordMapper { + + /** + * 查询订单异常记录 + * + * @param id 订单异常记录主键 + * @return 订单异常记录 + */ + public OrderAbnormalRecord selectOrderAbnormalRecordById(Integer id); + + /** + * 查询订单异常记录列表 + * + * @param orderAbnormalRecord 订单异常记录 + * @return 订单异常记录集合 + */ + public List selectOrderAbnormalRecordList(OrderAbnormalRecord orderAbnormalRecord); + + /** + * 新增订单异常记录 + * + * @param orderAbnormalRecord 订单异常记录 + * @return 结果 + */ + public int insertOrderAbnormalRecord(OrderAbnormalRecord orderAbnormalRecord); + + /** + * 修改订单异常记录 + * + * @param orderAbnormalRecord 订单异常记录 + * @return 结果 + */ + public int updateOrderAbnormalRecord(OrderAbnormalRecord orderAbnormalRecord); + + /** + * 删除订单异常记录 + * + * @param id 订单异常记录主键 + * @return 结果 + */ + public int deleteOrderAbnormalRecordById(Integer id); + + /** + * 批量删除订单异常记录 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteOrderAbnormalRecordByIds(Integer[] ids); +} \ No newline at end of file diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/OrderBasicInfoMapper.java b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/OrderBasicInfoMapper.java new file mode 100644 index 000000000..4c8008bd6 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/OrderBasicInfoMapper.java @@ -0,0 +1,185 @@ +package com.jsowell.pile.mapper; + +import com.jsowell.pile.domain.OrderBasicInfo; +import com.jsowell.pile.domain.OrderDetail; +import com.jsowell.pile.dto.IndexQueryDTO; +import com.jsowell.pile.dto.QueryOrderDTO; +import com.jsowell.pile.dto.QueryPersonPileDTO; +import com.jsowell.pile.vo.uniapp.OrderVO; +import com.jsowell.pile.vo.uniapp.PersonPileConnectorSumInfoVO; +import com.jsowell.pile.vo.uniapp.SendMessageVO; +import com.jsowell.pile.vo.web.IndexOrderInfoVO; +import com.jsowell.pile.vo.web.OrderListVO; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 订单Mapper接口 + * + * @author jsowell + * @date 2022-09-30 + */ +@Repository +public interface OrderBasicInfoMapper { + /** + * 查询订单 + * + * @param id 订单主键 + * @return 订单 + */ + public OrderBasicInfo selectOrderBasicInfoById(Long id); + + /** + * 条件查询订单基础信息 + * @param info + * @return + */ + OrderBasicInfo getOrderBasicInfo(OrderBasicInfo info); + + /** + * 查询订单列表 + * + * @param orderBasicInfo 订单 + * @return 订单集合 + */ + public List selectOrderBasicInfoList(QueryOrderDTO orderBasicInfo); + + /** + * 新增订单 + * + * @param orderBasicInfo 订单 + * @return 结果 + */ + public int insertOrderBasicInfo(OrderBasicInfo orderBasicInfo); + + /** + * 修改订单 + * + * @param orderBasicInfo 订单 + * @return 结果 + */ + public int updateOrderBasicInfo(OrderBasicInfo orderBasicInfo); + + /** + * 批量删除订单 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteOrderBasicInfoByIds(Long[] ids); + + /** + * 批量删除订单详情 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteOrderDetailByOrderCodes(Long[] ids); + + /** + * 批量新增订单详情 + * + * @param orderDetailList 订单详情列表 + * @return 结果 + */ + public int batchOrderDetail(List orderDetailList); + + + /** + * 通过订单主键删除订单详情信息 + * + * @param id 订单ID + * @return 结果 + */ + public int deleteOrderDetailByOrderCode(Long id); + + /** + * 修改订单详情 + * @param orderDetail 订单详情 + */ + void updateOrderDetail(OrderDetail orderDetail); + + /** + * 通过订单号查询订单基本信息 + * + * @param orderCode 订单号 + * @return + */ + OrderBasicInfo getOrderInfoByOrderCode(String orderCode); + + /** + * 根据桩编号和枪口号查询某状态订单 + * + * @param pileSn 桩编号 + * @param connectorCode 枪口号 + * @param orderStatus 订单状态 + * @return 订单 + */ + OrderBasicInfo queryOrderBasicInfo(@Param("pileSn") String pileSn, @Param("connectorCode") String connectorCode, @Param("orderStatus") String orderStatus); + + /** + * 通过订单号查询订单详情 + * + * @param orderCode 订单号 + * @return 订单详情 + */ + OrderDetail getOrderDetailByOrderCode(@Param("orderCode") String orderCode); + + /** + * 通过会员Id和订单状态查询订单信息 + * + * @param memberId 会员id + * @param orderStatusList 订单状态集合 + * @return + */ + List getListByMemberIdAndOrderStatus(@Param("memberId") String memberId, @Param("orderStatusList") List orderStatusList); + + /** + * 将某订单修改为某状态 + * @param orderCode 订单号 + * @param orderStatus 修改为某状态 + */ + void updateOrderStatusByOrderCode(@Param("orderCode") String orderCode, @Param("orderStatus") String orderStatus); + + + /** + * 首页订单数据展示 + * @param dto 首页信息查询dto + * @return + */ + List getIndexOrderInfo(@Param("dto") IndexQueryDTO dto); + + /** + * 获取超过15分钟的待支付状态订单 + * @return + */ + List getUnpaidOrderListOver15Min(@Param("createTime") String createTime); + + /** + * 根据orderId批量修改订单状态 + * @param orderIds + * @param orderStatus + */ + void updateOrderStatusById(@Param("orderIds") List orderIds, @Param("orderStatus") String orderStatus); + + /** + * 通过订单号查询订单信息(小程序发送消息用) + * @param orderCode + * @return + */ + SendMessageVO selectOrderInfoByOrderCode(@Param("orderCode") String orderCode); + + List selectOrderListByTimeRangeAndStatus(@Param("startTime") String startTime, + @Param("endTime") String endTime, + @Param("orderStatus") String orderStatus, + @Param("payStatus") String payStatus); + + /** + * 个人桩查询充电数据 + * @param dto + * @return + */ + List getAccumulativeInfo(QueryPersonPileDTO dto); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/OrderPayRecordMapper.java b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/OrderPayRecordMapper.java new file mode 100644 index 000000000..d64c0e2a5 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/OrderPayRecordMapper.java @@ -0,0 +1,24 @@ +package com.jsowell.pile.mapper; + +import com.jsowell.pile.domain.OrderPayRecord; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface OrderPayRecordMapper { + int deleteByPrimaryKey(Integer id); + + // int insert(OrderPayRecord record); + + // int insertSelective(OrderPayRecord record); + + OrderPayRecord selectByPrimaryKey(Integer id); + + int updateByPrimaryKeySelective(OrderPayRecord record); + + int updateByPrimaryKey(OrderPayRecord record); + + int batchInsert(@Param("payRecordList") List payRecordList); + + List getOrderPayRecordList(@Param("orderCode") String orderCode); +} \ No newline at end of file diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileBasicInfoMapper.java b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileBasicInfoMapper.java new file mode 100644 index 000000000..0e032eb99 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileBasicInfoMapper.java @@ -0,0 +1,146 @@ +package com.jsowell.pile.mapper; + +import com.jsowell.pile.domain.PileBasicInfo; +import com.jsowell.pile.dto.IndexQueryDTO; +import com.jsowell.pile.vo.uniapp.PersonalPileInfoVO; +import com.jsowell.pile.vo.web.IndexGeneralSituationVO; +import com.jsowell.pile.dto.QueryPileDTO; +import com.jsowell.pile.dto.ReplaceMerchantStationDTO; +import com.jsowell.pile.vo.web.PileDetailVO; +import com.jsowell.pile.vo.uniapp.PileConnectorDetailVO; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 设备管理Mapper接口 + * + * @author jsowell + * @date 2022-08-26 + */ +@Repository +public interface PileBasicInfoMapper { + /** + * 查询设备管理 + * + * @param id 设备管理主键 + * @return 设备管理 + */ + PileBasicInfo selectPileBasicInfoById(Long id); + + PileBasicInfo selectPileBasicInfoBySn(String pileSn); + + /** + * 查询设备管理列表 + * + * @param pileBasicInfo 设备管理 + * @return 设备管理集合 + */ + List selectPileBasicInfoList(PileBasicInfo pileBasicInfo); + + /** + * 新增设备管理 + * + * @param pileBasicInfo 设备管理 + * @return 结果 + */ + int insertPileBasicInfo(PileBasicInfo pileBasicInfo); + + /** + * 批量保存 + * + * @param pileBasicInfoList + * @return + */ + int batchInsertPileBasicInfo(@Param("infoList") List pileBasicInfoList); + + /** + * 修改设备管理 + * + * @param pileBasicInfo 设备管理 + * @return 结果 + */ + int updatePileBasicInfo(PileBasicInfo pileBasicInfo); + + /** + * 删除设备管理 + * + * @param id 设备管理主键 + * @return 结果 + */ + int deletePileBasicInfoById(Long id); + + /** + * 批量删除设备管理 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + int deletePileBasicInfoByIds(Long[] ids); + + /** + * 查询设备信息 + * + * @param dto 查询条件实体类 + * @return 设备信息对象集合 + */ + List queryPileInfos(@Param("dto") QueryPileDTO dto); + + /** + * 通过站点ids查询桩信息 + * + * @param stationIds 站点id + * @return 桩对象集合 + */ + List selectPileListByStationIds(@Param("stationIds") List stationIds); + + /** + * 通过pileId更换站点、运营商信息 + * + * @param dto + * @return + */ + int replaceMerchantStationByPileIds(ReplaceMerchantStationDTO dto); + + /** + * 通过桩id查询basic信息 + * + * @param id 桩id + * @return PileBasic对象集合 + */ + PileDetailVO selectBasicInfoById(Long id); + + /** + * 通过idList批量查询 + * + * @param pileIdList + * @return + */ + List selectByIdList(@Param("pileIdList") List pileIdList); + + /** + * 查询充电桩信息 + * + * @param pileConnectorCode 充电枪编号 + * @return + */ + PileConnectorDetailVO queryPileConnectorDetail(@Param("pileConnectorCode") String pileConnectorCode); + + + /** + * 后管首页基本信息查询 + * + * @param dto 站点Id + * @return 首页基本信息 + */ + public IndexGeneralSituationVO getGeneralSituation(@Param("IndexQueryDTO")IndexQueryDTO dto); + + + /** + * 通过会员id查询个人桩列表 + * @param memberId + * @return + */ + public List getPileInfoByMemberId(String memberId); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileBillingTemplateMapper.java b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileBillingTemplateMapper.java new file mode 100644 index 000000000..3622aacc5 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileBillingTemplateMapper.java @@ -0,0 +1,144 @@ +package com.jsowell.pile.mapper; + +import com.jsowell.pile.domain.PileBillingDetail; +import com.jsowell.pile.domain.PileBillingRelation; +import com.jsowell.pile.domain.PileBillingTemplate; +import com.jsowell.pile.vo.web.BillingTemplateVO; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 计费模板Mapper接口 + * + * @author jsowell + * @date 2022-09-20 + */ +@Repository +public interface PileBillingTemplateMapper { + /** + * 查询计费模板 + * + * @param id 计费模板主键 + * @return 计费模板 + */ + public PileBillingTemplate selectPileBillingTemplateById(Long id); + + /** + * 查询计费模板列表 + * + * @param pileBillingTemplate 计费模板 + * @return 计费模板集合 + */ + public List selectPileBillingTemplateList(PileBillingTemplate pileBillingTemplate); + + /** + * 新增计费模板 + * + * @param pileBillingTemplate 计费模板 + * @return 结果 + */ + public int insertPileBillingTemplate(PileBillingTemplate pileBillingTemplate); + + /** + * 修改计费模板 + * + * @param pileBillingTemplate 计费模板 + * @return 结果 + */ + public int updatePileBillingTemplate(PileBillingTemplate pileBillingTemplate); + + /** + * 删除计费模板 + * + * @param id 计费模板主键 + * @return 结果 + */ + public int deletePileBillingTemplateById(Long id); + + /** + * 批量删除计费模板 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deletePileBillingTemplateByIds(Long[] ids); + + /** + * 批量删除计费模板详情 + * + * @param templateCodes 需要删除的templateCode集合 + * @return 结果 + */ + public int deletePileBillingDetailByTemplateCodes(@Param("templateCodes") List templateCodes); + + /** + * 批量新增计费模板详情 + * + * @param pileBillingDetailList 计费模板详情列表 + * @return 结果 + */ + public int batchPileBillingDetail(List pileBillingDetailList); + + + /** + * 通过计费模板主键删除计费模板详情信息 + * + * @param templateCode 计费模板code + * @return 结果 + */ + public int deletePileBillingDetailByTemplateCode(String templateCode); + + /** + * 查询公共计费模板列表 + * @return + */ + List queryPublicBillingTemplateList(); + + /** + * 根据站点id查询站点计费模板列表 + * 根据发布时间倒序,最新一条就是目前正在使用的计费模板 + * @param stationId 站点id + * @return + */ + List queryStationBillingTemplateList(String stationId); + + /** + * 通过桩sn号查询计费模板 + * + * @param pileSn 桩sn + * @return 计费模板编号 + */ + BillingTemplateVO selectBillingTemplateByPileSn(@Param("pileSn") String pileSn); + + /** + * 通过计费模板id查询 + * @param templateId 计费模板id + * @return + */ + BillingTemplateVO selectBillingTemplateByTemplateId(@Param("templateId") String templateId); + + /** + * 查询站点最新发布的计费模板 + * @param stationId + * @return + */ + BillingTemplateVO selectBillingTemplateByStationId(@Param("stationId") String stationId); + + /** + * 通过模板id查询计费模板详情列表 + */ + List queryBillingDetailByTemplateIds(@Param("templateIds") Long[] templateIds); + + /** + * 插入充电桩和计费模板关系 + */ + void insertPileBillingRelation(List relationList); + + /** + * 根据桩号删除计费模板关系 + * @param pileSnList + */ + void deleteRelationByPileSn(@Param("pileSnList") List pileSnList); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileConnectorInfoMapper.java b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileConnectorInfoMapper.java new file mode 100644 index 000000000..5f461ab98 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileConnectorInfoMapper.java @@ -0,0 +1,141 @@ +package com.jsowell.pile.mapper; + +import com.jsowell.pile.domain.PileConnectorInfo; +import com.jsowell.pile.dto.QueryConnectorDTO; +import com.jsowell.pile.vo.base.ConnectorInfoVO; +import com.jsowell.pile.vo.web.PileConnectorInfoVO; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 充电桩枪口信息Mapper接口 + * + * @author jsowell + * @date 2022-08-31 + */ +@Repository +public interface PileConnectorInfoMapper { + /** + * 查询充电桩枪口信息 + * + * @param id 充电桩枪口信息主键 + * @return 充电桩枪口信息 + */ + public PileConnectorInfo selectPileConnectorInfoById(Integer id); + + /** + * 查询充电桩枪口信息列表 + * + * @param pileConnectorInfo 充电桩枪口信息 + * @return 充电桩枪口信息集合 + */ + public List selectPileConnectorInfoList(PileConnectorInfo pileConnectorInfo); + + /** + * 新增充电桩枪口信息 + * + * @param pileConnectorInfo 充电桩枪口信息 + * @return 结果 + */ + // public int insertPileConnectorInfo(PileConnectorInfo pileConnectorInfo); + + /** + * 批量新增充电桩枪口信息 + * + * @param pileConnectorInfoList 充电桩枪口集合 + * @return 结果 + */ + public int batchInsertConnectorInfo(@Param("connectorList") List pileConnectorInfoList); + + /** + * 修改充电桩枪口信息 + * + * @param pileConnectorInfo 充电桩枪口信息 + * @return 结果 + */ + public int updatePileConnectorInfo(PileConnectorInfo pileConnectorInfo); + + /** + * 删除充电桩枪口信息 + * + * @param id 充电桩枪口信息主键 + * @return 结果 + */ + // public int deletePileConnectorInfoById(Integer id); + + /** + * 批量删除充电桩枪口信息 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + // public int deletePileConnectorInfoByIds(Integer[] ids); + + /** + * 根据充电桩编号删除充电桩枪口信息 + * @param pileSnList + * @return + */ + int deletePileConnectorInfoByPileSnList(List pileSnList); + + /** + * 获取充电接口信息列表 + * + * @param dto 前台参数 + * @return 充电接口对象集合 + */ + List getConnectorInfoList(@Param("connectorDTO") QueryConnectorDTO dto); + + /** + * 通过站点id查询充电枪信息 + * + * @param stationId 站点id + * @return 充电枪设备集合 + */ + List selectConnectorListByStationId(@Param("stationId") Long stationId); + + /** + * 通过充电桩sn或connectorId 查询充电桩接口列表 + * + * @param pileSns 桩编号列表 + * @param connectorIds 枪口号列表 + * @return 充电桩接口列表 + */ + List getPileConnectorInfoList(@Param("pileSns") List pileSns, @Param("connectorIds") List connectorIds, @Param("connectorCodes") List connectorCodeList); + + /** + * 根据枪口号 修改枪口状态 + * + * @param connectorCode 枪口号 + * @param connectorStatus 状态 + */ + int updateConnectorStatus(@Param("connectorCode") String connectorCode, @Param("connectorStatus") String connectorStatus); + + /** + * 根据桩编号修改枪口状态 + * + * @param pileSn 桩编号 + * @param connectorStatus 状态 + * @return 修改数量 + */ + int updateConnectorStatusByPileSn(@Param("pileSn") String pileSn, @Param("connectorStatus") String connectorStatus); + + /** + * 查询枪口信息 + * @param pileConnectorCode 枪口号 + * @return 枪口信息 + */ + PileConnectorInfoVO getPileConnectorInfoByConnectorCode(@Param("pileConnectorCode") String pileConnectorCode); + + + /** + * uniApp通过站点id查询枪口列表信息 + * + * @param stationId 站点id + * @return + */ + List getUniAppConnectorList(@Param("stationId") Long stationId); + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileLicenceInfoMapper.java b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileLicenceInfoMapper.java new file mode 100644 index 000000000..58d689512 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileLicenceInfoMapper.java @@ -0,0 +1,61 @@ +package com.jsowell.pile.mapper; + +import com.jsowell.pile.domain.PileLicenceInfo; + +import java.util.List; + +/** + * 充电桩证书信息Mapper接口 + * + * @author jsowell + * @date 2022-08-27 + */ +public interface PileLicenceInfoMapper { + /** + * 查询充电桩证书信息 + * + * @param id 充电桩证书信息主键 + * @return 充电桩证书信息 + */ + public PileLicenceInfo selectPileLicenceInfoById(Long id); + + /** + * 查询充电桩证书信息列表 + * + * @param pileLicenceInfo 充电桩证书信息 + * @return 充电桩证书信息集合 + */ + public List selectPileLicenceInfoList(PileLicenceInfo pileLicenceInfo); + + /** + * 新增充电桩证书信息 + * + * @param pileLicenceInfo 充电桩证书信息 + * @return 结果 + */ + public int insertPileLicenceInfo(PileLicenceInfo pileLicenceInfo); + + /** + * 修改充电桩证书信息 + * + * @param pileLicenceInfo 充电桩证书信息 + * @return 结果 + */ + public int updatePileLicenceInfo(PileLicenceInfo pileLicenceInfo); + + /** + * 删除充电桩证书信息 + * + * @param id 充电桩证书信息主键 + * @return 结果 + */ + public int deletePileLicenceInfoById(Long id); + + /** + * 批量删除充电桩证书信息 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deletePileLicenceInfoByIds(Long[] ids); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileMemberRelationMapper.java b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileMemberRelationMapper.java new file mode 100644 index 000000000..672042776 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileMemberRelationMapper.java @@ -0,0 +1,74 @@ +package com.jsowell.pile.mapper; + +import java.util.List; +import com.jsowell.pile.domain.PileMemberRelation; +import com.jsowell.pile.vo.uniapp.PersonalPileInfoVO; +import org.springframework.stereotype.Repository; + +/** + * 桩与用户绑定关系Mapper接口 + * + * @author jsowell + * @date 2023-02-21 + */ +@Repository +public interface PileMemberRelationMapper +{ + /** + * 查询桩与用户绑定关系 + * + * @param id 桩与用户绑定关系主键 + * @return 桩与用户绑定关系 + */ + public PileMemberRelation selectPileMemberRelationById(Integer id); + + /** + * 查询桩与用户绑定关系列表 + * + * @param pileMemberRelation 桩与用户绑定关系 + * @return 桩与用户绑定关系集合 + */ + public List selectPileMemberRelationList(PileMemberRelation pileMemberRelation); + + /** + * 条件查询桩与用户绑定关系 + * + * @param pileMemberRelation + * @return + */ + PileMemberRelation selectPileMemberRelation(PileMemberRelation pileMemberRelation); + + /** + * 新增桩与用户绑定关系 + * + * @param pileMemberRelation 桩与用户绑定关系 + * @return 结果 + */ + public int insertPileMemberRelation(PileMemberRelation pileMemberRelation); + + /** + * 修改桩与用户绑定关系 + * + * @param pileMemberRelation 桩与用户绑定关系 + * @return 结果 + */ + public int updatePileMemberRelation(PileMemberRelation pileMemberRelation); + + /** + * 删除桩与用户绑定关系 + * + * @param id 桩与用户绑定关系主键 + * @return 结果 + */ + public int deletePileMemberRelationById(Integer id); + + /** + * 批量删除桩与用户绑定关系 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deletePileMemberRelationByIds(Integer[] ids); + + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileMerchantInfoMapper.java b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileMerchantInfoMapper.java new file mode 100644 index 000000000..1363b5b4a --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileMerchantInfoMapper.java @@ -0,0 +1,70 @@ +package com.jsowell.pile.mapper; + +import com.jsowell.pile.domain.PileMerchantInfo; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 充电桩运营商信息Mapper接口 + * + * @author jsowell + * @date 2022-08-27 + */ +@Repository +public interface PileMerchantInfoMapper { + /** + * 查询充电桩运营商信息 + * + * @param id 充电桩运营商信息主键 + * @return 充电桩运营商信息 + */ + public PileMerchantInfo selectPileMerchantInfoById(Long id); + + /** + * 通过appid查询充电桩运营商信息 + * @param appId + * @return + */ + PileMerchantInfo selectPileMerchantInfoByAppId(String appId); + + /** + * 查询充电桩运营商信息列表 + * + * @param pileMerchantInfo 充电桩运营商信息 + * @return 充电桩运营商信息集合 + */ + public List selectPileMerchantInfoList(PileMerchantInfo pileMerchantInfo); + + /** + * 新增充电桩运营商信息 + * + * @param pileMerchantInfo 充电桩运营商信息 + * @return 结果 + */ + public int insertPileMerchantInfo(PileMerchantInfo pileMerchantInfo); + + /** + * 修改充电桩运营商信息 + * + * @param pileMerchantInfo 充电桩运营商信息 + * @return 结果 + */ + public int updatePileMerchantInfo(PileMerchantInfo pileMerchantInfo); + + /** + * 删除充电桩运营商信息 + * + * @param id 充电桩运营商信息主键 + * @return 结果 + */ + public int deletePileMerchantInfoById(Long id); + + /** + * 批量删除充电桩运营商信息 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deletePileMerchantInfoByIds(Long[] ids); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileModelInfoMapper.java b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileModelInfoMapper.java new file mode 100644 index 000000000..1b9d4944d --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileModelInfoMapper.java @@ -0,0 +1,73 @@ +package com.jsowell.pile.mapper; + +import com.jsowell.pile.domain.PileModelInfo; +import com.jsowell.pile.vo.web.PileModelInfoVO; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 充电桩型号信息Mapper接口 + * + * @author jsowell + * @date 2022-08-26 + */ +@Repository +public interface PileModelInfoMapper { + /** + * 查询充电桩型号信息 + * + * @param id 充电桩型号信息主键 + * @return 充电桩型号信息 + */ + public PileModelInfo selectPileModelInfoById(Long id); + + /** + * 查询充电桩型号信息列表 + * + * @param pileModelInfo 充电桩型号信息 + * @return 充电桩型号信息集合 + */ + public List selectPileModelInfoList(PileModelInfo pileModelInfo); + + /** + * 新增充电桩型号信息 + * + * @param pileModelInfo 充电桩型号信息 + * @return 结果 + */ + public int insertPileModelInfo(PileModelInfo pileModelInfo); + + /** + * 修改充电桩型号信息 + * + * @param pileModelInfo 充电桩型号信息 + * @return 结果 + */ + public int updatePileModelInfo(PileModelInfo pileModelInfo); + + /** + * 删除充电桩型号信息 + * + * @param id 充电桩型号信息主键 + * @return 结果 + */ + public int deletePileModelInfoById(Long id); + + /** + * 批量删除充电桩型号信息 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deletePileModelInfoByIds(Long[] ids); + + /** + * 通过桩编号集合获取型号表中数据 + * + * @param pileSns 桩编号集合 + * @return PileModelInfo对象 + */ + List getPileModelInfoByPileSnList(@Param("pileSnList") List pileSns); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileMsgRecordMapper.java b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileMsgRecordMapper.java new file mode 100644 index 000000000..4c81951e1 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileMsgRecordMapper.java @@ -0,0 +1,32 @@ +package com.jsowell.pile.mapper; + +import com.jsowell.pile.domain.PileMsgRecord; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface PileMsgRecordMapper { + int insertSelective(PileMsgRecord record); + + // PileMsgRecord selectByPrimaryKey(Integer id); + + // PileMsgRecord getByPileSn(String pileSn); + + // PileMsgRecord getByConnectorCode(String connectorCode); + + /** + * 根据枪口号查询枪口日志 + * @param connectorCodeList + * @return + */ + // List getByConnectorCodeList(@Param("connectorCodeList") List connectorCodeList); + + /** + * 查询充电桩通信日志 + * @param pileSn 桩号 + * @return + */ + List getPileFeedList(@Param("pileSn") String pileSn); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileSimInfoMapper.java b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileSimInfoMapper.java new file mode 100644 index 000000000..3a2d38e75 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileSimInfoMapper.java @@ -0,0 +1,96 @@ +package com.jsowell.pile.mapper; + +import com.jsowell.pile.domain.PileSimInfo; +import com.jsowell.pile.dto.QuerySimInfoDTO; +import com.jsowell.pile.vo.web.SimCardInfoVO; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 充电桩SIM卡信息Mapper接口 + * + * @author jsowell + * @date 2022-08-26 + */ +@Component +public interface PileSimInfoMapper { + /** + * 查询充电桩SIM卡信息 + * + * @param id 充电桩SIM卡信息主键 + * @return 充电桩SIM卡信息 + */ + public PileSimInfo selectPileSimInfoById(Long id); + + /** + * 查询充电桩SIM卡信息列表 + * + * @param pileSimInfo 充电桩SIM卡信息 + * @return 充电桩SIM卡信息集合 + */ + public List selectPileSimInfoList(PileSimInfo pileSimInfo); + + /** + * 后管查询sim卡信息列表 + * @return + */ + List getSimInfoList(@Param("dto") QuerySimInfoDTO dto); + + /** + * 新增充电桩SIM卡信息 + * + * @param pileSimInfo 充电桩SIM卡信息 + * @return 结果 + */ + public int insertPileSimInfo(PileSimInfo pileSimInfo); + + /** + * 修改充电桩SIM卡信息 + * + * @param pileSimInfo 充电桩SIM卡信息 + * @return 结果 + */ + public int updatePileSimInfo(PileSimInfo pileSimInfo); + + /** + * 删除充电桩SIM卡信息 + * + * @param id 充电桩SIM卡信息主键 + * @return 结果 + */ + public int deletePileSimInfoById(Long id); + + /** + * 批量删除充电桩SIM卡信息 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deletePileSimInfoByIds(Long[] ids); + + + /** + * 通过桩编码查询sim卡信息 + * @param pileSn 桩编码 + * @return + */ + SimCardInfoVO querySimCardInfoByPileSn(String pileSn); + + /** + * 通过卡号批量查询sim卡信息 + * @param iccIds 卡号集合 + * @return + */ + List selectSimInfoByIccIds(@Param("iccIds") List iccIds); + + /** + * 通过卡号查询sim卡信息 + * @param iccId 卡号 + * @return + */ + PileSimInfo getBasicInfoByIccId(@Param("iccId") String iccId); + + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileStationInfoMapper.java b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileStationInfoMapper.java new file mode 100644 index 000000000..294303459 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/PileStationInfoMapper.java @@ -0,0 +1,74 @@ +package com.jsowell.pile.mapper; + +import com.jsowell.pile.domain.PileStationInfo; +import com.jsowell.pile.dto.QueryStationDTO; +import com.jsowell.pile.vo.web.PileStationVO; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 充电站信息Mapper接口 + * + * @author jsowell + * @date 2022-08-30 + */ +@Repository +public interface PileStationInfoMapper { + /** + * 查询充电站信息 + * + * @param id 充电站信息主键 + * @return 充电站信息 + */ + public PileStationInfo selectPileStationInfoById(Long id); + + /** + * 查询充电站信息列表 + * + * @param pileStationInfo 充电站信息 + * @return 充电站信息集合 + */ + public List selectPileStationInfoList(PileStationInfo pileStationInfo); + + /** + * 通过运营商id查询站点信息 + * + * @param merchantId 运营商id + * @return 站点信息列表 + */ + public List selectStationListByMerchantId(Long merchantId); + + /** + * 新增充电站信息 + * + * @param pileStationInfo 充电站信息 + * @return 结果 + */ + public int insertPileStationInfo(PileStationInfo pileStationInfo); + + /** + * 修改充电站信息 + * + * @param pileStationInfo 充电站信息 + * @return 结果 + */ + public int updatePileStationInfo(PileStationInfo pileStationInfo); + + /** + * 批量删除充电站信息 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deletePileStationInfoByIds(Long[] ids); + + /** + * 查询充电站信息 + * + * @param dto 前台参数 + * @return 充电站对象集合 + */ + List queryStationInfos(@Param("stationDTO") QueryStationDTO dto); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/WxpayCallbackRecordMapper.java b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/WxpayCallbackRecordMapper.java new file mode 100644 index 000000000..a907ed2e6 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/WxpayCallbackRecordMapper.java @@ -0,0 +1,39 @@ +package com.jsowell.pile.mapper; + + +import com.jsowell.pile.domain.WxpayCallbackRecord; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDateTime; +import java.util.List; + +public interface WxpayCallbackRecordMapper { + int deleteByPrimaryKey(Integer id); + + int insert(WxpayCallbackRecord record); + + int insertSelective(WxpayCallbackRecord record); + + WxpayCallbackRecord selectByPrimaryKey(Integer id); + + int updateByPrimaryKeySelective(WxpayCallbackRecord record); + + int updateByPrimaryKey(WxpayCallbackRecord record); + + /** + * 通过订单号查询支付记录 + * @param orderCode + * @return + */ + WxpayCallbackRecord selectByOrderCode(String orderCode); + + /** + * 根据会员id和时间查询支付记录 + * @param memberId 会员id + * @param date 日期,查询该日期之后的记录 + * @return + */ + List selectBalanceRechargeRecord(@Param("memberId") String memberId, @Param("date") LocalDateTime date); + + WxpayCallbackRecord selectByOutTradeNo(@Param("outTradeNo") String outTradeNo); +} \ No newline at end of file diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/WxpayRefundCallbackMapper.java b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/WxpayRefundCallbackMapper.java new file mode 100644 index 000000000..29dbd86e0 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/WxpayRefundCallbackMapper.java @@ -0,0 +1,17 @@ +package com.jsowell.pile.mapper; + +import com.jsowell.pile.domain.WxpayRefundCallback; + +public interface WxpayRefundCallbackMapper { + int deleteByPrimaryKey(Integer id); + + int insert(WxpayRefundCallback record); + + int insertSelective(WxpayRefundCallback record); + + WxpayRefundCallback selectByPrimaryKey(Integer id); + + int updateByPrimaryKeySelective(WxpayRefundCallback record); + + int updateByPrimaryKey(WxpayRefundCallback record); +} \ No newline at end of file diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/IMemberBasicInfoService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/IMemberBasicInfoService.java new file mode 100644 index 000000000..8ffd77fcf --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/IMemberBasicInfoService.java @@ -0,0 +1,118 @@ +package com.jsowell.pile.service; + +import com.jsowell.pile.domain.MemberBasicInfo; +import com.jsowell.pile.vo.uniapp.MemberVO; +import com.jsowell.pile.vo.uniapp.MemberWalletLogVO; +import com.jsowell.pile.vo.uniapp.PersonalPileInfoVO; +import com.jsowell.pile.vo.web.UpdateMemberBalanceDTO; + +import java.util.List; + +/** + * 会员基础信息Service接口 + * + * @author jsowell + * @date 2022-10-12 + */ +public interface IMemberBasicInfoService { + /** + * 查询会员基础信息 + * + * @param id 会员基础信息主键 + * @return 会员基础信息 + */ + public MemberBasicInfo selectMemberBasicInfoById(Integer id); + + /** + * 查询会员基础信息列表 + * + * @param memberBasicInfo 会员基础信息 + * @return 会员基础信息集合 + */ + // public List selectMemberBasicInfoList(MemberBasicInfo memberBasicInfo); + + /** + * 新增会员基础信息 + * + * @param memberBasicInfo 会员基础信息 + * @return 结果 + */ + public int insertMemberBasicInfo(MemberBasicInfo memberBasicInfo); + + /** + * 修改会员基础信息 + * + * @param memberBasicInfo 会员基础信息 + * @return 结果 + */ + public int updateMemberBasicInfo(MemberBasicInfo memberBasicInfo); + + /** + * 批量删除会员基础信息 + * + * @param ids 需要删除的会员基础信息主键集合 + * @return 结果 + */ + public int deleteMemberBasicInfoByIds(List ids); + + + /** + * 通过物理卡号查询基本信息 + * + * @param physicsCard 物理卡号 + * @return 会员基础信息 + */ + MemberVO selectInfoByPhysicsCard(String physicsCard); + + /** + * 根据手机号和运营商id查询会员信息 + * @param phone 手机号 mobile number + * @param merchantId 运营商id + * @return 会员信息 + */ + MemberBasicInfo selectInfoByMobileNumberAndMerchantId(String phone, String merchantId); + + /** + * 根据手机号查询会员信息 + * @param mobileNumber 手机号 + * @return + */ + MemberBasicInfo selectInfoByMobileNumber(String mobileNumber); + + /** + * 根据会员id查询会员信息 + * @param memberId 会员id + * @return + */ + MemberBasicInfo selectInfoByMemberId(String memberId); + + /** + * 更新会员余额 + * @param dto + * @return + */ + int updateMemberBalance(UpdateMemberBalanceDTO dto); + + /** + * + * @param memberId + * @return + */ + MemberVO queryMemberInfoByMemberId(String memberId); + + List selectMemberList(String mobileNumber, String nickName); + + /** + * 查询用户账户余额变动信息 + * @param memberId 会员id + * @param type 1-进账;2-出账 不传查全部 + */ + List getMemberBalanceChanges(String memberId, String type); + + /** + * 通过memberId查询会员的个人桩信息 + * @param memberId + * @return + */ + List getMemberPersonPileInfo(String memberId); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/IMemberTransactionRecordService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/IMemberTransactionRecordService.java new file mode 100644 index 000000000..79d5ad8e3 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/IMemberTransactionRecordService.java @@ -0,0 +1,18 @@ +package com.jsowell.pile.service; + +import com.jsowell.pile.domain.MemberTransactionRecord; +import com.jsowell.pile.vo.web.MemberTransactionVO; + +import java.util.List; + +public interface IMemberTransactionRecordService { + + /** + * 保存交易记录 + * @param record + * @return + */ + int insertSelective(MemberTransactionRecord record); + + List selectMemberTransactionRecordList(String memberId); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/IMemberWalletInfoService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/IMemberWalletInfoService.java new file mode 100644 index 000000000..ba81057b4 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/IMemberWalletInfoService.java @@ -0,0 +1,17 @@ +package com.jsowell.pile.service; + +import com.jsowell.pile.domain.MemberWalletInfo; + +public interface IMemberWalletInfoService { + int deleteByPrimaryKey(Integer id); + + int insert(MemberWalletInfo record); + + int insertSelective(MemberWalletInfo record); + + MemberWalletInfo selectByPrimaryKey(Integer id); + + int updateByPrimaryKeySelective(MemberWalletInfo record); + + int updateByPrimaryKey(MemberWalletInfo record); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/IMemberWalletLogService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/IMemberWalletLogService.java new file mode 100644 index 000000000..bfb159cf2 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/IMemberWalletLogService.java @@ -0,0 +1,16 @@ +package com.jsowell.pile.service; + +public interface IMemberWalletLogService { + // int deleteByPrimaryKey(Integer id); + // + // int insert(MemberWalletLog record); + // + // int insertSelective(MemberWalletLog record); + // + // MemberWalletLog selectByPrimaryKey(Integer id); + // + // int updateByPrimaryKeySelective(MemberWalletLog record); + // + // int updateByPrimaryKey(MemberWalletLog record); + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/IOrderAbnormalRecordService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/IOrderAbnormalRecordService.java new file mode 100644 index 000000000..923a689dc --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/IOrderAbnormalRecordService.java @@ -0,0 +1,61 @@ +package com.jsowell.pile.service; + +import com.jsowell.pile.domain.OrderAbnormalRecord; + +import java.util.List; + +/** + * 订单异常记录Service接口 + * + * @author jsowell + * @date 2023-02-13 + */ +public interface IOrderAbnormalRecordService { + /** + * 查询订单异常记录 + * + * @param id 订单异常记录主键 + * @return 订单异常记录 + */ + public OrderAbnormalRecord selectOrderAbnormalRecordById(Integer id); + + /** + * 查询订单异常记录列表 + * + * @param orderAbnormalRecord 订单异常记录 + * @return 订单异常记录集合 + */ + public List selectOrderAbnormalRecordList(OrderAbnormalRecord orderAbnormalRecord); + + /** + * 新增订单异常记录 + * + * @param orderAbnormalRecord 订单异常记录 + * @return 结果 + */ + public int insertOrderAbnormalRecord(OrderAbnormalRecord orderAbnormalRecord); + + /** + * 修改订单异常记录 + * + * @param orderAbnormalRecord 订单异常记录 + * @return 结果 + */ + public int updateOrderAbnormalRecord(OrderAbnormalRecord orderAbnormalRecord); + + /** + * 批量删除订单异常记录 + * + * @param ids 需要删除的订单异常记录主键集合 + * @return 结果 + */ + public int deleteOrderAbnormalRecordByIds(Integer[] ids); + + /** + * 删除订单异常记录信息 + * + * @param id 订单异常记录主键 + * @return 结果 + */ + public int deleteOrderAbnormalRecordById(Integer id); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/IOrderBasicInfoService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/IOrderBasicInfoService.java new file mode 100644 index 000000000..2dcb9f955 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/IOrderBasicInfoService.java @@ -0,0 +1,194 @@ +package com.jsowell.pile.service; + +import com.jsowell.common.core.domain.ykc.RealTimeMonitorData; +import com.jsowell.common.core.domain.ykc.TransactionRecordsData; +import com.jsowell.pile.domain.OrderBasicInfo; +import com.jsowell.pile.domain.OrderDetail; +import com.jsowell.pile.dto.IndexQueryDTO; +import com.jsowell.pile.dto.QueryOrderDTO; +import com.jsowell.pile.dto.QueryPersonPileDTO; +import com.jsowell.pile.vo.uniapp.OrderVO; +import com.jsowell.pile.vo.uniapp.PersonPileConnectorSumInfoVO; +import com.jsowell.pile.vo.uniapp.SendMessageVO; +import com.jsowell.pile.vo.web.IndexOrderInfoVO; +import com.jsowell.pile.vo.web.OrderListVO; +import com.jsowell.pile.vo.web.OrderTotalDataVO; +import com.jsowell.wxpay.dto.WeChatRefundDTO; + +import java.util.List; + +/** + * 订单Service接口 + * + * @author jsowell + * @date 2022-09-30 + */ +public interface IOrderBasicInfoService { + /** + * 查询订单 + * + * @param id 订单主键 + * @return 订单 + */ + OrderBasicInfo selectOrderBasicInfoById(Long id); + + /** + * 条件查询订单基础信息 + * @param info + * @return + */ + OrderBasicInfo getOrderBasicInfo(OrderBasicInfo info); + + /** + * 查询订单列表 + * + * @param dto 订单 + * @return 订单集合 + */ + List selectOrderBasicInfoList(QueryOrderDTO dto); + + /** + * 查询充电中的订单,没有数据权限校验,后管不要用 + * @param pileSn + * @return + */ + List selectChargingOrder(String pileSn); + + /** + * 修改订单 + * + * @param orderBasicInfo 订单 + * @return 结果 + */ + int updateOrderBasicInfo(OrderBasicInfo orderBasicInfo); + + /** + * 批量删除订单 + * + * @param ids 需要删除的订单主键集合 + * @return 结果 + */ + int deleteOrderBasicInfoByIds(Long[] ids); + + /** + * 通过订单号查询订单信息 + * + * @param orderCode 订单号 + * @return + */ + OrderBasicInfo getOrderInfoByOrderCode(String orderCode); + + /** + * 通过桩号和枪口号查询充电中的状态 + * @param pileSn 桩编号 + * @param connectorCode 枪口号 + * @return + */ + OrderBasicInfo queryChargingByPileSnAndConnectorCode(String pileSn, String connectorCode); + + OrderBasicInfo queryChargingByPileConnectorCode(String pileConnectorCode); + + /** + * 根据交易记录结算订单 + * @param data 交易记录数据 + * @param orderBasicInfo + */ + void settleOrder(TransactionRecordsData data, OrderBasicInfo orderBasicInfo); + + /** + * 关闭15分钟未支付订单 + * @return + */ + int close15MinutesOfUnpaidOrders(); + + /** + * 通过订单号查询订单详情 + * + * @param orderCode 订单号 + * @return 订单详情 + */ + OrderDetail getOrderDetailByOrderCode(String orderCode); + + /** + * 通过会员Id和订单状态查询订单信息 + * + * @param memberId 会员id + * @param orderStatusList 订单状态集合 + * @return + */ + List getListByMemberIdAndOrderStatus(String memberId, List orderStatusList); + + /** + * 结算订单退款和用户余额退款调这个方法 + */ + void weChatRefund(WeChatRefundDTO dto); + + /** + * 保存非法订单记录 + */ + void saveAbnormalOrder(TransactionRecordsData data); + + /** + * 获取充电实时数据 + * @param orderCode 订单编号 + * @return + */ + List getChargingRealTimeData(String orderCode); + + /** + * 首页订单数据展示 + * @param dto 首页信息查询dto + * @return + */ + List getIndexOrderInfo(IndexQueryDTO dto); + + /** + * 获取超过15分钟的待支付状态订单 + * @return + */ + List getUnpaidOrderListOver15Min(); + + /** + * 根据orderId批量修改订单状态 + * @param orderIds + * @param orderStatus + */ + void updateOrderStatusById(List orderIds, String orderStatus); + + /** + * 查询时间段内订单总金额和总用电量 + */ + OrderTotalDataVO getOrderTotalData(QueryOrderDTO orderBasicInfo); + + /** + * 通过订单号查询订单信息(小程序发送消息用) + * @param orderCode + * @return + */ + SendMessageVO selectOrderInfoByOrderCode(String orderCode); + + /** + * 充电桩启动失败 + * @param orderCode + * @param failedReasonMsg + */ + void chargingPileFailedToStart(String orderCode, String failedReasonMsg); + + /** + * 充电桩启动成功 + * @param orderCode + */ + void chargingPileStartedSuccessfully(String orderCode); + + /** + * 关闭启动失败的订单 + */ + void closeStartFailedOrder(String startTime, String endTime); + + /** + * 个人桩查询充电数据 + * @param dto + * @return + */ + List getAccumulativeInfo(QueryPersonPileDTO dto); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/IOrderPayRecordService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/IOrderPayRecordService.java new file mode 100644 index 000000000..5adad0548 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/IOrderPayRecordService.java @@ -0,0 +1,29 @@ +package com.jsowell.pile.service; + +import com.jsowell.pile.domain.OrderPayRecord; + +import java.util.List; + +public interface IOrderPayRecordService { + + // int deleteByPrimaryKey(Integer id); + + // int insert(OrderPayRecord record); + + // int insertSelective(OrderPayRecord record); + + // OrderPayRecord selectByPrimaryKey(Integer id); + + int updateByPrimaryKeySelective(OrderPayRecord record); + + // int updateByPrimaryKey(OrderPayRecord record); + + /** + * 批量保存订单支付记录 + * @param payRecordList + * @return + */ + int batchInsert(List payRecordList); + + List getOrderPayRecordList(String orderCode); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileBasicInfoService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileBasicInfoService.java new file mode 100644 index 000000000..2d5d46967 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileBasicInfoService.java @@ -0,0 +1,158 @@ +package com.jsowell.pile.service; + +import com.jsowell.common.core.domain.ykc.RealTimeMonitorData; +import com.jsowell.pile.domain.PileBasicInfo; +import com.jsowell.pile.dto.IndexQueryDTO; +import com.jsowell.pile.dto.QueryPileDTO; +import com.jsowell.pile.dto.ReplaceMerchantStationDTO; +import com.jsowell.pile.vo.base.PileInfoVO; +import com.jsowell.pile.vo.uniapp.PersonalPileInfoVO; +import com.jsowell.pile.vo.uniapp.PileConnectorDetailVO; +import com.jsowell.pile.vo.web.IndexGeneralSituationVO; +import com.jsowell.pile.vo.web.PileDetailVO; + +import java.util.List; + +/** + * 设备管理Service接口 + * + * @author jsowell + * @date 2022-08-26 + */ +public interface IPileBasicInfoService { + /** + * 查询设备管理 + * + * @param id 设备管理主键 + * @return 设备管理 + */ + PileBasicInfo selectPileBasicInfoById(Long id); + + PileBasicInfo selectPileBasicInfoBySN(String pileSn); + + /** + * 查询设备管理列表 + * + * @param pileBasicInfo 设备管理 + * @return 设备管理集合 + */ + List selectPileBasicInfoList(PileBasicInfo pileBasicInfo); + + /** + * 新增设备管理 + * + * @param pileBasicInfo 设备管理 + * @return 结果 + */ + int insertPileBasicInfo(PileBasicInfo pileBasicInfo); + + /** + * 修改设备管理 + * + * @param pileBasicInfo 设备管理 + * @return 结果 + */ + int updatePileBasicInfo(PileBasicInfo pileBasicInfo); + + /** + * 批量删除设备管理 + * + * @param ids 需要删除的设备管理主键集合 + * @return 结果 + */ + int deletePileBasicInfoByIds(Long[] ids); + + /** + * 删除设备管理信息 + * + * @param id 设备管理主键 + * @return 结果 + */ + int deletePileBasicInfoById(Long id); + + /** + * 查询列表 + */ + List queryPileInfos(QueryPileDTO dto); + + List queryPileInfoList(QueryPileDTO queryPileDTO); + + /** + * 通过pileId更改运营商、站点信息 + * + * @param dto 前台参数 + * @return 结果 + */ + int replaceMerchantStationByPileIds(ReplaceMerchantStationDTO dto); + + /** + * 通过桩id查询basic信息 + * + * @param id 桩id + * @return 结果集合 + */ + PileDetailVO selectBasicInfoById(Long id); + + PileInfoVO selectPileInfoBySn(String pileSn); + + /** + * 通过站点id查询桩集合 + * + * @param stationIdList 站点id + * @return 桩集合 + */ + List selectPileListByStationIds(List stationIdList); + + /** + * 通过桩编号查询站点id + * @param sn 桩编号 + * @return 站点id + */ + // String selectStationIdBySn(String sn); + + /** + * uniApp通过桩号查询桩详情 + * @param pileSn 桩号 + * @return + */ + // PileDetailVO uniAppGetPileDetailByPileSn(String pileSn); + + /** + * 修改状态 + * @param frameType + * @param pileSn + * @param connectorCode + * @param status + * @param putGunType + */ + void updateStatus(String frameType, String pileSn, String connectorCode, String status, String putGunType); + + /** + * 充电时保存实时数据到redis + * @param realTimeMonitorData 实时数据 + */ + void saveRealTimeMonitorData2Redis(RealTimeMonitorData realTimeMonitorData); + + PileConnectorDetailVO queryPileConnectorDetail(String pileConnectorCode); + + String getPileQrCodeUrl(String pileSn); + + // 更新充电桩的sim卡信息 + void updatePileSimInfo(String pileSn, String iccid); + + + /** + * 后管首页基本信息查询 + * + * @param dto 站点Id + * @return 首页基本信息 + */ + public IndexGeneralSituationVO getGeneralSituation(IndexQueryDTO dto); + + /** + * 通过会员id查询个人桩列表 + * @param memberId + * @return + */ + List getPileInfoByMemberId(String memberId); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileBillingTemplateService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileBillingTemplateService.java new file mode 100644 index 000000000..380072c96 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileBillingTemplateService.java @@ -0,0 +1,152 @@ +package com.jsowell.pile.service; + +import com.jsowell.pile.domain.PileBillingRelation; +import com.jsowell.pile.domain.PileBillingTemplate; +import com.jsowell.pile.dto.CreateOrUpdateBillingTemplateDTO; +import com.jsowell.pile.dto.ImportBillingTemplateDTO; +import com.jsowell.pile.vo.uniapp.BillingPriceVO; +import com.jsowell.pile.vo.uniapp.CurrentTimePriceDetails; +import com.jsowell.pile.vo.web.BillingTemplateVO; +import com.jsowell.pile.vo.web.EchoBillingTemplateVO; + +import java.util.List; + +/** + * 计费模板Service接口 + * + * @author jsowell + * @date 2022-09-20 + */ +public interface IPileBillingTemplateService { + /** + * 查询计费模板 + * + * @param id 计费模板主键 + * @return 计费模板 + */ + public PileBillingTemplate selectPileBillingTemplateById(Long id); + + /** + * 查询计费模板列表 + * + * @param pileBillingTemplate 计费模板 + * @return 计费模板集合 + */ + public List selectPileBillingTemplateList(PileBillingTemplate pileBillingTemplate); + + /** + * 新增计费模板 + * + * @param pileBillingTemplate 计费模板 + * @return 结果 + */ + public int insertPileBillingTemplate(PileBillingTemplate pileBillingTemplate); + + /** + * 修改计费模板 + * + * @param pileBillingTemplate 计费模板 + * @return 结果 + */ + public int updatePileBillingTemplate(PileBillingTemplate pileBillingTemplate); + + /** + * 批量删除计费模板 + * + * @param ids 需要删除的计费模板主键集合 + * @return 结果 + */ + public int deletePileBillingTemplateByIds(Long[] ids); + + /** + * 删除计费模板信息 + * + * @param id 计费模板主键 + * @return 结果 + */ + public int deletePileBillingTemplateById(Long id); + + /** + * 新增计费模板 + * + * @param dto 参数 + * @return 计费模板id + */ + void createBillingTemplate(CreateOrUpdateBillingTemplateDTO dto); + + /** + * 查询公共计费模板 + * + * @return + */ + List queryPublicBillingTemplateList(); + + /** + * 查询站点计费模板 + * + * @param stationId 站点id + * @return + */ + List queryStationBillingTemplateList(String stationId); + + /** + * 查询正在使用中的计费模板 + * @param stationId 站点id + * @return + */ + BillingTemplateVO queryUsedBillingTemplate(String stationId); + + /** + * 查询计费价格详情 + * @param stationId 站点id + * @return + */ + List queryBillingPrice(String stationId); + + /** + * 通过桩sn号查询计费模板信息 + * + * @param pileSn 桩sn + * @return 计费模板编号 + */ + BillingTemplateVO selectBillingTemplateDetailByPileSn(String pileSn); + + /** + * 站点导入计费模板 + * + * @param dto + * @return + */ + boolean stationImportBillingTemplate(ImportBillingTemplateDTO dto); + + byte[] generateBillingTemplateMsgBody(String pileSn, BillingTemplateVO billingTemplateVO); + + /** + * 根据计费模板id查询计费模板信息 + * + * @param templateId 计费模板id + * @return 计费模板信息 + */ + BillingTemplateVO selectBillingTemplateByTemplateId(String templateId); + + /** + * 保存计费模板和桩关系 + * + * @param relationList + */ + void insertPileBillingRelation(List relationList); + + /** + * 修改计费模板 + * + * @param dto + */ + void updateBillingTemplate(CreateOrUpdateBillingTemplateDTO dto); + + EchoBillingTemplateVO queryPileBillingTemplateById(Long id); + + /** + * 通过站点id查询当前时间的收费详情 + */ + CurrentTimePriceDetails getCurrentTimePriceDetails(String stationId); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileConnectorInfoService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileConnectorInfoService.java new file mode 100644 index 000000000..20f1fe669 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileConnectorInfoService.java @@ -0,0 +1,133 @@ +package com.jsowell.pile.service; + +import com.jsowell.common.core.page.PageResponse; +import com.jsowell.pile.domain.PileConnectorInfo; +import com.jsowell.pile.dto.QueryConnectorDTO; +import com.jsowell.pile.dto.QueryConnectorListDTO; +import com.jsowell.pile.vo.base.ConnectorInfoVO; +import com.jsowell.pile.vo.web.PileConnectorInfoVO; + +import java.util.List; +import java.util.Map; + +/** + * 充电桩枪口信息Service接口 + * + * @author jsowell + * @date 2022-08-31 + */ +public interface IPileConnectorInfoService { + /** + * 查询充电桩枪口信息 + * + * @param id 充电桩枪口信息主键 + * @return 充电桩枪口信息 + */ + PileConnectorInfo selectPileConnectorInfoById(Integer id); + + /** + * 查询充电桩枪口信息列表 + * + * @param pileConnectorInfo 充电桩枪口信息 + * @return 充电桩枪口信息集合 + */ + List selectPileConnectorInfoList(PileConnectorInfo pileConnectorInfo); + + List selectPileConnectorInfoList(String pileSn); + + /** + * 新增充电桩枪口信息 + * + * @param pileConnectorInfo 充电桩枪口信息 + * @return 结果 + */ + // int insertPileConnectorInfo(PileConnectorInfo pileConnectorInfo); + + /** + * 修改充电桩枪口信息 + * + * @param pileConnectorInfo 充电桩枪口信息 + * @return 结果 + */ + // int updatePileConnectorInfo(PileConnectorInfo pileConnectorInfo); + + /** + * 批量删除充电桩枪口信息 + * + * @param ids 需要删除的充电桩枪口信息主键集合 + * @return 结果 + */ + // int deletePileConnectorInfoByIds(Integer[] ids); + + int deletePileConnectorInfoByPileSnList(List pileSnList); + + int batchInsertConnectorInfo(List pileConnectorInfoList); + + /** + * 充电接口信息列表 + * + * @param dto 前台参数 + * @return 充电接口对象集合 + */ + List getConnectorInfoListByParams(QueryConnectorDTO dto); + + /** + * 通过充电站id查询充电枪信息 + * + * @param stationId 充电站id + * @return 充电枪信息集合 + */ + List selectConnectorListByStationId(Long stationId); + + /** + * 支持多种查询充电桩接口列表 + */ + List getConnectorInfoListByParams(QueryConnectorListDTO dto); + + /** + * 更新充电桩枪口状态 + * @param connectorCode 枪口号 + * @param status 状态 0:离网 (默认);1:空闲;2:占用(未充电);3:占用(充电中);4:占用(预约锁定) ;255:故障 + */ + int updateConnectorStatus(String connectorCode, String status); + + /** + * 通过桩编号修改枪口状态 + * 仅用于登录逻辑使用 + * @param pileSn 桩编号 + * @param status 状态 状态 0:离网 (默认);1:空闲;2:占用(未充电);3:占用(充电中);4:占用(预约锁定) ;255:故障 + */ + int updateConnectorStatusByPileSn(String pileSn, String status); + + /** + * 批量获取充电桩状态 + * @param pileSnList 桩编号list + * @return key:桩编号;value:状态值 + */ + Map getPileStatus(List pileSnList); + + /** + * 通过枪口编码查询枪口信息 + * + * @param connectorCode + * @return + */ + PileConnectorInfoVO getPileConnectorInfoByConnectorCode(String connectorCode); + + String getPileConnectorQrCodeUrl(String pileConnectorCode); + + /** + * 支持多种查询充电桩接口列表(uniapp) + */ + PageResponse getUniAppConnectorInfoListByParams(QueryConnectorListDTO dto); + + /** + * uniApp通过站点id查询枪口列表信息 + * + * @param stationId 站点id + * @return + */ + List getUniAppConnectorList(Long stationId); + + List selectConnectorInfoList(String pileSn); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileLicenceInfoService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileLicenceInfoService.java new file mode 100644 index 000000000..5e7eec071 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileLicenceInfoService.java @@ -0,0 +1,62 @@ +package com.jsowell.pile.service; + +import com.jsowell.pile.domain.PileLicenceInfo; + +import java.util.List; + +/** + * 充电桩证书信息Service接口 + * + * @author jsowell + * @date 2022-08-27 + */ +public interface IPileLicenceInfoService +{ + /** + * 查询充电桩证书信息 + * + * @param id 充电桩证书信息主键 + * @return 充电桩证书信息 + */ + public PileLicenceInfo selectPileLicenceInfoById(Long id); + + /** + * 查询充电桩证书信息列表 + * + * @param pileLicenceInfo 充电桩证书信息 + * @return 充电桩证书信息集合 + */ + public List selectPileLicenceInfoList(PileLicenceInfo pileLicenceInfo); + + /** + * 新增充电桩证书信息 + * + * @param pileLicenceInfo 充电桩证书信息 + * @return 结果 + */ + public int insertPileLicenceInfo(PileLicenceInfo pileLicenceInfo); + + /** + * 修改充电桩证书信息 + * + * @param pileLicenceInfo 充电桩证书信息 + * @return 结果 + */ + public int updatePileLicenceInfo(PileLicenceInfo pileLicenceInfo); + + /** + * 批量删除充电桩证书信息 + * + * @param ids 需要删除的充电桩证书信息主键集合 + * @return 结果 + */ + public int deletePileLicenceInfoByIds(Long[] ids); + + /** + * 删除充电桩证书信息信息 + * + * @param id 充电桩证书信息主键 + * @return 结果 + */ + public int deletePileLicenceInfoById(Long id); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileMemberRelationService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileMemberRelationService.java new file mode 100644 index 000000000..53a653737 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileMemberRelationService.java @@ -0,0 +1,79 @@ +package com.jsowell.pile.service; + +import com.jsowell.pile.domain.PileMemberRelation; +import com.jsowell.pile.vo.uniapp.PersonalPileInfoVO; + +import java.util.List; + +/** + * 桩与用户绑定关系Service接口 + * + * @author jsowell + * @date 2023-02-21 + */ +public interface IPileMemberRelationService +{ + /** + * 查询桩与用户绑定关系 + * + * @param id 桩与用户绑定关系主键 + * @return 桩与用户绑定关系 + */ + public PileMemberRelation selectPileMemberRelationById(Integer id); + + /** + * 查询桩与用户绑定关系列表 + * + * @param pileMemberRelation 桩与用户绑定关系 + * @return 桩与用户绑定关系集合 + */ + public List selectPileMemberRelationList(PileMemberRelation pileMemberRelation); + + /** + * 条件查询桩与用户绑定关系 + * + * @param pileMemberRelation 桩与用户绑定关系 + * @return 桩与用户绑定关系对象 + */ + PileMemberRelation selectPileMemberRelation(PileMemberRelation pileMemberRelation); + + /** + * 新增桩与用户绑定关系 + * + * @param pileMemberRelation 桩与用户绑定关系 + * @return 结果 + */ + public int insertPileMemberRelation(PileMemberRelation pileMemberRelation); + + /** + * 修改桩与用户绑定关系 + * + * @param pileMemberRelation 桩与用户绑定关系 + * @return 结果 + */ + public int updatePileMemberRelation(PileMemberRelation pileMemberRelation); + + /** + * 批量删除桩与用户绑定关系 + * + * @param ids 需要删除的桩与用户绑定关系主键集合 + * @return 结果 + */ + public int deletePileMemberRelationByIds(Integer[] ids); + + /** + * 删除桩与用户绑定关系信息 + * + * @param id 桩与用户绑定关系主键 + * @return 结果 + */ + public int deletePileMemberRelationById(Integer id); + + /** + * 通过桩编码查询关系信息 + * @param pileSn + * @return + */ + List selectPileMemberRelationByPileSn(String pileSn); + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileMerchantInfoService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileMerchantInfoService.java new file mode 100644 index 000000000..4a2279710 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileMerchantInfoService.java @@ -0,0 +1,66 @@ +package com.jsowell.pile.service; + +import com.jsowell.pile.domain.PileMerchantInfo; +import com.jsowell.pile.vo.base.MerchantInfoVO; + +import java.util.List; + +/** + * 充电桩运营商信息Service接口 + * + * @author jsowell + * @date 2022-08-27 + */ +public interface IPileMerchantInfoService { + /** + * 查询充电桩运营商信息 + * + * @param id 充电桩运营商信息主键 + * @return 充电桩运营商信息 + */ + public PileMerchantInfo selectPileMerchantInfoById(Long id); + + /** + * 查询充电桩运营商信息列表 + * + * @param pileMerchantInfo 充电桩运营商信息 + * @return 充电桩运营商信息集合 + */ + public List selectPileMerchantInfoList(PileMerchantInfo pileMerchantInfo); + + /** + * 新增充电桩运营商信息 + * + * @param pileMerchantInfo 充电桩运营商信息 + * @return 结果 + */ + public int insertPileMerchantInfo(PileMerchantInfo pileMerchantInfo); + + /** + * 修改充电桩运营商信息 + * + * @param pileMerchantInfo 充电桩运营商信息 + * @return 结果 + */ + public int updatePileMerchantInfo(PileMerchantInfo pileMerchantInfo); + + /** + * 批量删除充电桩运营商信息 + * + * @param ids 需要删除的充电桩运营商信息主键集合 + * @return 结果 + */ + public int deletePileMerchantInfoByIds(Long[] ids); + + /** + * 删除充电桩运营商信息信息 + * + * @param id 充电桩运营商信息主键 + * @return 结果 + */ + public int deletePileMerchantInfoById(Long id); + + String getMerchantIdByAppId(String appId); + + MerchantInfoVO getMerchantInfo(String merchantId); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileModelInfoService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileModelInfoService.java new file mode 100644 index 000000000..fbfc7c220 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileModelInfoService.java @@ -0,0 +1,72 @@ +package com.jsowell.pile.service; + +import com.jsowell.pile.domain.PileModelInfo; +import com.jsowell.pile.vo.web.PileModelInfoVO; + +import java.util.List; + +/** + * 充电桩型号信息Service接口 + * + * @author jsowell + * @date 2022-08-26 + */ +public interface IPileModelInfoService { + /** + * 查询充电桩型号信息 + * + * @param id 充电桩型号信息主键 + * @return 充电桩型号信息 + */ + public PileModelInfo selectPileModelInfoById(Long id); + + /** + * 查询充电桩型号信息列表 + * + * @param pileModelInfo 充电桩型号信息 + * @return 充电桩型号信息集合 + */ + public List selectPileModelInfoList(PileModelInfo pileModelInfo); + + /** + * 新增充电桩型号信息 + * + * @param pileModelInfo 充电桩型号信息 + * @return 结果 + */ + public int insertPileModelInfo(PileModelInfo pileModelInfo); + + /** + * 修改充电桩型号信息 + * + * @param pileModelInfo 充电桩型号信息 + * @return 结果 + */ + public int updatePileModelInfo(PileModelInfo pileModelInfo); + + /** + * 批量删除充电桩型号信息 + * + * @param ids 需要删除的充电桩型号信息主键集合 + * @return 结果 + */ + public int deletePileModelInfoByIds(Long[] ids); + + /** + * 删除充电桩型号信息信息 + * + * @param id 充电桩型号信息主键 + * @return 结果 + */ + public int deletePileModelInfoById(Long id); + + /** + * 通过桩编号集合获取型号表中数据 + * + * @param pileSns 桩编号集合 + * @return PileModelInfo对象 + */ + List getPileModelInfoByPileSnList(List pileSns); + + PileModelInfoVO getPileModelInfoByPileSn(String pileSn); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileMsgRecordService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileMsgRecordService.java new file mode 100644 index 000000000..31b43e810 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileMsgRecordService.java @@ -0,0 +1,24 @@ +package com.jsowell.pile.service; + +import com.jsowell.common.core.page.PageResponse; +import com.jsowell.pile.dto.QueryPileDTO; + +public interface IPileMsgRecordService { + + /** + * 保存报文 + * @param pileSn 桩编号 + * @param connectorCode 枪号 + * @param frameType 帧类型 + * @param jsonMsg msgBody的json字符串 + * @param originalMsg 原始报文 + */ + void save(String pileSn, String connectorCode, String frameType, String jsonMsg, String originalMsg); + + // List getByConnectorCodeList(List connectorCodeList); + + /** + * 查询充电桩通信日志 分页 + */ + PageResponse getPileFeedList(QueryPileDTO dto); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileSimInfoService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileSimInfoService.java new file mode 100644 index 000000000..ace0b4a0e --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileSimInfoService.java @@ -0,0 +1,92 @@ +package com.jsowell.pile.service; + +import com.jsowell.pile.domain.PileSimInfo; +import com.jsowell.pile.dto.QuerySimInfoDTO; +import com.jsowell.pile.vo.web.SimCardInfoVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 充电桩SIM卡信息Service接口 + * + * @author jsowell + * @date 2022-08-26 + */ +public interface IPileSimInfoService +{ + /** + * 查询充电桩SIM卡信息 + * + * @param id 充电桩SIM卡信息主键 + * @return 充电桩SIM卡信息 + */ + public PileSimInfo selectPileSimInfoById(Long id); + + /** + * 查询充电桩SIM卡信息列表 + * + * @param pileSimInfo 充电桩SIM卡信息 + * @return 充电桩SIM卡信息集合 + */ + public List selectPileSimInfoList(PileSimInfo pileSimInfo); + + /** + * 后管查询sim卡信息列表 + * @return + */ + List getSimInfoList(QuerySimInfoDTO dto); + + /** + * 新增充电桩SIM卡信息 + * + * @param pileSimInfo 充电桩SIM卡信息 + * @return 结果 + */ + public int insertPileSimInfo(PileSimInfo pileSimInfo); + + /** + * 修改充电桩SIM卡信息 + * + * @param pileSimInfo 充电桩SIM卡信息 + * @return 结果 + */ + public int updatePileSimInfo(PileSimInfo pileSimInfo); + + /** + * 批量删除充电桩SIM卡信息 + * + * @param ids 需要删除的充电桩SIM卡信息主键集合 + * @return 结果 + */ + public int deletePileSimInfoByIds(Long[] ids); + + /** + * 删除充电桩SIM卡信息信息 + * + * @param id 充电桩SIM卡信息主键 + * @return 结果 + */ + public int deletePileSimInfoById(Long id); + + /** + * 通过桩编码查询sim卡信息 + * @param pileSn 桩编码 + * @return + */ + SimCardInfoVO querySimCardInfoByPileSn(String pileSn); + + /** + * 通过卡号批量查询sim卡信息 + * @param iccIds 卡号 + * @return + */ + List selectSimInfoByIccIds(List iccIds); + + /** + * 通过卡号查询sim卡信息 + * @param iccId 卡号 + * @return + */ + PileSimInfo getBasicInfoByIccId(String iccId); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileStationInfoService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileStationInfoService.java new file mode 100644 index 000000000..03541046a --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/IPileStationInfoService.java @@ -0,0 +1,91 @@ +package com.jsowell.pile.service; + +import com.jsowell.common.core.page.PageResponse; +import com.jsowell.pile.domain.PileStationInfo; +import com.jsowell.pile.dto.FastCreateStationDTO; +import com.jsowell.pile.dto.QueryStationDTO; +import com.jsowell.pile.vo.web.PileStationVO; + +import java.util.List; + +/** + * 充电站信息Service接口 + * + * @author jsowell + * @date 2022-08-30 + */ +public interface IPileStationInfoService { + /** + * 查询充电站信息 + * + * @param id 充电站信息主键 + * @return 充电站信息 + */ + public PileStationInfo selectPileStationInfoById(Long id); + + /** + * 查询充电站信息列表 + * + * @param pileStationInfo 充电站信息 + * @return 充电站信息集合 + */ + public List selectPileStationInfoList(PileStationInfo pileStationInfo); + + /** + * 通过运营商id查询站点信息 + * + * @param merchantId 运营商id + * @return 站点信息列表 + */ + public List selectStationListByMerchantId(Long merchantId); + + /** + * 新增充电站信息 + * + * @param pileStationInfo 充电站信息 + * @return 结果 + */ + public int insertPileStationInfo(PileStationInfo pileStationInfo); + + /** + * 快速建站 + * + * @param dto + * @return + */ + public int fastCreateStation(FastCreateStationDTO dto); + + /** + * 修改充电站信息 + * + * @param pileStationInfo 充电站信息 + * @return 结果 + */ + public int updatePileStationInfo(PileStationInfo pileStationInfo); + + /** + * 批量删除充电站信息 + * + * @param ids 需要删除的充电站信息主键集合 + * @return 结果 + */ + public int deletePileStationInfoByIds(Long[] ids); + + /** + * 查询充电站信息 + * + * @param dto 前台参数 + * @return 充电站信息集合 + */ + List queryStationInfos(QueryStationDTO dto); + + /** + * 查询充电站信息并通过经纬度距离排序 + * + * @param queryStationDTO 前台参数 + * @return 充电站对象集合 + */ + PageResponse uniAppQueryStationInfoList(QueryStationDTO queryStationDTO); + + PileStationVO getStationInfo(String stationId); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/SimCardService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/SimCardService.java new file mode 100644 index 000000000..1ae56dd9f --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/SimCardService.java @@ -0,0 +1,468 @@ +package com.jsowell.pile.service; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.google.common.collect.Lists; +import com.jsowell.common.enums.sim.SimCardStatusCorrespondEnum; +import com.jsowell.common.enums.sim.SimSupplierEnum; +import com.jsowell.common.enums.ykc.ReturnCodeEnum; +import com.jsowell.common.exception.BusinessException; +import com.jsowell.common.util.RandomUtil; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.http.HttpUtils; +import com.jsowell.common.util.id.IdUtils; +import com.jsowell.common.util.sim.SimCardUtils; +import com.jsowell.common.util.sim.XunZhongSimUtils; +import com.jsowell.pile.vo.web.*; +import org.apache.commons.collections4.CollectionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.stream.Collectors; + + +/** + * Sim卡Service + * + * @author JS-ZZA + * @date 2022/12/3 15:35 + */ +@Service +public class SimCardService { + + @Autowired + private IPileSimInfoService pileSimInfoService; + + protected final Logger logger = LoggerFactory.getLogger(SimCardService.class); + + @Value("${xunzhong.apiId}") + private String API_ID; + + @Value("${xunzhong.sim.getSimCardDetailURL}") + private String getSimCardDetailURL; + + @Value("${xunzhong.trafficPool.poolListURL}") + private String poolListURL; + + @Value("${xunzhong.sim.renewURL}") + private String renewURL; + + @Value("${wulian.appId}") + private String appId; + + @Value("${wulian.appSecret}") + private String appSecret; + + @Value("${wulian.getWay}") + private String wuLianGetWay; + + @Value("${wulian.name.getSimInfo}") + private String getSimInfoName; + + @Value("${wulian.name.WuLianSimRenew}") + private String WuLianSimRenew; + + /** + * 不知道iccid属于哪家供应商,就用这个方法查 + * @param iccid + * @return + */ + public SimCardVO searchByLoop(String iccid) { + SimCardVO vo = null; + // 查XunZhong + List simCardVOS = XunZhongGetSimInfoByIccIds(Lists.newArrayList(iccid)); + if (CollectionUtils.isNotEmpty(simCardVOS)) { + vo = simCardVOS.get(0); + } + // 查WuLian平台 + List wuLianSimData = WuLianGetSimInfoByIccIds(Lists.newArrayList(iccid)); + if (CollectionUtils.isNotEmpty(wuLianSimData)) { + vo = wuLianSimData.get(0); + } + // 第三个供应商 + + logger.info("查询iccid:{}, 详情信息:{}", iccid, JSON.toJSONString(vo)); + return vo; + } + + /** + * 批量续费(后管调用此方法) + * @param iccIds 卡号 + * @param cycleNumber 续费周期 + */ + public List renewSimByLoop(List iccIds, int cycleNumber) { + if (CollectionUtils.isEmpty(iccIds)) { + return Lists.newArrayList(); + } + // 将集合中为空和0的过滤 + iccIds = iccIds.stream() + .filter(StringUtils::isNotEmpty) + .filter(x -> !StringUtils.equals(x, "0")) + .collect(Collectors.toList()); + + ArrayList list = new ArrayList<>(); + for (String iccId : iccIds) { + // 查出此卡属于哪家公司(拿到code) + SimCardVO simCardVO = searchByLoop(iccId); + String simSupplierCode = simCardVO.getSimCardFactory(); + + // 根据不同的公司执行不同的续费方法 + SimRenewResultVO simRenewResultVO = renewSimBySupplier(simSupplierCode, iccId, cycleNumber); + list.add(simRenewResultVO); + } + return list; + } + + + /** + * 根据不同的公司执行不同的续费方法 + * @param code SimSupplierEnum.getCode() + * @param iccId 卡号 + * @param cycleNumber 续费周期 + */ + private SimRenewResultVO renewSimBySupplier(String code, String iccId, int cycleNumber) { + SimRenewResultVO vo = new SimRenewResultVO(); + vo.setIccId(iccId); + vo.setCycleNumber(cycleNumber); + + if (StringUtils.equals(code, SimSupplierEnum.XUN_ZHONG.getCode())) { + // 讯众 + try { + vo.setSimSuppler(SimSupplierEnum.XUN_ZHONG.getName()); + XunZhongSimRenewal(Lists.newArrayList(iccId), cycleNumber); + vo.setResult(true); + }catch (BusinessException e) { + vo.setResult(false); + vo.setReason(e.getMessage()); + } + } + // 物联平台 + if (StringUtils.equals(code, SimSupplierEnum.WU_LIAN_INTERNET.getCode())) { + try { + vo.setSimSuppler(SimSupplierEnum.WU_LIAN_INTERNET.getName()); + WuLianSimRenew(Lists.newArrayList(iccId), cycleNumber); + vo.setResult(true); + }catch (BusinessException e){ + vo.setResult(false); + vo.setReason(e.getMessage()); + } + } + return vo; + } + + + + /** + * 讯众 内部接口 + * 通过iccIds查询讯众Sim卡信息 + * @param iccIds + */ + public List XunZhongGetSimInfoByIccIds(List iccIds) { + List resultList = new ArrayList<>(); + + List dataList = getSimCardDetail(iccIds); + SimCardVO simCard = null; + for (XunZhongSimData zhongSimData : dataList) { + // sim卡套餐 + XunZhongSimData.Products products = null; + if (CollectionUtils.isNotEmpty(zhongSimData.getCurrent_products())) { + products = zhongSimData.getCurrent_products().get(0); + } else { + if (CollectionUtils.isNotEmpty(zhongSimData.getFuture_products())) { + products = zhongSimData.getFuture_products().get(0); + } + } + if (products == null) { + logger.info("iccid:{}, 没有套餐", zhongSimData.getIccid()); + continue; + } + // set + simCard = new SimCardVO(); + simCard.setIccId(zhongSimData.getIccid()); // 卡号 + simCard.setSimCardFactory(SimSupplierEnum.XUN_ZHONG.getCode()); // 1-讯众物联 + simCard.setSimCardOperator(zhongSimData.getCarrier_type()); // 运营商类型 + simCard.setSimCardStatus(zhongSimData.getNet_status()); // 联网状态 + simCard.setName(products.getName()); // 套餐名称 + simCard.setExpiredTime(products.getExpiration_time()); // 过期时间 + + BigDecimal packageCapacity = products.getPackage_capacity(); // 套餐总量(mb) + simCard.setPackageCapacity(packageCapacity); + + if (products.getCurrent_cycle_usage() != null) { + BigDecimal current_cycle_usage = products.getCurrent_cycle_usage(); // 当前已用(kb) + String currentCycleUsage = SimCardUtils.kb2MbOrGb(current_cycle_usage.intValue()); + BigDecimal currentCycleUsageData = new BigDecimal(currentCycleUsage); + simCard.setUsedFlowRate(currentCycleUsageData); + BigDecimal residualData = packageCapacity.subtract(currentCycleUsageData); // 套餐剩余 + simCard.setResidualFlowRate(residualData.setScale(2, BigDecimal.ROUND_HALF_UP)); + } + + resultList.add(simCard); + } + return resultList; + } + + public static void main(String[] args) { + // List iccIds = Lists.newArrayList(); + // iccIds.add("123"); + // iccIds.add(null); + // iccIds.add("0"); + // iccIds.add("asdf"); + // iccIds.add(null); + // iccIds.add("0"); + // iccIds.add(null); + // iccIds.add("0"); + // iccIds.add("9461351351"); + // iccIds = iccIds.stream() + // .filter(StringUtils::isNotEmpty) + // .filter(x -> !StringUtils.equals(x, "0")) + // .collect(Collectors.toList()); + // String iccId = StringUtils.join(iccIds, ","); + // System.out.println(iccId); + + + String packName = "移动 500M/月"; + System.out.println(StringUtils.contains(packName, "移动")); + // StringUtils.containsAny() + } + + + /** + * 讯众官方接口 + * 获取sim卡详细信息 + * @param iccIds Sim卡集合 + * @return + */ + public List getSimCardDetail(List iccIds) { + String iccId = list2Str(iccIds); + + String timestamp = String.valueOf(System.currentTimeMillis() / 1000); + + Hashtable params = new Hashtable<>(); + params.put("api_id", API_ID); + params.put("timestamp", timestamp); + params.put("iccids", iccId); + + String signStr = XunZhongSimUtils.getSignStr(params); + + params.put("sign", signStr); + + String postResult = XunZhongSimUtils.sendPost(getSimCardDetailURL, params); + logger.info("【====讯众物联====】查询Sim卡信息, param:{}, result:{}", params, postResult); + JSONObject jsonObject = JSONObject.parseObject(postResult); + if (StringUtils.equals(jsonObject.getString("code"), "0")) { + String result = jsonObject.getString("result"); + return JSON.parseArray(result, XunZhongSimData.class); + } + return Lists.newArrayList(); + } + + /** + * 讯众官方接口 + * 获取流量池信息 + * @return + */ + public String getTrafficPoolInfo() { + String timestamp = String.valueOf(System.currentTimeMillis() / 1000); + + Hashtable params = new Hashtable<>(); + params.put("api_id", API_ID); + params.put("timestamp", timestamp); + + String signStr = XunZhongSimUtils.getSignStr(params); + params.put("sign", signStr); + + String postResult = XunZhongSimUtils.sendPost(poolListURL, params); + logger.info("【====讯众物联====】获取流量池信息, result:{}", postResult); + return postResult; + } + + + /** + * 讯众官方接口 + * sim卡续期 + * @param iccIds 需要续期的sim卡 + * @param cycleNumber 续费周期(0-999),0表示无限周期 + * @return + */ + public void XunZhongSimRenewal(List iccIds, int cycleNumber) { + if (CollectionUtils.isEmpty(iccIds)) { + throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR); + } + String iccId = list2Str(iccIds); + + String timestamp = String.valueOf(System.currentTimeMillis() / 1000); + + Hashtable params = new Hashtable<>(); + params.put("api_id", API_ID); + params.put("timestamp", timestamp); + params.put("iccids", iccId); + params.put("cycle_number", cycleNumber); + + String signStr = XunZhongSimUtils.getSignStr(params); + + params.put("sign", signStr); + + String postResult = XunZhongSimUtils.sendPost(renewURL, params); + logger.info("【====讯众物联====】sim卡续费业务, result:{}", postResult); + JSONObject jsonResult = JSONObject.parseObject(postResult); + String code = jsonResult.getString("code"); + if (!StringUtils.equals("0", code)) { + String message = jsonResult.getString("message"); + throw new BusinessException(code, message); + } + } + + + /** + * 物联平台官方接口 + * + * 查询卡信息 + * + * @param iccIds 卡号集合 + * @return 卡信息集合 + */ + public List WuLianGetSimInfo(List iccIds) { + ArrayList list = new ArrayList<>(); + + if (CollectionUtils.isEmpty(iccIds)) { + return list; + } + for (String iccId : iccIds) { + JSONObject param = new JSONObject(); + param.put("appid", appId); + param.put("appsecret", appSecret); + param.put("name", getSimInfoName); + param.put("iccid", iccId); + + String result = HttpUtils.sendPostContentType(wuLianGetWay, param.toJSONString(), "application/json"); + logger.info("【====物联网智能云平台====】查询Sim卡信息, result: {}", result); + + JSONObject jsonObject = JSONObject.parseObject(result); + JSONArray dataArray = (JSONArray) jsonObject.get("data"); + + if (CollectionUtils.isNotEmpty(dataArray)) { + JSONObject data = dataArray.getJSONObject(0); + WuLianSimData wuLianSimData = JSONObject.parseObject(data.toJSONString(), WuLianSimData.class); + + list.add(wuLianSimData); + } + } + return list; + } + + + /** + * 物联平台 内部接口 + * 通过iccIds查询物联平台Sim卡信息 + * + * @param iccIds + * @return + */ + public List WuLianGetSimInfoByIccIds(List iccIds) { + List resultList = new ArrayList<>(); + + List list = WuLianGetSimInfo(iccIds); + SimCardVO vo = null; + + for (WuLianSimData wuLianSimData : list) { + String WuLianCardStatus = wuLianSimData.getCardStatus(); + + // 卡状态不一致,需做对应 + String dataBaseCardStatus = SimCardStatusCorrespondEnum.getDataBaseCardStatus(WuLianCardStatus); + + BigDecimal packageCanUsage = new BigDecimal(wuLianSimData.getPackageCanUsage()); // 可用流量(MB) + BigDecimal packageHasUsage = new BigDecimal(wuLianSimData.getPackageHasUsage()); // 已用流量(MB) + BigDecimal residualFlowRate = packageCanUsage.subtract(packageHasUsage); // 剩余 + + // 运营商 + String packageName = wuLianSimData.getPackageName(); + String operator = ""; + if (StringUtils.contains(packageName, "移动")) { + operator = "china_mobile"; + }else if (StringUtils.contains(packageName, "电信")) { + operator = "china_telecom"; + } else if (StringUtils.contains(packageName, "联通")) { + operator = "china_unicom"; + } + + //将信息封装到SimCardVO + vo = new SimCardVO(); + vo.setIccId(wuLianSimData.getIccId()); + vo.setSimCardFactory(SimSupplierEnum.WU_LIAN_INTERNET.getCode()); // 2-物联网智能云平台 + vo.setSimCardOperator(operator); // 运营商 + vo.setSimCardStatus(dataBaseCardStatus); // 卡状态 + vo.setName(packageName); // 套餐名称 + vo.setExpiredTime(wuLianSimData.getCardEndTime()); // 卡到期时间 + vo.setPackageCapacity(packageCanUsage); // 套餐容量 + vo.setUsedFlowRate(packageHasUsage); // 已用流量 + vo.setResidualFlowRate(residualFlowRate); // 剩余流量 + + + resultList.add(vo); + + } + return resultList; + + } + + + /** + * 物联平台续费接口 + * + * @param iccIds + * @param cycleNumber + * @return + */ + public void WuLianSimRenew(List iccIds, int cycleNumber) { + List resultList = new ArrayList<>(); + if (CollectionUtils.isEmpty(iccIds)) { + return; + } + // 先查出卡的信息 + List wuLianSimData = WuLianGetSimInfo(iccIds); + for (WuLianSimData data : wuLianSimData) { + JSONObject param = new JSONObject(); + param.put("appid", appId); + param.put("appsecret", appSecret); + param.put("name", WuLianSimRenew); + param.put("msisdn", data.getMsisdn()); + param.put("packageId", data.getPackageId()); // 套餐id + param.put("outOrderNo", IdUtils.generateOrderCode(data.getMsisdn())); // 外部业务订单号 + param.put("period", cycleNumber); // 续费周期 + + String result = HttpUtils.sendPostContentType(wuLianGetWay, param.toJSONString(), "application/json"); + logger.info("【====物联网智能云平台====】Sim卡续费, result: {}", result); + JSONObject resultJson = JSONObject.parseObject(result); + String resultCode = resultJson.getString("code"); + WuLianSimRenewVO vo = null; + if (!StringUtils.equals("0", resultCode)) { + throw new BusinessException(resultCode, resultJson.getString("msg")); + } + } + } + + /** + * 将list数组转化为逗号分隔的字符串 + * + * @param iccIds + * @return + */ + private String list2Str(List iccIds) { + iccIds = iccIds.stream() + .filter(StringUtils::isNotEmpty) + .filter(x -> !StringUtils.equals(x, "0")) + .collect(Collectors.toList()); + // 数组转成逗号分割的字符串 + String iccId = StringUtils.join(iccIds, ","); + return iccId; + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/WechatPayService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/WechatPayService.java new file mode 100644 index 000000000..2a0911a09 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/WechatPayService.java @@ -0,0 +1,43 @@ +package com.jsowell.pile.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.jsowell.pile.dto.WeixinPayDTO; +import com.jsowell.wxpay.response.WechatPayNotifyParameter; +import com.jsowell.wxpay.response.WechatPayRefundRequest; +import com.jsowell.wxpay.response.WechatPayRefundResponse; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +public interface WechatPayService { + /** + * 获取微信支付参数 + * @param dto + * @return + * @throws Exception + */ + Map weixinPayV3(WeixinPayDTO dto) throws Exception; + + /** + * 获取微信支付回调信息 + * @param request + * @param body + * @return + * @throws Exception + */ + Map wechatPayCallbackInfo(HttpServletRequest request, WechatPayNotifyParameter body) throws Exception; + + /** + * 微信退款接口 + * ApplyForARefund + */ + WechatPayRefundResponse ApplyForWechatPayRefundV3(WechatPayRefundRequest request) throws JsonProcessingException; + + /** + * 获取微信退款回调信息 + * @param request + * @param body + * @return + */ + Map wechatPayRefundCallbackInfo(HttpServletRequest request, WechatPayNotifyParameter body) throws Exception; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/WxpayCallbackRecordService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/WxpayCallbackRecordService.java new file mode 100644 index 000000000..a296f0d40 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/WxpayCallbackRecordService.java @@ -0,0 +1,34 @@ +package com.jsowell.pile.service; + +import com.jsowell.pile.domain.WxpayCallbackRecord; + +import java.util.List; + +public interface WxpayCallbackRecordService { + int deleteByPrimaryKey(Integer id); + + int insert(WxpayCallbackRecord record); + + int insertSelective(WxpayCallbackRecord record); + + WxpayCallbackRecord selectByPrimaryKey(Integer id); + + int updateByPrimaryKeySelective(WxpayCallbackRecord record); + + int updateByPrimaryKey(WxpayCallbackRecord record); + + WxpayCallbackRecord selectByOrderCode(String orderCode); + + /** + * 通过微信商户订单号查询 + * @param outTradeNo + * @return + */ + WxpayCallbackRecord selectByOutTradeNo(String outTradeNo); + + /** + * 根据memberId查询最近一年的余额充值记录 + * queryTheBalanceTopUpRecordOfTheLatestYear + */ + List queryBalanceRechargeRecordOfTheLatestYear(String memberId); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/WxpayRefundCallbackService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/WxpayRefundCallbackService.java new file mode 100644 index 000000000..9c4112164 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/WxpayRefundCallbackService.java @@ -0,0 +1,15 @@ +package com.jsowell.pile.service; + +import com.jsowell.pile.domain.WxpayRefundCallback; + +public interface WxpayRefundCallbackService { + int deleteByPrimaryKey(Integer id); + + int insertSelective(WxpayRefundCallback record); + + WxpayRefundCallback selectByPrimaryKey(Integer id); + + int updateByPrimaryKeySelective(WxpayRefundCallback record); + + int updateByPrimaryKey(WxpayRefundCallback record); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/MemberBasicInfoServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/MemberBasicInfoServiceImpl.java new file mode 100644 index 000000000..5cec801c2 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/MemberBasicInfoServiceImpl.java @@ -0,0 +1,264 @@ +package com.jsowell.pile.service.impl; + +import com.google.common.collect.Lists; +import com.jsowell.common.util.DateUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.pile.domain.MemberBasicInfo; +import com.jsowell.pile.domain.MemberWalletInfo; +import com.jsowell.pile.domain.MemberWalletLog; +import com.jsowell.pile.mapper.MemberBasicInfoMapper; +import com.jsowell.pile.mapper.MemberWalletInfoMapper; +import com.jsowell.pile.mapper.MemberWalletLogMapper; +import com.jsowell.pile.mapper.PileBasicInfoMapper; +import com.jsowell.pile.service.IMemberBasicInfoService; +import com.jsowell.pile.service.IPileBasicInfoService; +import com.jsowell.pile.vo.uniapp.MemberVO; +import com.jsowell.pile.vo.uniapp.MemberWalletLogVO; +import com.jsowell.pile.vo.uniapp.PersonalPileInfoVO; +import com.jsowell.pile.vo.web.UpdateMemberBalanceDTO; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 会员基础信息Service业务层处理 + * + * @author jsowell + * @date 2022-10-12 + */ +@Slf4j +@Service +public class MemberBasicInfoServiceImpl implements IMemberBasicInfoService { + @Autowired + private MemberBasicInfoMapper memberBasicInfoMapper; + + @Autowired + private MemberWalletInfoMapper memberWalletInfoMapper; + + @Autowired + private MemberWalletLogMapper memberWalletLogMapper; + + @Autowired + private IPileBasicInfoService pileBasicInfoService; + /** + * 查询会员基础信息 + * + * @param id 会员基础信息主键 + * @return 会员基础信息 + */ + @Override + public MemberBasicInfo selectMemberBasicInfoById(Integer id) { + return memberBasicInfoMapper.selectMemberBasicInfoById(id); + } + + /** + * 查询会员基础信息列表 + * + * @param memberBasicInfo 会员基础信息 + * @return 会员基础信息 + */ + /*@Override + public List selectMemberBasicInfoList(MemberBasicInfo memberBasicInfo) { + List voList = memberBasicInfoMapper.selectMemberList(memberBasicInfo.getMobileNumber(), memberBasicInfo.getNickName()); + for (MemberVO memberVO : voList) { + memberVO.setPrincipalBalance(memberVO.getPrincipalBalance() == null ? BigDecimal.ZERO : memberVO.getPrincipalBalance()); + memberVO.setGiftBalance(memberVO.getGiftBalance() == null ? BigDecimal.ZERO : memberVO.getGiftBalance()); + } + return voList; + }*/ + + /** + * 新增会员基础信息 + * + * @param memberBasicInfo 会员基础信息 + * @return 结果 + */ + @Override + public int insertMemberBasicInfo(MemberBasicInfo memberBasicInfo) { + memberBasicInfo.setCreateTime(DateUtils.getNowDate()); + return memberBasicInfoMapper.insertMemberBasicInfo(memberBasicInfo); + } + + /** + * 修改会员基础信息 + * + * @param memberBasicInfo 会员基础信息 + * @return 结果 + */ + @Override + public int updateMemberBasicInfo(MemberBasicInfo memberBasicInfo) { + memberBasicInfo.setUpdateTime(DateUtils.getNowDate()); + return memberBasicInfoMapper.updateMemberBasicInfo(memberBasicInfo); + } + + /** + * 批量删除会员基础信息 + * + * @param ids 需要删除的会员基础信息主键 + * @return 结果 + */ + @Override + public int deleteMemberBasicInfoByIds(List ids) { + return memberBasicInfoMapper.deleteMemberBasicInfoByIds(ids); + } + + /** + * 通过物理卡号查询会员基本信息 + * + * @param physicsCard 物理卡号 + * @return 会员基本信息 + */ + @Override + public MemberVO selectInfoByPhysicsCard(String physicsCard) { + return memberBasicInfoMapper.selectInfoByPhysicsCard(physicsCard); + } + + /** + * 根据手机号和运营商id查询会员信息 + * @param mobileNumber 手机号 + * @param merchantId 运营商id + * @return 会员信息 + */ + public MemberBasicInfo selectInfoByMobileNumberAndMerchantId(String mobileNumber, String merchantId) { + return memberBasicInfoMapper.selectInfoByMobileNumberAndMerchantId(mobileNumber, merchantId); + } + + @Override + public MemberBasicInfo selectInfoByMobileNumber(String mobileNumber) { + return selectInfoByMobileNumberAndMerchantId(mobileNumber, null); + } + + @Override + public MemberBasicInfo selectInfoByMemberId(String memberId) { + return memberBasicInfoMapper.selectInfoByMemberId(memberId); + } + + /** + * 修改用户余额 唯一方法 + * 接收的金额都是正数,通过操作类型判断 充值还是扣减 + */ + @Override + public int updateMemberBalance(UpdateMemberBalanceDTO dto) { + String memberId = dto.getMemberId(); + BigDecimal updateGiftBalance = dto.getUpdateGiftBalance(); + BigDecimal updatePrincipalBalance = dto.getUpdatePrincipalBalance(); + log.info("修改用户余额 memberId:{}, updatePrincipalBalance:{}, updateGiftBalance:{}", memberId, updatePrincipalBalance, updateGiftBalance); + // 查询用户余额 + MemberWalletInfo info = memberWalletInfoMapper.selectByMemberId(memberId); + if (info == null) { + log.warn("根据会员id:{}, 查询会员信息为空", memberId); + return 0; + } + + // 记录流水 + List logList = Lists.newArrayList(); + // 计算新的余额 + BigDecimal newPrincipalBalance = null; + BigDecimal newGiftBalance = null; + + // 更新本金金额 + if (updatePrincipalBalance != null) { + if (StringUtils.equals(dto.getType(), "2")) { + // 扣款 转为负数 + updatePrincipalBalance = updatePrincipalBalance.negate(); + } + // 会员老的余额 + BigDecimal oldPrincipalBalance = info.getPrincipalBalance() == null + ? BigDecimal.ZERO + : info.getPrincipalBalance(); + newPrincipalBalance = oldPrincipalBalance.add(updatePrincipalBalance); + if (newPrincipalBalance.compareTo(BigDecimal.ZERO) < 0) { + log.warn("新本金余额不能为负数"); + return 0; + } + // 记流水 + logList.add(MemberWalletLog.builder() + .memberId(dto.getMemberId()) + .type(dto.getType()) + .subType(dto.getSubType()) + .amount(updatePrincipalBalance) + .category("1") + .relatedOrderCode(dto.getRelatedOrderCode()) + .createBy(dto.getMemberId()) + .build()); + } + + // 更新赠送金额 + if (updateGiftBalance != null) { + if (StringUtils.equals(dto.getType(), "2")) { + // 扣款 转为负数 + updateGiftBalance = updateGiftBalance.negate(); + } + BigDecimal oldGiftBalance = info.getGiftBalance() == null + ? BigDecimal.ZERO + : info.getGiftBalance(); + newGiftBalance = oldGiftBalance.add(updateGiftBalance); + // 余额不能为负数 + if (newGiftBalance.compareTo(BigDecimal.ZERO) < 0) { + log.warn("新赠送余额不能为负数"); + return 0; + } + // 记流水 + logList.add(MemberWalletLog.builder() + .memberId(dto.getMemberId()) + .type(dto.getType()) + .subType(dto.getSubType()) + .amount(updateGiftBalance) + .category("2") + .relatedOrderCode(dto.getRelatedOrderCode()) + .createBy(dto.getMemberId()) + .build()); + } + + // 修改数据库 + int i = 0; + if (newPrincipalBalance != null || newGiftBalance != null) { + i = memberBasicInfoMapper.updateMemberBalance(memberId, newPrincipalBalance, newGiftBalance, info.getVersion()); + if (i == 0) { + log.warn("修改余额失败, memberId:{}", memberId); + } + } + // 插入 member_wallet_log 表 + if (CollectionUtils.isNotEmpty(logList)) { + memberWalletLogMapper.batchInsert(logList); + } + return i; + } + + @Override + public MemberVO queryMemberInfoByMemberId(String memberId) { + // 加缓存 + MemberVO vo = memberBasicInfoMapper.queryMemberInfoByMemberId(memberId); + return vo; + } + + @Override + public List selectMemberList(String mobileNumber, String nickName) { + return memberBasicInfoMapper.selectMemberList(mobileNumber, nickName); + } + + /** + * 查询用户账户余额变动信息 + * @param memberId 会员id + * @param type 1-进账;2-出账 不传查全部 + */ + @Override + public List getMemberBalanceChanges(String memberId, String type) { + return memberWalletLogMapper.getMemberBalanceChanges(memberId, type); + } + + /** + * 通过memberId查询会员的个人桩信息 + * @param memberId + * @return + */ + @Override + public List getMemberPersonPileInfo(String memberId) { + return pileBasicInfoService.getPileInfoByMemberId(memberId); + } + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/MemberTransactionRecordServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/MemberTransactionRecordServiceImpl.java new file mode 100644 index 000000000..6fac0fd17 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/MemberTransactionRecordServiceImpl.java @@ -0,0 +1,41 @@ +package com.jsowell.pile.service.impl; + +import com.google.common.collect.Lists; +import com.jsowell.common.util.bean.BeanUtils; +import com.jsowell.pile.domain.MemberTransactionRecord; +import com.jsowell.pile.mapper.MemberTransactionRecordMapper; +import com.jsowell.pile.service.IMemberTransactionRecordService; +import com.jsowell.pile.vo.web.MemberTransactionVO; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +@Service +public class MemberTransactionRecordServiceImpl implements IMemberTransactionRecordService { + + @Resource + private MemberTransactionRecordMapper memberTransactionRecordMapper; + + @Override + public int insertSelective(MemberTransactionRecord record) { + return memberTransactionRecordMapper.insertSelective(record); + } + + @Override + public List selectMemberTransactionRecordList(String memberId) { + List list = memberTransactionRecordMapper.selectByMemberId(memberId); + if (CollectionUtils.isEmpty(list)) { + return Lists.newArrayList(); + } + List resultList = Lists.newArrayList(); + for (MemberTransactionRecord memberTransactionRecord : list) { + MemberTransactionVO vo = new MemberTransactionVO(); + BeanUtils.copyBeanProp(vo, memberTransactionRecord); + resultList.add(vo); + } + return resultList; + } + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/MemberWalletInfoServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/MemberWalletInfoServiceImpl.java new file mode 100644 index 000000000..e41dbef02 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/MemberWalletInfoServiceImpl.java @@ -0,0 +1,46 @@ +package com.jsowell.pile.service.impl; + +import com.jsowell.pile.domain.MemberWalletInfo; +import com.jsowell.pile.mapper.MemberWalletInfoMapper; +import com.jsowell.pile.service.IMemberWalletInfoService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +@Service +public class MemberWalletInfoServiceImpl implements IMemberWalletInfoService { + + @Resource + private MemberWalletInfoMapper memberWalletInfoMapper; + + @Override + public int deleteByPrimaryKey(Integer id) { + return memberWalletInfoMapper.deleteByPrimaryKey(id); + } + + @Override + public int insert(MemberWalletInfo record) { + return memberWalletInfoMapper.insert(record); + } + + @Override + public int insertSelective(MemberWalletInfo record) { + return memberWalletInfoMapper.insertSelective(record); + } + + @Override + public MemberWalletInfo selectByPrimaryKey(Integer id) { + return memberWalletInfoMapper.selectByPrimaryKey(id); + } + + @Override + public int updateByPrimaryKeySelective(MemberWalletInfo record) { + return memberWalletInfoMapper.updateByPrimaryKeySelective(record); + } + + @Override + public int updateByPrimaryKey(MemberWalletInfo record) { + return memberWalletInfoMapper.updateByPrimaryKey(record); + } + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/MemberWalletLogServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/MemberWalletLogServiceImpl.java new file mode 100644 index 000000000..c66e39eac --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/MemberWalletLogServiceImpl.java @@ -0,0 +1,46 @@ +package com.jsowell.pile.service.impl; + +import com.jsowell.pile.domain.MemberWalletLog; +import com.jsowell.pile.mapper.MemberWalletLogMapper; +import com.jsowell.pile.service.IMemberWalletLogService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +@Service +public class MemberWalletLogServiceImpl implements IMemberWalletLogService { + + @Resource + private MemberWalletLogMapper memberWalletLogMapper; + + // @Override + // public int deleteByPrimaryKey(Integer id) { + // return memberWalletLogMapper.deleteByPrimaryKey(id); + // } + // + // @Override + // public int insert(MemberWalletLog record) { + // return memberWalletLogMapper.insert(record); + // } + // + // @Override + // public int insertSelective(MemberWalletLog record) { + // return memberWalletLogMapper.insertSelective(record); + // } + // + // @Override + // public MemberWalletLog selectByPrimaryKey(Integer id) { + // return memberWalletLogMapper.selectByPrimaryKey(id); + // } + // + // @Override + // public int updateByPrimaryKeySelective(MemberWalletLog record) { + // return memberWalletLogMapper.updateByPrimaryKeySelective(record); + // } + // + // @Override + // public int updateByPrimaryKey(MemberWalletLog record) { + // return memberWalletLogMapper.updateByPrimaryKey(record); + // } + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderAbnormalRecordServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderAbnormalRecordServiceImpl.java new file mode 100644 index 000000000..c5d0f5de1 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderAbnormalRecordServiceImpl.java @@ -0,0 +1,88 @@ +package com.jsowell.pile.service.impl; + +import com.jsowell.pile.domain.OrderAbnormalRecord; +import com.jsowell.pile.mapper.OrderAbnormalRecordMapper; +import com.jsowell.pile.service.IOrderAbnormalRecordService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 订单异常记录Service业务层处理 + * + * @author jsowell + * @date 2023-02-13 + */ +@Service +public class OrderAbnormalRecordServiceImpl implements IOrderAbnormalRecordService { + @Autowired + private OrderAbnormalRecordMapper orderAbnormalRecordMapper; + + /** + * 查询订单异常记录 + * + * @param id 订单异常记录主键 + * @return 订单异常记录 + */ + @Override + public OrderAbnormalRecord selectOrderAbnormalRecordById(Integer id) { + return orderAbnormalRecordMapper.selectOrderAbnormalRecordById(id); + } + + /** + * 查询订单异常记录列表 + * + * @param orderAbnormalRecord 订单异常记录 + * @return 订单异常记录 + */ + @Override + public List selectOrderAbnormalRecordList(OrderAbnormalRecord orderAbnormalRecord) { + return orderAbnormalRecordMapper.selectOrderAbnormalRecordList(orderAbnormalRecord); + } + + /** + * 新增订单异常记录 + * + * @param orderAbnormalRecord 订单异常记录 + * @return 结果 + */ + @Override + public int insertOrderAbnormalRecord(OrderAbnormalRecord orderAbnormalRecord) { + // orderAbnormalRecord.setCreateTime(DateUtils.getNowDate()); + return orderAbnormalRecordMapper.insertOrderAbnormalRecord(orderAbnormalRecord); + } + + /** + * 修改订单异常记录 + * + * @param orderAbnormalRecord 订单异常记录 + * @return 结果 + */ + @Override + public int updateOrderAbnormalRecord(OrderAbnormalRecord orderAbnormalRecord) { + return orderAbnormalRecordMapper.updateOrderAbnormalRecord(orderAbnormalRecord); + } + + /** + * 批量删除订单异常记录 + * + * @param ids 需要删除的订单异常记录主键 + * @return 结果 + */ + @Override + public int deleteOrderAbnormalRecordByIds(Integer[] ids) { + return orderAbnormalRecordMapper.deleteOrderAbnormalRecordByIds(ids); + } + + /** + * 删除订单异常记录信息 + * + * @param id 订单异常记录主键 + * @return 结果 + */ + @Override + public int deleteOrderAbnormalRecordById(Integer id) { + return orderAbnormalRecordMapper.deleteOrderAbnormalRecordById(id); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java new file mode 100644 index 000000000..7a0ce2ed2 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java @@ -0,0 +1,852 @@ +package com.jsowell.pile.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.jsowell.common.annotation.DataScope; +import com.jsowell.common.constant.CacheConstants; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.domain.ykc.RealTimeMonitorData; +import com.jsowell.common.core.domain.ykc.TransactionRecordsData; +import com.jsowell.common.core.redis.RedisCache; +import com.jsowell.common.enums.MemberWalletEnum; +import com.jsowell.common.enums.ykc.OrderPayModeEnum; +import com.jsowell.common.enums.ykc.OrderPayStatusEnum; +import com.jsowell.common.enums.ykc.OrderStatusEnum; +import com.jsowell.common.enums.ykc.PileConnectorDataBaseStatusEnum; +import com.jsowell.common.enums.ykc.ReturnCodeEnum; +import com.jsowell.common.exception.BusinessException; +import com.jsowell.common.util.DateUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.bean.BeanUtils; +import com.jsowell.common.util.id.SnowflakeIdWorker; +import com.jsowell.pile.domain.OrderAbnormalRecord; +import com.jsowell.pile.domain.OrderBasicInfo; +import com.jsowell.pile.domain.OrderDetail; +import com.jsowell.pile.domain.OrderPayRecord; +import com.jsowell.pile.domain.WxpayCallbackRecord; +import com.jsowell.pile.dto.IndexQueryDTO; +import com.jsowell.pile.dto.QueryOrderDTO; +import com.jsowell.pile.dto.QueryPersonPileDTO; +import com.jsowell.pile.mapper.OrderBasicInfoMapper; +import com.jsowell.pile.service.IMemberBasicInfoService; +import com.jsowell.pile.service.IOrderAbnormalRecordService; +import com.jsowell.pile.service.IOrderBasicInfoService; +import com.jsowell.pile.service.IOrderPayRecordService; +import com.jsowell.pile.service.IPileConnectorInfoService; +import com.jsowell.pile.service.WechatPayService; +import com.jsowell.pile.service.WxpayCallbackRecordService; +import com.jsowell.pile.transaction.dto.OrderTransactionDTO; +import com.jsowell.pile.transaction.service.TransactionService; +import com.jsowell.pile.vo.uniapp.*; +import com.jsowell.pile.vo.web.IndexOrderInfoVO; +import com.jsowell.pile.vo.web.OrderListVO; +import com.jsowell.pile.vo.web.OrderTotalDataVO; +import com.jsowell.pile.vo.web.PileConnectorInfoVO; +import com.jsowell.pile.vo.web.UpdateMemberBalanceDTO; +import com.jsowell.wxpay.common.WeChatPayParameter; +import com.jsowell.wxpay.dto.WeChatRefundDTO; +import com.jsowell.wxpay.dto.WechatSendMsgDTO; +import com.jsowell.wxpay.response.WechatPayRefundRequest; +import com.jsowell.wxpay.response.WechatPayRefundResponse; +import com.jsowell.wxpay.service.WxAppletRemoteService; +import org.apache.commons.collections4.CollectionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 订单Service业务层处理 + * + * @author jsowell + * @date 2022-09-30 + */ +@Service +public class OrderBasicInfoServiceImpl implements IOrderBasicInfoService { + + private Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Autowired + private OrderBasicInfoMapper orderBasicInfoMapper; + + @Autowired + private TransactionService transactionService; + + @Autowired + private RedisCache redisCache; + + @Autowired + private IOrderPayRecordService orderPayRecordService; + + @Autowired + private IMemberBasicInfoService memberBasicInfoService; + + @Autowired + private WxpayCallbackRecordService wxpayCallbackRecordService; + + @Autowired + private WechatPayService wechatPayService; + + @Autowired + private IOrderAbnormalRecordService orderAbnormalRecordService; + + @Autowired + private WxAppletRemoteService wxAppletRemoteService; + + @Autowired + private IPileConnectorInfoService pileConnectorInfoService; + + /** + * 查询订单 + * + * @param id 订单主键 + * @return 订单 + */ + @Override + public OrderBasicInfo selectOrderBasicInfoById(Long id) { + return orderBasicInfoMapper.selectOrderBasicInfoById(id); + } + + /** + * 条件查询订单基本信息 + * @param info + * @return + */ + @Override + public OrderBasicInfo getOrderBasicInfo(OrderBasicInfo info) { + return orderBasicInfoMapper.getOrderBasicInfo(info); + } + + /** + * 查询订单列表 + * + * @param dto 订单 + * @return 订单 + */ + @Override + @DataScope(deptAlias = "t3") + public List selectOrderBasicInfoList(QueryOrderDTO dto) { + List orderListVOS = orderBasicInfoMapper.selectOrderBasicInfoList(dto); + if (CollectionUtils.isNotEmpty(orderListVOS)) { + for (OrderListVO orderListVO : orderListVOS) { + // 如果是微信支付,通过订单号查询微信支付单号 + if (StringUtils.equals(OrderPayModeEnum.PAYMENT_OF_WECHATPAY.getValue(), orderListVO.getPayMode())) { + WxpayCallbackRecord wxpayCallbackRecord = wxpayCallbackRecordService.selectByOrderCode(orderListVO.getOrderCode()); + orderListVO.setOutTradeNo(wxpayCallbackRecord.getOutTradeNo()); + } + orderListVO.setPileConnectorCode(orderListVO.getPileSn() + orderListVO.getConnectorCode()); + + // 订单状态描述 + String orderStatus = orderListVO.getOrderStatus(); // 订单状态 + String payStatus = orderListVO.getPayStatus(); // 支付状态 + String orderStatusDescribe; + if (StringUtils.equals(orderStatus, OrderStatusEnum.NOT_START.getValue())) { + // 未启动还有两种情况 待支付 / 支付成功 + if (StringUtils.equals(payStatus, OrderPayStatusEnum.paid.getValue())) { + // 支付成功,未启动 + orderStatusDescribe = OrderPayStatusEnum.paid.getLabel() + ", " + OrderStatusEnum.getOrderStatus(orderStatus); + } else { + // 待支付 + orderStatusDescribe = OrderPayStatusEnum.unpaid.getLabel(); + } + } else { + orderStatusDescribe = OrderStatusEnum.getOrderStatus(orderStatus); + } + orderListVO.setOrderStatusDescribe(orderStatusDescribe); + } + } + return orderListVOS; + } + + /** + * 查询时间段内订单总金额和总用电量 + * + * @param dto + * @return + */ + @Override + @DataScope(deptAlias = "t3") + public OrderTotalDataVO getOrderTotalData(QueryOrderDTO dto) { + OrderTotalDataVO vo = new OrderTotalDataVO(); + dto.setOrderStatus(OrderStatusEnum.ORDER_COMPLETE.getValue()); + List list = orderBasicInfoMapper.selectOrderBasicInfoList(dto); + BigDecimal sumOrderAmount = BigDecimal.ZERO; + BigDecimal sumUsedElectricity = BigDecimal.ZERO; + vo.setDateDescription(dto.getStartTime() + " - " + dto.getEndTime()); + if (CollectionUtils.isNotEmpty(list)) { + for (OrderListVO orderListVO : list) { + BigDecimal orderAmount = StringUtils.isBlank(orderListVO.getOrderAmount()) + ? BigDecimal.ZERO + : new BigDecimal(orderListVO.getOrderAmount()); + sumOrderAmount = sumOrderAmount.add(orderAmount); + + BigDecimal chargingDegree = StringUtils.isBlank(orderListVO.getChargingDegree()) + ? BigDecimal.ZERO + : new BigDecimal(orderListVO.getChargingDegree()); + sumUsedElectricity = sumUsedElectricity.add(chargingDegree); + } + } + vo.setSumOrderAmount(sumOrderAmount); + vo.setSumUsedElectricity(sumUsedElectricity); + return vo; + } + + /** + * 通过订单号查询订单信息(小程序发送消息用) + * @param orderCode + * @return + */ + @Override + public SendMessageVO selectOrderInfoByOrderCode(String orderCode) { + return orderBasicInfoMapper.selectOrderInfoByOrderCode(orderCode); + } + + /** + * 充电桩启动失败 + * @param orderCode + * @param failedReasonMsg + */ + @Override + public void chargingPileFailedToStart(String orderCode, String failedReasonMsg) { + OrderBasicInfo orderInfo = getOrderInfoByOrderCode(orderCode); + if (orderInfo == null) { + return; + } + // 启动失败原因 + orderInfo.setReason(failedReasonMsg); + // 订单退款(结算订单) + TransactionRecordsData data = TransactionRecordsData.builder() + .orderCode(orderInfo.getOrderCode()) + .consumptionAmount(String.valueOf(orderInfo.getOrderAmount())) + .stopReasonMsg(failedReasonMsg) + .totalElectricity(Constants.ZERO) + .sharpUsedElectricity(Constants.ZERO) + .peakUsedElectricity(Constants.ZERO) + .flatUsedElectricity(Constants.ZERO) + .valleyUsedElectricity(Constants.ZERO) + .build(); + settleOrder(data, orderInfo); + } + + /** + * 充电桩启动成功 + * @param orderCode + */ + @Override + public void chargingPileStartedSuccessfully(String orderCode) { + OrderBasicInfo orderInfo = getOrderInfoByOrderCode(orderCode); + if (orderInfo == null) { + return; + } + // 启动成功,订单状态改为充电中 + orderInfo.setOrderStatus(OrderStatusEnum.IN_THE_CHARGING.getValue()); + if (orderInfo.getChargeStartTime() == null) { + orderInfo.setChargeStartTime(new Date()); + } + updateOrderBasicInfo(orderInfo); + } + + public static void main(String[] args) { + Date date = DateUtils.addMinute(new Date(), -10); + System.out.println(DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, date)); + long currentTimeMillis = System.currentTimeMillis(); + if (currentTimeMillis - date.getTime() > 1000 * 60 * 15) { + // 已经支付超过15分钟 + System.out.println("已经支付超过15分钟"); + } else { + System.out.println("已经支付, 不到15分钟"); + } + } + + @Override + public void closeStartFailedOrder(String startTime, String endTime) { + List orderList = orderBasicInfoMapper.selectOrderListByTimeRangeAndStatus(startTime, endTime, OrderStatusEnum.NOT_START.getValue(), OrderPayStatusEnum.paid.getValue()); + List orderCodeList = orderList.stream().map(OrderBasicInfo::getOrderCode).collect(Collectors.toList()); + logger.info("{}-{}期间,共{}条,支付成功未启动订单,订单号:{}", startTime, endTime, orderList.size(), orderCodeList); + if (CollectionUtils.isEmpty(orderList)) { + return; + } + long currentTimeMillis = System.currentTimeMillis(); + // 判断支付成功时间距当前时间是否大于15分钟 + for (OrderBasicInfo orderBasicInfo : orderList) { + String orderCode = orderBasicInfo.getOrderCode(); + Date payTime = orderBasicInfo.getPayTime(); + if (payTime == null) { + continue; + } + long time = payTime.getTime(); + if (currentTimeMillis - time < 1000 * 60 * 15) { + continue; + } + // 已经支付超过15分钟 判断充电桩的状态 + String pileConnectorCode = orderBasicInfo.getPileSn() + orderBasicInfo.getConnectorCode(); + PileConnectorInfoVO connector = pileConnectorInfoService.getPileConnectorInfoByConnectorCode(pileConnectorCode); + // 充电桩不在线,则不做处理。请联系管理员操作 + if (PileConnectorDataBaseStatusEnum.OFF_NETWORK.getValue().equals(String.valueOf(connector.getStatus()))) { + logger.info("订单号:{}已经支付超过15分钟,充电桩:{}未启动,但是该桩为离线状态,请联系管理员处理", orderCode, pileConnectorCode); + continue; + } + // 获取订单的实时检测数据,有实时数据说明充电了,没有实时数据说明桩确实没有充电 + List chargingRealTimeData = getChargingRealTimeData(orderCode); + if (CollectionUtils.isEmpty(chargingRealTimeData)) { + // 充电桩在线,并且没有0x13实时数据,则执行结算退款操作 + chargingPileFailedToStart(orderCode, "充电桩启动失败,执行退款处理"); + } + } + } + + /** + * 查询充电中的订单,没有数据权限校验,后管不要用 + * + * @param pileSn + * @return + */ + @Override + public List selectChargingOrder(String pileSn) { + QueryOrderDTO dto = QueryOrderDTO.builder() + .pileSn(pileSn) + .orderStatus(OrderStatusEnum.IN_THE_CHARGING.getValue()) + .build(); + return orderBasicInfoMapper.selectOrderBasicInfoList(dto); + } + + /** + * 修改订单 + * + * @param orderBasicInfo 订单 + * @return 结果 + */ + @Transactional + @Override + public int updateOrderBasicInfo(OrderBasicInfo orderBasicInfo) { + // 清缓存 + String redisKey = CacheConstants.GET_ORDER_INFO_BY_ORDER_CODE + orderBasicInfo.getOrderCode(); + redisCache.deleteObject(redisKey); + return orderBasicInfoMapper.updateOrderBasicInfo(orderBasicInfo); + } + + /** + * 批量删除订单 + * + * @param ids 需要删除的订单主键 + * @return 结果 + */ + @Transactional + @Override + public int deleteOrderBasicInfoByIds(Long[] ids) { + orderBasicInfoMapper.deleteOrderDetailByOrderCodes(ids); + return orderBasicInfoMapper.deleteOrderBasicInfoByIds(ids); + } + + /** + * 通过订单号查询订单信息 + * + * @param orderCode 订单号 + * @return + */ + @Override + public OrderBasicInfo getOrderInfoByOrderCode(String orderCode) { + String redisKey = CacheConstants.GET_ORDER_INFO_BY_ORDER_CODE + orderCode; + OrderBasicInfo orderBasicInfo = redisCache.getCacheObject(redisKey); + if (orderBasicInfo == null) { + // 查数据库 + orderBasicInfo = orderBasicInfoMapper.getOrderInfoByOrderCode(orderCode); + // 放缓存 + if (orderBasicInfo != null) { + redisCache.setCacheObject(redisKey, orderBasicInfo, 5, TimeUnit.MINUTES); + } + } + logger.info("通过订单号:{}, 查询订单信息:{}", orderCode, JSON.toJSONString(orderBasicInfo)); + return orderBasicInfo; + } + + /** + * 根据充电桩编号和枪口号查询正在充电中的订单 + * @param pileSn 桩编号 + * @param connectorCode 枪口号 + * @return + */ + @Override + public OrderBasicInfo queryChargingByPileSnAndConnectorCode(String pileSn, String connectorCode) { + return orderBasicInfoMapper.queryOrderBasicInfo(pileSn, connectorCode, OrderStatusEnum.IN_THE_CHARGING.getValue()); + } + + /** + * 根据枪口编号查询正在充电中的订单 + * @param pileConnectorCode + * @return + */ + @Override + public OrderBasicInfo queryChargingByPileConnectorCode(String pileConnectorCode) { + if (StringUtils.isBlank(pileConnectorCode)) { + return null; + } + String pileSn = pileConnectorCode.substring(0, pileConnectorCode.length() - 2); + String connectorCode = pileConnectorCode.substring(pileConnectorCode.length() - 2); + return queryChargingByPileSnAndConnectorCode(pileSn, connectorCode); + } + + /** + * 结算订单逻辑 + * + * @param data 交易记录数据 + * @param orderBasicInfo 订单主表信息,由调用方传过来 + */ + @Override + public void settleOrder(TransactionRecordsData data, OrderBasicInfo orderBasicInfo) { + logger.info("结算订单start data:{}, orderBasicInfo:{}", data.toString(), orderBasicInfo.toString()); + String orderCode = data.getOrderCode(); + + // 判断订单状态 + if (StringUtils.equals(orderBasicInfo.getOrderStatus(), OrderStatusEnum.ORDER_COMPLETE.getValue())) { + logger.info("结算订单:{}, 是订单完成状态", orderCode); + return; + } + // 消费金额就是订单总金额 + BigDecimal orderAmount = new BigDecimal(data.getConsumptionAmount()); + + // 付款金额 - 实际消费金额,如果有剩余,需要走退款操作 当使用余额支付时payAmount = principalPay + giftPay + BigDecimal payAmount = orderBasicInfo.getPayAmount(); + + // 有时候充电桩到达金额停止充电会多出一点金额,比如实际需要充50元的电,充电桩传来的消费金额为50.01元,在后台记录的时候需要舍去 + if (orderAmount.compareTo(payAmount) > 0) { + logger.info("结算订单:【{}】充电桩传来的消费金额:【{}】大于付款金额:【{}】, 消费金额设置为付款金额相等数据", orderBasicInfo.getOrderCode(), orderAmount, payAmount); + orderAmount = payAmount; + } + + // 剩余需要退回的金额 residue + BigDecimal residue = payAmount.subtract(orderAmount); + if (residue.compareTo(BigDecimal.ZERO) > 0) { + // 查支付记录 + List payRecordList = orderPayRecordService.getOrderPayRecordList(orderBasicInfo.getOrderCode()); + // 更新订单支付记录 + List updatePayRecordList = Lists.newArrayList(); + // 根据支付方式不同,走不同渠道退款 + String payMode = orderBasicInfo.getPayMode(); + if (StringUtils.equals(payMode, OrderPayModeEnum.PAYMENT_OF_BALANCE.getValue())) { // 余额支付 + Map payRecordMap = payRecordList.stream() + .collect(Collectors.toMap(OrderPayRecord::getPayMode, Function.identity(), (k1, k2) -> k1)); + // 取出本金支付金额,赠送支付金额 + BigDecimal principalPay = null; + BigDecimal giftPay = null; + + OrderPayRecord principalPayRecord = payRecordMap.get("1"); + if (principalPayRecord != null) { + principalPay = principalPayRecord.getPayAmount(); + } + + OrderPayRecord giftPayRecord = payRecordMap.get("2"); + if (giftPayRecord != null) { + giftPay = giftPayRecord.getPayAmount(); + } + + Map returnAmountMap = calculateReturnAmount(principalPay, giftPay, orderAmount); + logger.info("结算订单:{}, 剩余金额退回余额, 订单消费金额:{}, 本金支付金额:{}, 赠送支付金额:{}, 退回金额map:{}", + orderCode, orderAmount, principalPay, giftPay, JSONObject.toJSONString(returnAmountMap)); + // 更新会员钱包 + BigDecimal returnPrincipal = returnAmountMap.get("returnPrincipal"); + if (returnPrincipal != null && principalPayRecord != null) { + principalPayRecord.setRefundAmount(returnPrincipal); + updatePayRecordList.add(principalPayRecord); + } + BigDecimal returnGift = returnAmountMap.get("returnGift"); + if (returnGift != null && giftPayRecord != null) { + giftPayRecord.setRefundAmount(returnGift); + updatePayRecordList.add(giftPayRecord); + } + UpdateMemberBalanceDTO updateMemberBalanceDTO = UpdateMemberBalanceDTO.builder() + .memberId(orderBasicInfo.getMemberId()) + .type(MemberWalletEnum.TYPE_IN.getValue()) // 进账 + .subType(MemberWalletEnum.SUBTYPE_ORDER_SETTLEMENT_REFUND.getValue()) // 订单结算退款 + .updatePrincipalBalance(returnPrincipal) + .updateGiftBalance(returnGift) + .relatedOrderCode(orderCode) + .build(); + memberBasicInfoService.updateMemberBalance(updateMemberBalanceDTO); + } else if (StringUtils.equals(payMode, OrderPayModeEnum.PAYMENT_OF_WECHATPAY.getValue())) { // 微信支付 + // 微信退款逻辑 + WeChatRefundDTO weChatRefundDTO = new WeChatRefundDTO(); + weChatRefundDTO.setOrderCode(orderCode); + weChatRefundDTO.setRefundType("1"); + weChatRefundDTO.setRefundAmount(residue); + this.weChatRefund(weChatRefundDTO); + // 订单支付记录 + OrderPayRecord orderPayRecord = payRecordList.get(0); + orderPayRecord.setRefundAmount(residue); + updatePayRecordList.add(orderPayRecord); + } else if (StringUtils.equals(payMode, OrderPayModeEnum.PAYMENT_OF_ALIPAY.getValue())) { // 支付宝支付 + // 支付宝退款逻辑 + } else { + // 白名单支付 + logger.info("订单:{}使用白名单支付,不进行退款处理", orderBasicInfo.getOrderCode()); + } + + // 更新order_pay_record + if (CollectionUtils.isNotEmpty(updatePayRecordList)) { + for (OrderPayRecord orderPayRecord : updatePayRecordList) { + orderPayRecordService.updateByPrimaryKeySelective(orderPayRecord); + } + } + } + + // 修改订单数据 + // 查询订单详情 + OrderDetail orderDetail = getOrderDetailByOrderCode(orderCode); + + // 把交易记录中的用电量,金额等信息 更新到orderBasicInfo和orderDetail + orderBasicInfo.setOrderAmount(orderAmount); // 订单总金额 + orderBasicInfo.setOrderStatus(OrderStatusEnum.ORDER_COMPLETE.getValue()); + orderBasicInfo.setReason(data.getStopReasonMsg()); // 充电停止原因 + orderBasicInfo.setSettlementTime(DateUtils.getNowDate()); // 结算时间 + orderBasicInfo.setRefundAmount(residue); // 结算退款金额 + + orderDetail.setTotalUsedElectricity(new BigDecimal(data.getTotalElectricity())); // 总用电量 + orderDetail.setTotalOrderAmount(orderAmount); // 订单总金额 + orderDetail.setSharpUsedElectricity(new BigDecimal(data.getSharpUsedElectricity())); // 尖时段用电量 + orderDetail.setPeakUsedElectricity(new BigDecimal(data.getPeakUsedElectricity())); // 峰时段用电量 + orderDetail.setFlatUsedElectricity(new BigDecimal(data.getFlatUsedElectricity())); // 平时段用电量 + orderDetail.setValleyUsedElectricity(new BigDecimal(data.getValleyUsedElectricity())); // 谷时段用电量 + OrderTransactionDTO dto = new OrderTransactionDTO(); + dto.setOrderBasicInfo(orderBasicInfo); + dto.setOrderDetail(orderDetail); + transactionService.doUpdateOrder(dto); + logger.info("结算订单 end OrderTransactionDTO:{}", JSONObject.toJSONString(dto)); + + try { + // uniApp 发送停止充电订阅消息 + WechatSendMsgDTO wechatSendMsgDTO = new WechatSendMsgDTO(); + wechatSendMsgDTO.setOrderCode(orderCode); + Map resultMap = wxAppletRemoteService.stopChargingSendMsg(wechatSendMsgDTO); + logger.info("小程序发送充电停止推送消息 result:{}", JSON.toJSONString(resultMap)); + }catch (Exception e) { + logger.error("小程序发送充电停止推送消息 error", e); + } + } + + /** + * 余额支付 计算需要退回的金额 + * + * @param principalPay + * @param giftPay + * @param orderAmount + * @return + */ + private Map calculateReturnAmount(BigDecimal principalPay, BigDecimal giftPay, BigDecimal orderAmount) { + Map resultMap = Maps.newHashMap(); + + // 消费金额优先使用本金 + BigDecimal returnPrincipal = null; // 退回本金金额 + BigDecimal returnGift = null; // 退回赠送金额 + + // 余额支付 有3种情况 + if (principalPay != null && giftPay == null) { + // 只有本金支付 + BigDecimal subtract = principalPay.subtract(orderAmount); + if (subtract.compareTo(BigDecimal.ZERO) > 0) { + returnPrincipal = subtract; + } + } + if (principalPay == null && giftPay != null) { + // 只有赠送金额支付 + BigDecimal subtract = giftPay.subtract(orderAmount); + if (subtract.compareTo(BigDecimal.ZERO) > 0) { + returnGift = subtract; + } + } + if (principalPay != null && giftPay != null) { + // 本金+赠送支付 + BigDecimal subtract = principalPay.subtract(orderAmount); + if (subtract.compareTo(BigDecimal.ZERO) > 0) { + // 本金减掉订单金额后还有剩余,那就把剩余的退回,赠送原封不动退回 + returnPrincipal = subtract; + returnGift = giftPay; + } else if (subtract.compareTo(BigDecimal.ZERO) == 0) { + // 本金刚好够,那赠送金额支付的原封不动退回 + returnGift = giftPay; + } else { + returnGift = giftPay.subtract(subtract.negate()); + } + } + + resultMap.put("returnPrincipal", returnPrincipal); + resultMap.put("returnGift", returnGift); + return resultMap; + } + + /** + * 结算订单退款和用户余额退款调这个方法 + * + * @param dto + */ + @Override + public void weChatRefund(WeChatRefundDTO dto) { + // 退款有两种情况 1-订单结算退款 2-用户余额退款 + String refundType = dto.getRefundType(); + if (StringUtils.equals(refundType, "1")) { + WechatPayRefundResponse response = refundForOrder(dto); + logger.info("订单结算退款 result:{}", JSONObject.toJSONString(response)); + } else if (StringUtils.equals(refundType, "2")) { + WechatPayRefundResponse response = refundForBalance(dto); + logger.info("用户余额退款 result:{}", JSONObject.toJSONString(response)); + } else { + logger.warn("没有找到退款处理逻辑"); + } + } + + @Override + public void saveAbnormalOrder(TransactionRecordsData data) { + OrderAbnormalRecord record = new OrderAbnormalRecord(); + BeanUtils.copyBeanProp(record, data); + orderAbnormalRecordService.insertOrderAbnormalRecord(record); + } + + /** + * 根据订单编号获取充电实时数据 时间倒序 + * 订单只有在充电中,才会把实时数据保存到redis + * @param orderCode 订单编号 + * @return + */ + @Override + public List getChargingRealTimeData(String orderCode) { + List resultList = Lists.newArrayList(); + if (StringUtils.isBlank(orderCode)) { + return resultList; + } + String pileConnectorCode = orderCode.substring(0, 16); + String redisKey = CacheConstants.PILE_REAL_TIME_MONITOR_DATA + pileConnectorCode + "_" + orderCode; + // 拿到所有数据 + Map map = redisCache.hmget(redisKey); + if (map != null && !map.isEmpty()) { + List keyList = map.keySet().stream() + .map(x -> (String) x) + .sorted(Comparator.reverseOrder()) // 对keyList排序 时间倒序 + .collect(Collectors.toList()); + for (String s : keyList) { + Object o = map.get(s); + RealTimeMonitorData data = JSONObject.parseObject((String) o, RealTimeMonitorData.class); + resultList.add(data); + } + } + return resultList; + } + + /** + * 首页订单数据展示 + * + * @param dto 首页信息查询dto + * @return + */ + @Override + public List getIndexOrderInfo(IndexQueryDTO dto) { + return orderBasicInfoMapper.getIndexOrderInfo(dto); + } + + /** + * 获取超过15分钟的待支付状态订单 + * + * @return + */ + @Override + public List getUnpaidOrderListOver15Min() { + Date now = DateUtils.addMinute(new Date(), -15); + String nowString = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, now); + return orderBasicInfoMapper.getUnpaidOrderListOver15Min(nowString); + } + + /** + * 根据orderId批量修改订单状态 + * + * @param orderIds + * @param orderStatus + */ + @Override + public void updateOrderStatusById(List orderIds, String orderStatus) { + orderBasicInfoMapper.updateOrderStatusById(orderIds, orderStatus); + } + + @Override + public int close15MinutesOfUnpaidOrders() { + List orderList = getUnpaidOrderListOver15Min(); + if (CollectionUtils.isNotEmpty(orderList)) { + List orderIdList = orderList.stream() + .map(x -> String.valueOf(x.getId())) + .collect(Collectors.toList()); + // 修改订单状态 + updateOrderStatusById(orderIdList, OrderStatusEnum.ORDER_CLOSE_TIMEOUT.getValue()); + } + return orderList.size(); + } + + /** + * 订单退款处理逻辑 + */ + private WechatPayRefundResponse refundForOrder(WeChatRefundDTO dto) { + // 查到orderCode对应的支付订单 + // OrderBasicInfo orderInfo = this.getOrderInfoByOrderCode(dto.getOrderCode()); + // // 校验订单 + // if (Objects.isNull(orderInfo)) { + // logger.warn("orderCode:{}, 订单退款处理逻辑, 查询订单为空!", dto.getOrderCode()); + // throw new BusinessException(ReturnCodeEnum.CODE_QUERY_ORDER_NULL_ERROR); + // } + // // 判断退款金额,不能大于支付金额 + // BigDecimal payAmount = orderInfo.getPayAmount(); + // // BigDecimal orderAmount = orderInfo.getOrderAmount(); + // // BigDecimal amountToBeRefunded = payAmount.subtract(orderAmount); // 可退金额 + // BigDecimal amountToBeRefunded = dto.getRefundAmount(); // 可退金额 + // logger.info("订单:{}, 支付金额:{}, 需退款金额:{}", dto.getOrderCode(), payAmount, amountToBeRefunded); + // if (dto.getRefundAmount().compareTo(amountToBeRefunded) > 0) { + // // 退款金额,大于可退金额 抛出异常 + // logger.warn("退款金额:{},大于可退金额{}, 抛出异常", dto.getRefundAmount(), amountToBeRefunded); + // throw new BusinessException(ReturnCodeEnum.CODE_REFUND_ORDER_AMOUNT_ERROR); + // } + // 查出来原来的支付信息 + WxpayCallbackRecord record = wxpayCallbackRecordService.selectByOrderCode(dto.getOrderCode()); + if (Objects.isNull(record)) { + logger.warn("orderCode:{}, 订单退款处理逻辑, 查询订单微信支付记录为空!", dto.getOrderCode()); + throw new BusinessException(ReturnCodeEnum.CODE_REFUND_ORDER_CALLBACK_RECORD_ERROR); + } + + // 判断支付金额和退款金额 + int refundAmountInt = dto.getRefundAmount().multiply(new BigDecimal("100")).intValue(); + int payerTotalInt = Integer.parseInt(record.getPayerTotal()); + if (refundAmountInt > payerTotalInt) { + logger.warn("订单号:{}, 退款金额:{}(分),大于可退金额{}(分), 抛出异常", dto.getOrderCode(), refundAmountInt, payerTotalInt); + throw new BusinessException(ReturnCodeEnum.CODE_REFUND_ORDER_AMOUNT_ERROR); + } + + // 调微信退款接口 + WechatPayRefundRequest request = new WechatPayRefundRequest(); + request.setTransaction_id(record.getTransactionId()); // 微信支付单号 + request.setOut_trade_no(record.getOutTradeNo()); // 商户订单号 + // 生成退款单号 + request.setOut_refund_no(SnowflakeIdWorker.getSnowflakeId()); // 商户退款单号 + request.setNotify_url(WeChatPayParameter.refundNotifyUrl); // 回调接口 + WechatPayRefundRequest.Amount amount = new WechatPayRefundRequest.Amount(); + amount.setRefund(refundAmountInt); // 退款金额 + amount.setTotal(payerTotalInt); // 原订单金额 + request.setAmount(amount); + request.setReason("订单结算退款"); + request.setFunds_account("AVAILABLE"); + try { + return wechatPayService.ApplyForWechatPayRefundV3(request); + } catch (JsonProcessingException e) { + logger.error("调微信退款API发生异常", e); + } + return null; + } + + /** + * 余额退款处理逻辑 + * + * @param dto + */ + private WechatPayRefundResponse refundForBalance(WeChatRefundDTO dto) { + // 查会员余额 + MemberVO memberVO = memberBasicInfoService.queryMemberInfoByMemberId(dto.getMemberId()); + if (memberVO == null) { + throw new BusinessException(ReturnCodeEnum.CODE_SELECT_MEMBER_NULL_ERROR); + } + // 校验退款金额 + BigDecimal principalBalance = memberVO.getPrincipalBalance(); + BigDecimal refundAmount = dto.getRefundAmount(); + if (refundAmount.compareTo(principalBalance) > 0) { + throw new BusinessException(ReturnCodeEnum.CODE_REFUND_MEMBER_BALANCE_ERROR); + } + // 退款金额 元转分 123 + int totalCents = refundAmount.multiply(new BigDecimal(100)).intValue(); + // 查询最近一年余额充值订单 + List recordList = wxpayCallbackRecordService.queryBalanceRechargeRecordOfTheLatestYear(dto.getMemberId()); + // 也许需要多笔支付订单才够退款 + List requestList = Lists.newArrayList(); + WechatPayRefundRequest request; + for (WxpayCallbackRecord record : recordList) { + int payerTotal = Integer.parseInt(record.getPayerTotal()); // 该笔支付订单的支付金额,单位分 + totalCents = totalCents - payerTotal; // 123 - 100 + request = new WechatPayRefundRequest(); + request.setTransaction_id(record.getTransactionId()); // 微信支付单号 + request.setOut_trade_no(record.getOutTradeNo()); // 商户订单号 + request.setOut_refund_no(SnowflakeIdWorker.getSnowflakeId()); // 商户退款单号 + request.setNotify_url(WeChatPayParameter.refundNotifyUrl); // 回调接口 + request.setReason("用户余额退款"); + request.setFunds_account("AVAILABLE"); + if (totalCents > 0) { + // 如果大于0说明,这笔单退完也不够 + WechatPayRefundRequest.Amount amount = new WechatPayRefundRequest.Amount(); + amount.setRefund(payerTotal); // 退款金额 + amount.setTotal(payerTotal); // 原订单金额 + request.setAmount(amount); + requestList.add(request); + } else { + // 如果小于0,这笔单退一部分 + // 生成退款单号 + WechatPayRefundRequest.Amount amount = new WechatPayRefundRequest.Amount(); + // 部分退 + int i = payerTotal + totalCents; + amount.setRefund(i); // 退款金额 + amount.setTotal(payerTotal); // 原订单金额 + request.setAmount(amount); + requestList.add(request); + break; + } + } + // 调微信退款api + if (CollectionUtils.isNotEmpty(requestList)) { + for (WechatPayRefundRequest refundRequest : requestList) { + try { + return wechatPayService.ApplyForWechatPayRefundV3(refundRequest); + // logger.info("调微信退款API退款====={}", JSONObject.toJSONString(refundRequest)); + } catch (Exception e) { + logger.error("调微信退款API发生异常", e); + } + } + } + return null; + } + + /** + * 通过订单号查询订单详情 + * + * @param orderCode 订单号 + * @return 订单详情 + */ + @Override + public OrderDetail getOrderDetailByOrderCode(String orderCode) { + return orderBasicInfoMapper.getOrderDetailByOrderCode(orderCode); + } + + /** + * 通过会员Id和订单状态查询订单信息 + * + * @param memberId 会员id + * @param orderStatusList 订单状态集合 + * @return + */ + @Override + public List getListByMemberIdAndOrderStatus(String memberId, List orderStatusList) { + return orderBasicInfoMapper.getListByMemberIdAndOrderStatus(memberId, orderStatusList); + } + + /** + * 个人桩查询充电数据 + * @param dto + * @return + */ + @Override + public List getAccumulativeInfo(QueryPersonPileDTO dto) { + return orderBasicInfoMapper.getAccumulativeInfo(dto); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderPayRecordServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderPayRecordServiceImpl.java new file mode 100644 index 000000000..05bc9e0e7 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderPayRecordServiceImpl.java @@ -0,0 +1,58 @@ +package com.jsowell.pile.service.impl; + +import com.jsowell.pile.domain.OrderPayRecord; +import com.jsowell.pile.mapper.OrderPayRecordMapper; +import com.jsowell.pile.service.IOrderPayRecordService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +@Service +public class OrderPayRecordServiceImpl implements IOrderPayRecordService { + + @Resource + private OrderPayRecordMapper orderPayRecordMapper; + + + // @Override + // public int deleteByPrimaryKey(Integer id) { + // return orderPayRecordMapper.deleteByPrimaryKey(id); + // } + + // @Override + // public int insert(OrderPayRecord record) { + // return orderPayRecordMapper.insert(record); + // } + // + // @Override + // public int insertSelective(OrderPayRecord record) { + // return orderPayRecordMapper.insertSelective(record); + // } + + // @Override + // public OrderPayRecord selectByPrimaryKey(Integer id) { + // return orderPayRecordMapper.selectByPrimaryKey(id); + // } + + @Override + public int updateByPrimaryKeySelective(OrderPayRecord record) { + return orderPayRecordMapper.updateByPrimaryKeySelective(record); + } + + // @Override + // public int updateByPrimaryKey(OrderPayRecord record) { + // return orderPayRecordMapper.updateByPrimaryKey(record); + // } + + @Override + public int batchInsert(List payRecordList) { + return orderPayRecordMapper.batchInsert(payRecordList); + } + + @Override + public List getOrderPayRecordList(String orderCode) { + return orderPayRecordMapper.getOrderPayRecordList(orderCode); + } + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileBasicInfoServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileBasicInfoServiceImpl.java new file mode 100644 index 000000000..4e09b5dc5 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileBasicInfoServiceImpl.java @@ -0,0 +1,566 @@ +package com.jsowell.pile.service.impl; + +import com.alibaba.fastjson2.JSONObject; +import com.google.common.collect.Lists; +import com.jsowell.common.annotation.DataScope; +import com.jsowell.common.constant.CacheConstants; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.domain.ykc.RealTimeMonitorData; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.core.redis.RedisCache; +import com.jsowell.common.enums.ykc.PileConnectorDataBaseStatusEnum; +import com.jsowell.common.enums.ykc.PileConnectorStatusEnum; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.DateUtils; +import com.jsowell.common.util.PageUtils; +import com.jsowell.common.util.SecurityUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.pile.domain.PileBasicInfo; +import com.jsowell.pile.domain.PileConnectorInfo; +import com.jsowell.pile.domain.PileSimInfo; +import com.jsowell.pile.dto.IndexQueryDTO; +import com.jsowell.pile.dto.QueryPileDTO; +import com.jsowell.pile.dto.ReplaceMerchantStationDTO; +import com.jsowell.pile.mapper.PileBasicInfoMapper; +import com.jsowell.pile.mapper.PileMemberRelationMapper; +import com.jsowell.pile.service.*; +import com.jsowell.pile.vo.base.MerchantInfoVO; +import com.jsowell.pile.vo.base.PileInfoVO; +import com.jsowell.pile.vo.uniapp.PersonalPileInfoVO; +import com.jsowell.pile.vo.uniapp.PileConnectorDetailVO; +import com.jsowell.pile.vo.web.IndexGeneralSituationVO; +import com.jsowell.pile.vo.web.PileDetailVO; +import com.jsowell.pile.vo.web.PileModelInfoVO; +import com.jsowell.pile.vo.web.SimCardVO; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 设备管理Service业务层处理 + * + * @author jsowell + * @date 2022-08-26 + */ +@Slf4j +@Service +public class PileBasicInfoServiceImpl implements IPileBasicInfoService { + @Autowired + private PileBasicInfoMapper pileBasicInfoMapper; + + @Autowired + private IPileConnectorInfoService pileConnectorInfoService; + + @Autowired + private IPileModelInfoService pileModelInfoService; + + @Autowired + private IPileMerchantInfoService pileMerchantInfoService; + + @Autowired + private IPileSimInfoService pileSimInfoService; + + @Autowired + private SimCardService simCardService; + + @Autowired + private RedisCache redisCache; + + @Value("${qrcodeurl.prefix}") + private String QRCODE_URL_PREFIX; + + /** + * 查询设备管理 + * + * @param id 设备管理主键 + * @return 设备管理 + */ + @Override + public PileBasicInfo selectPileBasicInfoById(Long id) { + return pileBasicInfoMapper.selectPileBasicInfoById(id); + } + + @Override + public PileBasicInfo selectPileBasicInfoBySN(String pileSn) { + return pileBasicInfoMapper.selectPileBasicInfoBySn(pileSn); + } + + /** + * 查询设备管理列表 + * + * @param pileBasicInfo 设备管理 + * @return 设备管理 + */ + @Override + public List selectPileBasicInfoList(PileBasicInfo pileBasicInfo) { + return pileBasicInfoMapper.selectPileBasicInfoList(pileBasicInfo); + } + + /** + * 新增设备管理 + * + * @param pileBasicInfo 设备管理 + * @return 结果 + */ + @Override + public int insertPileBasicInfo(PileBasicInfo pileBasicInfo) { + pileBasicInfo.setCreateBy(SecurityUtils.getUsername()); + pileBasicInfo.setCreateTime(DateUtils.getNowDate()); + return pileBasicInfoMapper.insertPileBasicInfo(pileBasicInfo); + } + + /** + * 修改设备管理 + * + * @param pileBasicInfo 设备管理 + * @return 结果 + */ + @Override + public int updatePileBasicInfo(PileBasicInfo pileBasicInfo) { + // pileBasicInfo.setUpdateBy(SecurityUtils.getUsername()); + pileBasicInfo.setUpdateTime(DateUtils.getNowDate()); + return pileBasicInfoMapper.updatePileBasicInfo(pileBasicInfo); + } + + /** + * 批量删除设备管理 + * + * @param ids 需要删除的设备管理主键 + * @return 结果 + */ + @Override + public int deletePileBasicInfoByIds(Long[] ids) { + return pileBasicInfoMapper.deletePileBasicInfoByIds(ids); + } + + /** + * 删除设备管理信息 + * + * @param id 设备管理主键 + * @return 结果 + */ + @Override + public int deletePileBasicInfoById(Long id) { + return pileBasicInfoMapper.deletePileBasicInfoById(id); + } + + /** + * 查询充电桩信息 + * + * @param dto 前台参数 + * @return 充电桩信息集合 + */ + @Override + @DataScope(deptAlias = "t2") + public List queryPileInfos(QueryPileDTO dto) { + log.info("queryPileInfos dto:{}", JSONObject.toJSONString(dto)); + // 首先不分页查询所有符合条件的充电桩 + List pileInfoVOS = queryPileInfoList(dto); + // 获取桩sn列表 + List pileSnList = pileInfoVOS.stream().map(PileDetailVO::getPileSn).collect(Collectors.toList()); + // log.info("获取桩sn列表:{}", JSONObject.toJSONString(pileSnList)); + // 批量获取桩状态 key:桩编号; value:状态 + Map pileStatusMap = pileConnectorInfoService.getPileStatus(pileSnList); + // log.info("批量获取桩状态:{}", JSONObject.toJSONString(pileStatusMap)); + // 根据状态过滤 + List snList = Lists.newArrayList(); + if (StringUtils.isNotBlank(dto.getStatus())) { + for (Map.Entry entry : pileStatusMap.entrySet()) { + String pileSn = entry.getKey(); + String pileStatus = entry.getValue(); + if (StringUtils.equals(dto.getStatus(), pileStatus)) { + snList.add(pileSn); + } + } + } else { + // 不根据状态过滤,snList就等于pileSnList + snList = pileSnList; + // 根据状态排序 + + } + + if (CollectionUtils.isEmpty(snList)) { + return Lists.newArrayList(); + } + // 使用snList查询 分页 + if (dto.getPageNum() != 0) { + PageUtils.startPage(); + } + QueryPileDTO queryPileDTO = new QueryPileDTO(); + queryPileDTO.setPileSns(snList); + pileInfoVOS = queryPileInfoList(queryPileDTO); + + for (PileDetailVO pileInfoVO : pileInfoVOS) { + pileInfoVO.setStatus(pileStatusMap.get(pileInfoVO.getPileSn())); + } + return pileInfoVOS; + } + + @Override + // @DataScope(deptAlias = "t1") + public List queryPileInfoList(QueryPileDTO queryPileDTO) { + return pileBasicInfoMapper.queryPileInfos(queryPileDTO); + } + + /** + * 通过pileId更改运营商、站点信息 + * + * @param dto 前台参数 + * @return 结果 + */ + @Override + public int replaceMerchantStationByPileIds(ReplaceMerchantStationDTO dto) { + dto.setUpdateBy(SecurityUtils.getUsername()); + dto.setUpdateTime(DateUtils.getNowDate()); + int num = pileBasicInfoMapper.replaceMerchantStationByPileIds(dto); + // 修改枪口信息 + if (dto.getConnectorNum() != null && dto.getConnectorNum() > 0) { + // 先删后插 + pileConnectorInfoService.deletePileConnectorInfoByPileSnList(dto.getPileSnList()); + List connectorInfoList = Lists.newArrayList(); + for (String pileSn : dto.getPileSnList()) { + for (int i = 1; i < dto.getConnectorNum() + 1; i++) { + // 组装pile_connector_info表数据 + PileConnectorInfo connectorInfo = new PileConnectorInfo(); + connectorInfo.setPileSn(pileSn); // sn号 + String connectorCode = String.format("%1$02d", i); + connectorInfo.setPileConnectorCode(pileSn + connectorCode); // 枪口编号 + connectorInfo.setStatus(Constants.ZERO); //状态,默认 0-离网 + connectorInfo.setCreateBy(SecurityUtils.getUsername()); // 创建人 + connectorInfo.setDelFlag(Constants.DEL_FLAG_NORMAL); // 删除标识 + connectorInfoList.add(connectorInfo); + } + } + if (CollectionUtils.isNotEmpty(connectorInfoList)) { + pileConnectorInfoService.batchInsertConnectorInfo(connectorInfoList); + } + } + return num; + } + + /** + * 通过桩id查询basic信息 + * + * @param id 桩id + * @return 结果集合 + */ + @Override + public PileDetailVO selectBasicInfoById(Long id) { + PileDetailVO pileInfoVO = pileBasicInfoMapper.selectBasicInfoById(id); + if (pileInfoVO == null) { + return null; + } + // 获取桩状态 + Map pileStatusMap = pileConnectorInfoService.getPileStatus(Lists.newArrayList(pileInfoVO.getPileSn())); + pileInfoVO.setStatus(pileStatusMap.get(pileInfoVO.getPileSn())); + // String url = "http://localhost:8080/uniapp/pile/pileDetail/"; + String pileQrCodeUrl = getPileQrCodeUrl(pileInfoVO.getPileSn()); + pileInfoVO.setQrCodeURL(pileQrCodeUrl); + // 额定功率 瓦改为千瓦(2023.2.8发现数据库中型号表已经存的单位是 kw,因此注释掉) + // pileInfoVO.setRatedPower(pileInfoVO.getRatedPower() / 1000); + return pileInfoVO; + } + + @Override + public PileInfoVO selectPileInfoBySn(String pileSn) { + PileBasicInfo basicInfo = pileBasicInfoMapper.selectPileBasicInfoBySn(pileSn); + if (basicInfo == null) { + return null; + } + + PileInfoVO pileInfoVO = PileInfoVO.builder() + .pileId(String.valueOf(basicInfo.getId())) + .pileSn(basicInfo.getSn()) + .stationId(String.valueOf(basicInfo.getStationId())) + .merchantId(String.valueOf(basicInfo.getMerchantId())) + .build(); + // 查站点信息 + MerchantInfoVO merchantInfo = pileMerchantInfoService.getMerchantInfo(String.valueOf(basicInfo.getMerchantId())); + if (merchantInfo != null) { + pileInfoVO.setMerchantName(merchantInfo.getMerchantName()); + } + + // 查询充电桩额定功率 + PileModelInfoVO pileModelInfoVO = pileModelInfoService.getPileModelInfoByPileSn(pileSn); + if (pileModelInfoVO != null) { + pileInfoVO.setRatedPower(pileModelInfoVO.getRatedPower()); + pileInfoVO.setRatedCurrent(pileModelInfoVO.getRatedCurrent()); + pileInfoVO.setRatedVoltage(pileModelInfoVO.getRatedVoltage()); + } + return pileInfoVO; + } + + @Override + public List selectPileListByStationIds(List stationIdList) { + // 加缓存 + List pileInfoVOS = pileBasicInfoMapper.selectPileListByStationIds(stationIdList); + if (CollectionUtils.isEmpty(pileInfoVOS)) { + return Lists.newArrayList(); + } + List pileSnList = pileInfoVOS.stream().map(PileDetailVO::getPileSn).collect(Collectors.toList()); + Map pileStatusMap = pileConnectorInfoService.getPileStatus(pileSnList); + for (PileDetailVO pileInfoVO : pileInfoVOS) { + pileInfoVO.setStatus(pileStatusMap.get(pileInfoVO.getPileSn())); + } + return pileInfoVOS; + } + + /** + * 通过桩sn号查询站点id + * + * @param sn 桩sn号 + * @return 站点id + */ + // @Override + // public String selectStationIdBySn(String sn) { + // return pileBasicInfoMapper.selectStationIdBySn(sn); + // } + + /** + * uniApp通过桩号查询桩详情 + * + * @param pileSn 桩号 + * @return + */ + // @Override + // public PileDetailVO uniAppGetPileDetailByPileSn(String pileSn) { + // PileDetailVO pileDetailVO = pileBasicInfoMapper.uniAppGetPileDetailByPileSn(pileSn); + // // 查询计费模板 + // BillingTemplateVO billingTemplateVO = pileBillingTemplateService.selectBillingTemplateDetailByPileSn(pileSn); + // if (Objects.nonNull(billingTemplateVO)) { + // pileDetailVO.setBillingTemplate(billingTemplateVO); + // + // } + // // 查询枪口相关信息 + // List connectorList = new ArrayList<>(); + // + // return pileDetailVO; + // } + + /** + * 修改状态 + * 用于登陆协议,心跳包协议,上传实时数据协议 更新状态的方法 + * 数据库状态 + * 充电枪:0-离线;1-空闲;2-占用(未充电);3-占用(充电中);4-占用(预约锁定);255-故障 + *

+ * 充电枪口状态在以下情况会改变 + * 1.登陆成功后,设置为【1-空闲】 + * 2.心跳包传0x01故障,设置为【255-故障】 + * 3.上传实时数据,枪状态传0x00离线,设置为【0-离线】 + * 4.上传实时数据,枪状态传0x01故障,设置为【255-故障】 + * 5.上传实时数据,枪状态传0x02空闲,并且为插枪状态,设置为【2-占用(未充电)】 + * 6.上传实时数据,枪状态传0x02空闲,并且为未插枪状态,设置为【1-空闲】 + * 7.上传实时数据,枪状态传0x03充电,设置为【3-占用(充电中)】 + */ + @Override + public void updateStatus(String frameType, String pileSn, String connectorCode, String status, String putGunType) { + // log.info("updateStatus传参:帧类型:{}, 桩编号:{}, 枪口号:{}, 状态:{}, 插拔枪:{}", "0x" + frameType, pileSn, connectorCode, status, putGunType); + /* + 0x01 登陆认证 + 登陆成功,把充电桩和枪口的状态都更新为【在线】 + */ + if (StringUtils.equals(frameType, String.valueOf(YKCFrameTypeCode.LOGIN_CODE.getCode()))) { + // 枪口状态设置为【空闲】 + pileConnectorInfoService.updateConnectorStatusByPileSn(pileSn, PileConnectorDataBaseStatusEnum.FREE.getValue()); + } + + /* + 0x03 心跳包 枪口状态 0-正常 1-故障 + 心跳包会传过来枪口的状态,由枪口状态推导出来充电桩的状态。任意一个枪口状态为故障,则充电桩状态也是故障 + */ + if (StringUtils.equals(frameType, BytesUtil.bcd2Str(YKCFrameTypeCode.HEART_BEAT_CODE.getBytes()))) { + // log.info("心跳包的修改状态逻辑 桩号:{}, 枪号:{}, 枪状态:{}", pileSn, connectorCode, status); + + /* + 更新充电桩接口的数据库状态 + 传入的状态为【1-故障】,就把枪口数据库改为故障 + */ + if (StringUtils.equals(status, Constants.ONE)) { + // 故障 + pileConnectorInfoService.updateConnectorStatus(pileSn + connectorCode, PileConnectorDataBaseStatusEnum.FAULT.getValue()); + } else { + // 正常 + // List connectorInfoList = pileConnectorInfoService.selectPileConnectorInfoList(pileSn); + // for (PileConnectorInfo connectorInfo : connectorInfoList) { + // // 收到心跳包,只有在离线或故障时,修改为空闲状态 + // if (StringUtils.equals(connectorInfo.getStatus(), PileConnectorDataBaseStatusEnum.OFF_NETWORK.getValue()) + // || StringUtils.equals(connectorInfo.getStatus(), PileConnectorDataBaseStatusEnum.FAULT.getValue())) { + // pileConnectorInfoService.updateConnectorStatus(pileSn + connectorCode, PileConnectorDataBaseStatusEnum.FREE.getValue()); + // } + // } + } + } + + /* + 0x13 上传实时数据 枪口状态 0x00:离线 0x01:故障 0x02:空闲 0x03:充电 + 根据传来的枪状态和是否插枪,判断状态 + */ + if (StringUtils.equals(frameType, BytesUtil.bcd2Str(YKCFrameTypeCode.UPLOAD_REAL_TIME_MONITOR_DATA_CODE.getBytes())) + || StringUtils.equals(frameType, BytesUtil.bcd2Str(YKCFrameTypeCode.UPLOAD_REAL_TIME_MONITOR_DATA_OLD_VERSION_CODE.getBytes()))) { + // log.info("上传实时数据中的修改状态逻辑 桩号:{}, 枪号:{}, 枪状态{}, 是否插枪:{}", pileSn, connectorCode, status, putGunType); + /** + * 更新枪状态 + * connectorStatus 桩传过来的枪口状态: 0x00:离线 0x01:故障 0x02:空闲 0x03:充电 + * 数据库状态:0:离网 (默认);1:空闲;2:占用(未充电);3:占用(充电中);4:占用(预约锁定) ;255:故障 + */ + String statusDataBase = ""; // 传给数据库的状态 + if (StringUtils.equals(status, PileConnectorStatusEnum.OFF_NETWORK.getValue())) { // 离线 + statusDataBase = PileConnectorDataBaseStatusEnum.OFF_NETWORK.getValue(); + } else if (StringUtils.equals(status, PileConnectorStatusEnum.FAULT.getValue())) { // 故障 + statusDataBase = PileConnectorDataBaseStatusEnum.FAULT.getValue(); + } else if (StringUtils.equals(status, PileConnectorStatusEnum.FREE.getValue())) { // 空闲 + //是否插枪 0x00:否 0x01:是 + if (StringUtils.equals(putGunType, Constants.ZERO_ONE)) { + // 空闲并插枪 设置为【占用(未充电)】 + statusDataBase = PileConnectorDataBaseStatusEnum.OCCUPIED_NOT_CHARGED.getValue(); + } else { + // 空闲没插枪 设置为【空闲】 + statusDataBase = PileConnectorDataBaseStatusEnum.FREE.getValue(); + } + } else if (StringUtils.equals(status, PileConnectorStatusEnum.OCCUPIED_CHARGING.getValue())) { // 充电 + // 设置为【占用(充电中)】 + statusDataBase = PileConnectorDataBaseStatusEnum.OCCUPIED_CHARGING.getValue(); + } + pileConnectorInfoService.updateConnectorStatus(pileSn + connectorCode, statusDataBase); + } + } + + /** + * 充电桩正在充电时的实时数据存到redis + * @param realTimeMonitorData 实时数据 + */ + @Override + public void saveRealTimeMonitorData2Redis(RealTimeMonitorData realTimeMonitorData) { + // 保存到redis + String redisKey = CacheConstants.PILE_REAL_TIME_MONITOR_DATA + realTimeMonitorData.getPileConnectorCode() + "_" + realTimeMonitorData.getOrderCode(); + + // 设置接收到实时数据的时间 + Date now = new Date(); + realTimeMonitorData.setDateTime(DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, now)); + // 计算功率,后面查询要用 + String power = new BigDecimal(realTimeMonitorData.getOutputVoltage()) + .multiply(new BigDecimal(realTimeMonitorData.getOutputCurrent())).setScale(2, BigDecimal.ROUND_HALF_UP).toString(); + realTimeMonitorData.setOutputPower(power); + // 保存json字符串 + String jsonMsg = JSONObject.toJSONString(realTimeMonitorData); + // 上传实时数据每10秒发送一次,1分钟6次,在同一分钟内,只保留最后一条实时数据 + redisCache.hset(redisKey, DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:00", now), jsonMsg); + } + + @Override + public PileConnectorDetailVO queryPileConnectorDetail(String pileConnectorCode) { + return pileBasicInfoMapper.queryPileConnectorDetail(pileConnectorCode); + } + + /** + * 返回充电桩二维码 + * @param pileSn 如充电桩编号为空,则返回前缀 https://api.jsowellcloud.com/app-xcx-h5/pile/pileDetail/ + * @return + */ + @Override + public String getPileQrCodeUrl(String pileSn) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(QRCODE_URL_PREFIX); + stringBuilder.append("/app-xcx-h5/pile/pileDetail/"); + if (StringUtils.isNotEmpty(pileSn)) { + if (!stringBuilder.toString().endsWith("/")) { + stringBuilder.append("/"); + } + stringBuilder.append(pileSn); + } + return stringBuilder.toString(); + } + + @Override + public void updatePileSimInfo(String pileSn, String iccid) { + PileBasicInfo basicInfo = selectPileBasicInfoBySN(pileSn); + if (basicInfo == null) { + return; + } + // 通过iccid查pile_sim_info + PileSimInfo simInfo = pileSimInfoService.getBasicInfoByIccId(iccid); + if (simInfo == null) { + SimCardVO simCardVO = simCardService.searchByLoop(iccid); + // pile_sim_info 新增数据 + if (simCardVO != null) { + simInfo = PileSimInfo.builder() + .iccid(iccid) + .name(simCardVO.getName()) + .simSupplier(simCardVO.getSimCardFactory()) + .expireTime(DateUtils.parseDate(simCardVO.getExpiredTime())) + .operator(simCardVO.getSimCardOperator()) + .status(simCardVO.getSimCardStatus()) + .surplusData(String.valueOf(simCardVO.getResidualFlowRate())) + .totalData(String.valueOf(simCardVO.getPackageCapacity())) + .build(); + }else { + // 卡商未查到信息,记录为新卡,只存卡号 + simInfo = PileSimInfo.builder() + .iccid(iccid) + .build(); + } + pileSimInfoService.insertPileSimInfo(simInfo); + // 拿到id + Long simId = simInfo.getId(); + log.info("新保存的sim卡id:{}", simId); + // sim_id 更新到pile_basic_info + basicInfo.setSimId(simId); + } else { + // sim信息已经存库了 然后更新充电桩的sim卡 + basicInfo.setSimId(simInfo.getId()); + } + updatePileBasicInfo(basicInfo); + } + + /** + * 后管首页基本信息查询 + * + * @param dto 站点Id + * @return 首页基本信息 + */ + @Override + public IndexGeneralSituationVO getGeneralSituation(IndexQueryDTO dto) { + // + return pileBasicInfoMapper.getGeneralSituation(dto); + } + + /** + * 通过会员id查询个人桩列表 + * @param memberId + * @return + */ + @Override + public List getPileInfoByMemberId(String memberId) { + List list = pileBasicInfoMapper.getPileInfoByMemberId(memberId); + if(CollectionUtils.isEmpty(list)){ + return null; + } + + for (PersonalPileInfoVO personalPileInfoVO : list) { + String pileSn = personalPileInfoVO.getPileSn(); + + // 获取桩状态 + Map pileStatus = pileConnectorInfoService.getPileStatus(Lists.newArrayList(pileSn)); + personalPileInfoVO.setPileStatus(pileStatus.get(pileSn)); + if (StringUtils.equals("1", personalPileInfoVO.getType())) { + personalPileInfoVO.setType("管理员用户"); + }else { + personalPileInfoVO.setType("普通用户"); + } + if (StringUtils.equals("1", personalPileInfoVO.getSpeedType())) { + personalPileInfoVO.setSpeedType("快充"); + }else { + personalPileInfoVO.setSpeedType("慢充"); + } + } + return list; + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileBillingTemplateServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileBillingTemplateServiceImpl.java new file mode 100644 index 000000000..f09382280 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileBillingTemplateServiceImpl.java @@ -0,0 +1,619 @@ +package com.jsowell.pile.service.impl; + +import com.alibaba.fastjson2.JSONObject; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.primitives.Bytes; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.enums.DelFlagEnum; +import com.jsowell.common.enums.ykc.BillingTimeEnum; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.DateUtils; +import com.jsowell.common.util.SecurityUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.common.util.id.Seq; +import com.jsowell.pile.domain.PileBillingDetail; +import com.jsowell.pile.domain.PileBillingRelation; +import com.jsowell.pile.domain.PileBillingTemplate; +import com.jsowell.pile.dto.BillingTimeDTO; +import com.jsowell.pile.dto.CreateOrUpdateBillingTemplateDTO; +import com.jsowell.pile.dto.ImportBillingTemplateDTO; +import com.jsowell.pile.mapper.PileBillingTemplateMapper; +import com.jsowell.pile.service.IPileBasicInfoService; +import com.jsowell.pile.service.IPileBillingTemplateService; +import com.jsowell.pile.transaction.dto.BillingTemplateTransactionDTO; +import com.jsowell.pile.transaction.service.TransactionService; +import com.jsowell.pile.vo.uniapp.BillingPriceVO; +import com.jsowell.pile.vo.uniapp.CurrentTimePriceDetails; +import com.jsowell.pile.vo.web.BillingDetailVO; +import com.jsowell.pile.vo.web.BillingTemplateVO; +import com.jsowell.pile.vo.web.EchoBillingTemplateVO; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * 计费模板Service业务层处理 + * + * @author jsowell + * @date 2022-09-20 + */ +@Slf4j +@Service +public class PileBillingTemplateServiceImpl implements IPileBillingTemplateService { + @Autowired + private PileBillingTemplateMapper pileBillingTemplateMapper; + + @Autowired + private TransactionService transactionService; + + @Autowired + private IPileBasicInfoService pileBasicInfoService; + + /** + * 查询计费模板 + * + * @param id 计费模板主键 + * @return 计费模板 + */ + @Override + public PileBillingTemplate selectPileBillingTemplateById(Long id) { + return pileBillingTemplateMapper.selectPileBillingTemplateById(id); + } + + /** + * 查询计费模板列表 + * + * @param pileBillingTemplate 计费模板 + * @return 计费模板 + */ + @Override + public List selectPileBillingTemplateList(PileBillingTemplate pileBillingTemplate) { + return pileBillingTemplateMapper.selectPileBillingTemplateList(pileBillingTemplate); + } + + /** + * 新增计费模板 + * + * @param pileBillingTemplate 计费模板 + * @return 结果 + */ + @Transactional + @Override + public int insertPileBillingTemplate(PileBillingTemplate pileBillingTemplate) { + pileBillingTemplate.setCreateTime(DateUtils.getNowDate()); + int rows = pileBillingTemplateMapper.insertPileBillingTemplate(pileBillingTemplate); + insertPileBillingDetail(pileBillingTemplate); + return rows; + } + + /** + * 修改计费模板 + * + * @param pileBillingTemplate 计费模板 + * @return 结果 + */ + @Transactional + @Override + public int updatePileBillingTemplate(PileBillingTemplate pileBillingTemplate) { + return pileBillingTemplateMapper.updatePileBillingTemplate(pileBillingTemplate); + } + + /** + * 批量删除计费模板 + * + * @param ids 需要删除的计费模板主键 + * @return 结果 + */ + @Transactional + @Override + public int deletePileBillingTemplateByIds(Long[] ids) { + // 查询计费模板详情列表 + List pileBillingDetails = pileBillingTemplateMapper.queryBillingDetailByTemplateIds(ids); + if (CollectionUtils.isNotEmpty(pileBillingDetails)) { + List templateCodes = pileBillingDetails.stream().map(PileBillingDetail::getTemplateCode).collect(Collectors.toList()); + pileBillingTemplateMapper.deletePileBillingDetailByTemplateCodes(templateCodes); + } + return pileBillingTemplateMapper.deletePileBillingTemplateByIds(ids); + } + + /** + * 删除计费模板信息 + * + * @param id 计费模板主键 + * @return 结果 + */ + @Transactional + @Override + public int deletePileBillingTemplateById(Long id) { + // pileBillingTemplateMapper.deletePileBillingDetailByTemplateCode(id); + return pileBillingTemplateMapper.deletePileBillingTemplateById(id); + } + + @Override + public void createBillingTemplate(CreateOrUpdateBillingTemplateDTO dto) { + // 生成计费模板编号 + String templateCode = Seq.getId(); + // 组装入库参数 + PileBillingTemplate billingTemplate = new PileBillingTemplate(); + billingTemplate.setName(dto.getName()); + billingTemplate.setType(String.valueOf(dto.getType())); + billingTemplate.setRemark(dto.getRemark()); + billingTemplate.setTemplateCode(templateCode); + billingTemplate.setCreateBy(SecurityUtils.getUsername()); + billingTemplate.setDelFlag(DelFlagEnum.normal.getValue()); + if (StringUtils.isBlank(dto.getStationId())) { + // 公共模板标识(0-私有;1-公共) + billingTemplate.setPublicFlag(Constants.ONE); + } else { + billingTemplate.setPublicFlag(Constants.ZERO); + } + + // key为时间类型,value为时间描述 + Map> map = dto.getTimeArray().stream() + .collect(Collectors.groupingBy(BillingTimeDTO::getType)); + + // 计费模板详情 + List detailList = Lists.newArrayList(); + PileBillingDetail detail = null; + for (BillingTimeEnum billingTimeEnum : BillingTimeEnum.values()) { + detail = new PileBillingDetail(); + detail.setTemplateCode(templateCode); + String type = billingTimeEnum.getValue(); // 时段类型 + detail.setTimeType(type); + Map priceMap = getPriceMap(dto, type); + detail.setElectricityPrice(priceMap.get("electricityPrice")); + detail.setServicePrice(priceMap.get("servicePrice")); + List entryValue = map.get(type); + String applyTime = ""; + if (CollectionUtils.isNotEmpty(entryValue)) { + applyTime = entryValue.stream() + .map(x -> x.getStartTime() + "-" + x.getEndTime()) + .collect(Collectors.joining(",")); + } + detail.setApplyTime(applyTime); + detail.setCreateBy(SecurityUtils.getUsername()); + detailList.add(detail); + } + + BillingTemplateTransactionDTO build = BillingTemplateTransactionDTO.builder() + .billingTemplate(billingTemplate) + .detailList(detailList) + .build(); + // 入库 + transactionService.doCreateBillingTemplate(build); + } + + @Override + public void updateBillingTemplate(CreateOrUpdateBillingTemplateDTO dto) { + PileBillingTemplate billingTemplate = selectPileBillingTemplateById(Long.valueOf(dto.getBillingTemplateId())); + if (billingTemplate == null) { + log.info("根据计费模板id:{},查询为null。无法修改直接返回", dto.getBillingTemplateId()); + return; + } + billingTemplate.setName(dto.getName()); + billingTemplate.setType(String.valueOf(dto.getType())); + billingTemplate.setRemark(dto.getRemark()); + billingTemplate.setUpdateBy(SecurityUtils.getUsername()); + + // key为时间类型,value为时间描述 + Map> map = dto.getTimeArray().stream() + .collect(Collectors.groupingBy(BillingTimeDTO::getType)); + + // 计费模板详情 + List detailList = Lists.newArrayList(); + PileBillingDetail detail = null; + for (BillingTimeEnum billingTimeEnum : BillingTimeEnum.values()) { + detail = new PileBillingDetail(); + detail.setTemplateCode(billingTemplate.getTemplateCode()); + String type = billingTimeEnum.getValue(); // 时段类型 + detail.setTimeType(type); + Map priceMap = getPriceMap(dto, type); + detail.setElectricityPrice(priceMap.get("electricityPrice")); + detail.setServicePrice(priceMap.get("servicePrice")); + List entryValue = map.get(type); + String applyTime = ""; + if (CollectionUtils.isNotEmpty(entryValue)) { + applyTime = entryValue.stream() + .map(x -> x.getStartTime() + "-" + x.getEndTime()) + .collect(Collectors.joining(",")); + } + detail.setApplyTime(applyTime); + detail.setCreateBy(SecurityUtils.getUsername()); + detailList.add(detail); + } + + BillingTemplateTransactionDTO build = BillingTemplateTransactionDTO.builder() + .billingTemplate(billingTemplate) + .detailList(detailList) + .build(); + // 入库 + transactionService.doUpdateBillingTemplate(build); + } + + @Override + public EchoBillingTemplateVO queryPileBillingTemplateById(Long id) { + PileBillingTemplate pileBillingTemplate = selectPileBillingTemplateById(id); + if (pileBillingTemplate == null) { + return null; + } + EchoBillingTemplateVO result = new EchoBillingTemplateVO(); + result.setBillingTemplateId(String.valueOf(id)); + result.setName(pileBillingTemplate.getName()); + result.setRemark(pileBillingTemplate.getRemark()); + result.setType(pileBillingTemplate.getType()); + + // 查计费模板详情 + List detailList = pileBillingTemplateMapper.queryBillingDetailByTemplateIds(new Long[]{id}); + if (CollectionUtils.isNotEmpty(detailList)) { + List timeArray = Lists.newArrayList(); + // 取出4个时间段类型 + for (PileBillingDetail billingDetail : detailList) { + String type = billingDetail.getTimeType(); + BigDecimal electricityPrice = billingDetail.getElectricityPrice(); + BigDecimal servicePrice = billingDetail.getServicePrice(); + if (StringUtils.equals(type, BillingTimeEnum.SHARP.getValue())) { + result.setElectricityPriceA(electricityPrice); + result.setServicePriceA(servicePrice); + } else if (StringUtils.equals(type, BillingTimeEnum.PEAK.getValue())) { + result.setElectricityPriceB(electricityPrice); + result.setServicePriceB(servicePrice); + } else if (StringUtils.equals(type, BillingTimeEnum.FLAT.getValue())) { + result.setElectricityPriceC(electricityPrice); + result.setServicePriceC(servicePrice); + } else if (StringUtils.equals(type, BillingTimeEnum.VALLEY.getValue())) { + result.setElectricityPriceD(electricityPrice); + result.setServicePriceD(servicePrice); + } + // 逗号拼接的时间段 00:00-01:30,01:30-04:00,23:30-24:00 + String applyTime = billingDetail.getApplyTime(); + if (StringUtils.isBlank(applyTime)) { + continue; + } + List list = Lists.newArrayList(applyTime.split(",")); + BillingTimeDTO timeDTO; + for (String s : list) { + String[] split = s.split("-"); + timeDTO = new BillingTimeDTO(); + timeDTO.setType(type); + timeDTO.setStartTime(split[0]); + timeDTO.setEndTime(split[1]); + timeArray.add(timeDTO); + } + } + // timeArray重新排序 + result.setTimeArray(timeArray.stream().sorted(Comparator.comparing(BillingTimeDTO::getStartTime)).collect(Collectors.toList())); + } + return result; + } + + /** + * 通过站点id查询当前时间的收费详情 + * + * @param stationId 站点id + */ + @Override + public CurrentTimePriceDetails getCurrentTimePriceDetails(String stationId) { + CurrentTimePriceDetails result = null; + // 查询当前时段电费 + LocalTime localTime = LocalTime.now(); + String now = LocalTime.of(localTime.getHour(), localTime.getMinute(), localTime.getSecond()).toString(); + // 通过站点id查询计费模板 + BillingTemplateVO billingTemplateVO = pileBillingTemplateMapper.selectBillingTemplateByStationId(stationId); + if (Objects.nonNull(billingTemplateVO)) { + result = new CurrentTimePriceDetails(); + result.setTemplateCode(billingTemplateVO.getTemplateCode()); + result.setStationId(stationId); + result.setDateTime(localTime.toString()); + + List billingDetailList = billingTemplateVO.getBillingDetailList(); + for (BillingDetailVO detailVO : billingDetailList) { + List applyTimeList = detailVO.getApplyTime(); + for (String applyTime : applyTimeList) { + boolean b = DateUtils.checkTime(now + "-" + now, applyTime); + if (b) { + // 将桩的费率存入stationVO + BigDecimal electricityPrice = detailVO.getElectricityPrice(); + BigDecimal servicePrice = detailVO.getServicePrice(); + + result.setElectricityPrice(electricityPrice.toString()); + result.setServicePrice(servicePrice.toString()); + result.setTotalPrice(electricityPrice.add(servicePrice).toString()); + } + } + } + } + return result; + } + + @Override + public List queryPublicBillingTemplateList() { + return pileBillingTemplateMapper.queryPublicBillingTemplateList(); + } + + @Override + public List queryStationBillingTemplateList(String stationId) { + if (StringUtils.isBlank(stationId)) { + return Lists.newArrayList(); + } + return pileBillingTemplateMapper.queryStationBillingTemplateList(stationId); + } + + /** + * 查询正在使用中的计费模板 + * 1 发布时间不为null + * 2 最近发布的为正在使用中的 + * @param stationId 站点id + * @return + */ + @Override + public BillingTemplateVO queryUsedBillingTemplate(String stationId) { + List list = queryStationBillingTemplateList(stationId); + Optional max = list.stream() + .filter(x -> StringUtils.isNotBlank(x.getPublishTime())) + .max(Comparator.comparing(BillingTemplateVO::getPublishTime)); + return max.orElse(null); + } + + @Override + public List queryBillingPrice(String stationId) { + BillingTemplateVO billingTemplateVO = queryUsedBillingTemplate(stationId); + if (billingTemplateVO == null) { + return Lists.newArrayList(); + } + EchoBillingTemplateVO echoBillingTemplateVO = queryPileBillingTemplateById(Long.parseLong(billingTemplateVO.getTemplateId())); + List timeArray = echoBillingTemplateVO.getTimeArray(); + log.info("计费模板时段:{}", JSONObject.toJSONString(timeArray)); + + List resultList = Lists.newArrayList(); + for (BillingTimeDTO billingTimeDTO : timeArray) { + BillingPriceVO vo = new BillingPriceVO(); + String type = billingTimeDTO.getType(); + if (StringUtils.equals(type, BillingTimeEnum.SHARP.getValue())) { + vo.setElectricityPrice(billingTemplateVO.getSharpElectricityPrice().toString()); + vo.setServicePrice(billingTemplateVO.getSharpServicePrice().toString()); + } else if (StringUtils.equals(type, BillingTimeEnum.PEAK.getValue())) { + vo.setElectricityPrice(billingTemplateVO.getPeakElectricityPrice().toString()); + vo.setServicePrice(billingTemplateVO.getPeakServicePrice().toString()); + } else if (StringUtils.equals(type, BillingTimeEnum.FLAT.getValue())) { + vo.setElectricityPrice(billingTemplateVO.getFlatElectricityPrice().toString()); + vo.setServicePrice(billingTemplateVO.getFlatServicePrice().toString()); + } else if (StringUtils.equals(type, BillingTimeEnum.VALLEY.getValue())) { + vo.setElectricityPrice(billingTemplateVO.getValleyElectricityPrice().toString()); + vo.setServicePrice(billingTemplateVO.getValleyServicePrice().toString()); + } + // 总费用 + vo.setTotalPrice(new BigDecimal(vo.getElectricityPrice()).add(new BigDecimal(vo.getServicePrice())).toString()); + // 开始时间 + vo.setStartTime(billingTimeDTO.getStartTime()); + // 结束时间 + String endTime = StringUtils.equals("24:00", billingTimeDTO.getEndTime()) ? "23:59" : billingTimeDTO.getEndTime(); + vo.setEndTime(endTime); + // 时段类型 + vo.setTimeType(type); + // 是否当前时间 + boolean in = DateUtils.isIn(LocalTime.now(), LocalTime.parse(vo.getStartTime()), LocalTime.parse(vo.getEndTime())); + vo.setIsCurrentTime(in ? Constants.ONE : Constants.ZERO); + resultList.add(vo); + } + return resultList; + } + + public static void main(String[] args) { + // 21:30-23:00 + String startTime = "01:30"; + String endTime = "23:00"; + boolean in = DateUtils.isIn(LocalTime.now(), LocalTime.parse(startTime), LocalTime.parse(endTime)); + System.out.println(in); + + } + + /** + * 通过桩sn号查询计费模板 + * + * @param pileSn 桩sn + * @return 计费模板编号 + */ + @Override + public BillingTemplateVO selectBillingTemplateDetailByPileSn(String pileSn) { + return pileBillingTemplateMapper.selectBillingTemplateByPileSn(pileSn); + } + + @Override + public boolean stationImportBillingTemplate(ImportBillingTemplateDTO dto) { + // 查询公共计费模板是否存在 + PileBillingTemplate pileBillingTemplate = pileBillingTemplateMapper.selectPileBillingTemplateById(Long.valueOf(dto.getBillingTemplateId())); + if (pileBillingTemplate == null) { + log.warn("根据计费模板id:{},查询为空", dto.getBillingTemplateId()); + return false; + } + List billingDetailList = pileBillingTemplate.getPileBillingDetailList(); + // 复制计费模板 + PileBillingTemplate stationBillingTemplate = new PileBillingTemplate(); + + String templateCode = Seq.getId(); // 生成计费模板编号 + BeanUtils.copyProperties(pileBillingTemplate, stationBillingTemplate, "id", "createBy", "createTime", "updateBy", "updateTime"); + stationBillingTemplate.setStationId(Long.valueOf(dto.getStationId())); // 站点id + stationBillingTemplate.setPublicFlag(Constants.ZERO); // 站点私有计费模板 + stationBillingTemplate.setTemplateCode(templateCode); + stationBillingTemplate.setCreateBy(SecurityUtils.getUsername()); + stationBillingTemplate.setDelFlag(DelFlagEnum.normal.getValue()); + // 复制计费模板详情 + List stationBillingDetailList = Lists.newArrayList(); + PileBillingDetail pileBillingDetail; + for (PileBillingDetail detail : billingDetailList) { + pileBillingDetail = new PileBillingDetail(); + BeanUtils.copyProperties(detail, pileBillingDetail, "id", "templateCode", "createBy", "createTime", "updateBy", "updateTime"); + pileBillingDetail.setTemplateCode(templateCode); + pileBillingDetail.setCreateBy(SecurityUtils.getUsername()); + pileBillingDetail.setDelFlag(DelFlagEnum.normal.getValue()); + stationBillingDetailList.add(pileBillingDetail); + } + // 入库 + BillingTemplateTransactionDTO billingTemplateTransactionDTO = new BillingTemplateTransactionDTO(); + billingTemplateTransactionDTO.setBillingTemplate(stationBillingTemplate); + billingTemplateTransactionDTO.setDetailList(stationBillingDetailList); + transactionService.doCreateBillingTemplate(billingTemplateTransactionDTO); + return true; + } + + @Override + public byte[] generateBillingTemplateMsgBody(String pileSn, BillingTemplateVO billingTemplateVO) { + if (StringUtils.isBlank(pileSn) || billingTemplateVO == null) { + log.error("生成发给充电桩的计费模板msgBody 参数不能为空"); + return null; + } + + // 桩编码 + byte[] pileSnByte = BytesUtil.str2Bcd(pileSn); + + // 计费模型编号 固定值: 01 00 + byte[] billingTemplateCode = new byte[]{0x01, 0x00}; + + // 尖时段电费 + byte[] sharpElectricityPrice = YKCUtils.getPriceByte(billingTemplateVO.getSharpElectricityPrice().toString(), 5); + // 尖时段服务费 + byte[] sharpServicePrice = YKCUtils.getPriceByte(billingTemplateVO.getSharpServicePrice().toString(), 5); + + // 峰时段电费 + byte[] peakElectricityPrice = YKCUtils.getPriceByte(billingTemplateVO.getPeakElectricityPrice().toString(), 5); + // 峰时段服务费 + byte[] peakServicePrice = YKCUtils.getPriceByte(billingTemplateVO.getPeakServicePrice().toString(), 5); + + // 平时段电费 + byte[] flatElectricityPrice = YKCUtils.getPriceByte(billingTemplateVO.getFlatElectricityPrice().toString(), 5); + // 平时段服务费 + byte[] flatServicePrice = YKCUtils.getPriceByte(billingTemplateVO.getFlatServicePrice().toString(), 5); + + // 谷时段电费 + byte[] valleyElectricityPrice = YKCUtils.getPriceByte(billingTemplateVO.getValleyElectricityPrice().toString(), 5); + // 谷时段服务费 + byte[] valleyServicePrice = YKCUtils.getPriceByte(billingTemplateVO.getValleyServicePrice().toString(), 5); + // 计损比例(目前置0) + byte[] PlanLossRatio = Constants.zeroByteArray; + + // 48个时段费率(半小时为一个时段) + Map> timeMap = billingTemplateVO.getTimeMap(); + List periodOfTime = DateUtils.getPeriodOfTime(); + byte[] timeArray = new byte[periodOfTime.size()]; + for (int i = 0; i < periodOfTime.size(); i++) { + // 对比时间段 + byte timeType = getTimeType(periodOfTime.get(i), timeMap); + // log.info("时间段:{}, 对应费率类型:{}", periodOfTime.get(i), timeType); + timeArray[i] = timeType; + } + + // 消息体 + byte[] msgBody = Bytes.concat(pileSnByte, billingTemplateCode, sharpElectricityPrice, sharpServicePrice, + peakElectricityPrice, peakServicePrice, flatElectricityPrice, flatServicePrice, valleyElectricityPrice, + valleyServicePrice, PlanLossRatio, timeArray); + log.warn("generateBillingTemplateMsgBody:{}", BytesUtil.binary(msgBody, 16)); + return msgBody; + } + + /** + * 获取时段费率号 + * + * @param timeRange 时间段 例如:07:30:00-10:30:00 + * @param timeMap 尖峰平谷4个类型的时间段,每个类型都包含一个时间段集合 + * @return + */ + private byte getTimeType(String timeRange, Map> timeMap) { + byte b = 5; + // 这里的timeRange是00:00-00:30 这样的格式 + String[] split = timeRange.split("-"); + LocalTime startTime1 = DateUtils.getLocalTime(split[0]); + LocalTime endTime1 = DateUtils.getLocalTime(split[1]); + + for (Map.Entry> entry : timeMap.entrySet()) { + List value = entry.getValue().stream().filter(StringUtils::isNotBlank).collect(Collectors.toList()); // 每个类型时间段集合 + if (CollectionUtils.isEmpty(value)) { + continue; + } + for (String s : value) { + // 这里的s是07:30-10:30 这样的格式 + List list = Lists.newArrayList(s.split("-")); + LocalTime startTime2 = DateUtils.getLocalTime(list.get(0)); + LocalTime endTime2 = DateUtils.getLocalTime(list.get(1)); + + boolean overlap = DateUtils.isOverlap(startTime1, endTime1, startTime2, endTime2, false); + if (overlap) { + return (byte) entry.getKey().intValue(); + } + } + } + return b; + } + + @Override + public BillingTemplateVO selectBillingTemplateByTemplateId(String templateId) { + return pileBillingTemplateMapper.selectBillingTemplateByTemplateId(templateId); + } + + @Override + public void insertPileBillingRelation(List relationList) { + if (CollectionUtils.isEmpty(relationList)) { + return; + } + // 根据桩号删除计费模板关系 + List pileSnList = relationList.stream().map(PileBillingRelation::getPileSn).collect(Collectors.toList()); + pileBillingTemplateMapper.deleteRelationByPileSn(pileSnList); + pileBillingTemplateMapper.insertPileBillingRelation(relationList); + } + + private Map getPriceMap(CreateOrUpdateBillingTemplateDTO dto, String type) { + BigDecimal electricityPrice = null; + BigDecimal servicePrice = null; + if (StringUtils.equals(type, "1")) { + electricityPrice = dto.getElectricityPriceA(); + servicePrice = dto.getServicePriceA(); + } else if (StringUtils.equals(type, "2")) { + electricityPrice = dto.getElectricityPriceB(); + servicePrice = dto.getServicePriceB(); + } else if (StringUtils.equals(type, "3")) { + electricityPrice = dto.getElectricityPriceC(); + servicePrice = dto.getServicePriceC(); + } else if (StringUtils.equals(type, "4")) { + electricityPrice = dto.getElectricityPriceD(); + servicePrice = dto.getServicePriceD(); + } + Map resultMap = Maps.newHashMap(); + resultMap.put("electricityPrice", electricityPrice); + resultMap.put("servicePrice", servicePrice); + return resultMap; + } + + /** + * 新增计费模板详情信息 + * + * @param pileBillingTemplate 计费模板对象 + */ + public void insertPileBillingDetail(PileBillingTemplate pileBillingTemplate) { + List pileBillingDetailList = pileBillingTemplate.getPileBillingDetailList(); + String templateCode = pileBillingTemplate.getTemplateCode(); + if (StringUtils.isNotNull(pileBillingDetailList)) { + List list = new ArrayList(); + for (PileBillingDetail pileBillingDetail : pileBillingDetailList) { + pileBillingDetail.setTemplateCode(templateCode); + list.add(pileBillingDetail); + } + if (list.size() > 0) { + pileBillingTemplateMapper.batchPileBillingDetail(list); + } + } + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileConnectorInfoServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileConnectorInfoServiceImpl.java new file mode 100644 index 000000000..4e84292d7 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileConnectorInfoServiceImpl.java @@ -0,0 +1,562 @@ +package com.jsowell.pile.service.impl; + +import com.alibaba.fastjson2.JSONObject; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.jsowell.common.constant.CacheConstants; +import com.jsowell.common.core.domain.ykc.RealTimeMonitorData; +import com.jsowell.common.core.page.PageResponse; +import com.jsowell.common.core.redis.RedisCache; +import com.jsowell.common.enums.ykc.PileConnectorDataBaseStatusEnum; +import com.jsowell.common.enums.ykc.PileStatusEnum; +import com.jsowell.common.util.DateUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.pile.domain.OrderBasicInfo; +import com.jsowell.pile.domain.PileBasicInfo; +import com.jsowell.pile.domain.PileConnectorInfo; +import com.jsowell.pile.dto.QueryConnectorDTO; +import com.jsowell.pile.dto.QueryConnectorListDTO; +import com.jsowell.pile.mapper.PileBasicInfoMapper; +import com.jsowell.pile.mapper.PileConnectorInfoMapper; +import com.jsowell.pile.service.IOrderBasicInfoService; +import com.jsowell.pile.service.IPileBasicInfoService; +import com.jsowell.pile.service.IPileConnectorInfoService; +import com.jsowell.pile.service.IPileModelInfoService; +import com.jsowell.pile.vo.base.ConnectorInfoVO; +import com.jsowell.pile.vo.web.PileConnectorInfoVO; +import com.jsowell.pile.vo.web.PileDetailVO; +import com.jsowell.pile.vo.web.PileModelInfoVO; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 充电桩枪口信息Service业务层处理 + * + * @author jsowell + * @date 2022-08-31 + */ +@Slf4j +@Service +public class PileConnectorInfoServiceImpl implements IPileConnectorInfoService { + @Autowired + private PileConnectorInfoMapper pileConnectorInfoMapper; + + @Autowired + private PileBasicInfoMapper pileBasicInfoMapper; + + @Autowired + private IPileBasicInfoService pileBasicInfoService; + + @Autowired + private IPileModelInfoService pileModelInfoService; + + @Autowired + private IOrderBasicInfoService orderBasicInfoService; + + @Autowired + private RedisCache redisCache; + + private final String URL = "http://localhost/pileConnectorInfo&code="; + + @Value("${qrcodeurl.prefix}") + private String QRCODE_URL_PREFIX; + + /** + * 查询充电桩枪口信息 + * + * @param id 充电桩枪口信息主键 + * @return 充电桩枪口信息 + */ + @Override + public PileConnectorInfo selectPileConnectorInfoById(Integer id) { + return pileConnectorInfoMapper.selectPileConnectorInfoById(id); + } + + /** + * 查询充电桩枪口信息列表 + * + * @param pileConnectorInfo 充电桩枪口信息 + * @return 充电桩枪口信息 + */ + @Override + public List selectPileConnectorInfoList(PileConnectorInfo pileConnectorInfo) { + return pileConnectorInfoMapper.selectPileConnectorInfoList(pileConnectorInfo); + } + + /** + * 通过充电桩号查询枪口信息列表 加缓存 + * + * @param pileSn 桩编号 + * @return 枪口信息列表 + */ + @Override + public List selectPileConnectorInfoList(String pileSn) { + // 取缓存 + String redisKey = CacheConstants.SELECT_PILE_CONNECTOR_INFO_LIST + pileSn; + List result = redisCache.getCacheObject(redisKey); + if (CollectionUtils.isEmpty(result)) { + // 缓存为空,查数据库 + PileConnectorInfo pileConnectorInfo = new PileConnectorInfo(); + pileConnectorInfo.setPileSn(pileSn); + result = selectPileConnectorInfoList(pileConnectorInfo); + if (CollectionUtils.isNotEmpty(result)) { + // 查询数据库不为空,存redis 2分钟 + redisCache.setCacheObject(redisKey, result, 2, TimeUnit.MINUTES); + } + } + return result; + } + + /** + * 公共方法 根据桩编号删除redis缓存 + */ + private void deleteRedisByPileSn(String pileSn) { + List keys = Lists.newArrayList(); + // 删除枪口信息缓存 + keys.add(CacheConstants.SELECT_PILE_CONNECTOR_INFO_LIST + pileSn); + // 删除充电桩详情缓存 + keys.add(CacheConstants.PILE_DETAIL_KEY + pileSn); + // 删除充电桩枪口状态缓存 + Set scan = redisCache.scan(CacheConstants.PILE_CONNECTOR_STATUS_KEY + pileSn + "*"); + if (CollectionUtils.isNotEmpty(scan)) { + keys.addAll(scan); + } + // 批量删除 + // log.debug("批量删除缓存 pileSn:{}, keys:{}", pileSn, keys); + redisCache.deleteObject(keys); + } + + /** + * 新增充电桩枪口信息 + * + * @param pileConnectorInfo 充电桩枪口信息 + * @return 结果 + */ + // @Override + // public int insertPileConnectorInfo(PileConnectorInfo pileConnectorInfo) { + // pileConnectorInfo.setCreateTime(DateUtils.getNowDate()); + // return pileConnectorInfoMapper.insertPileConnectorInfo(pileConnectorInfo); + // } + + /** + * 修改充电桩枪口信息 + * + * @param pileConnectorInfo 充电桩枪口信息 + * @return 结果 + */ + // @Override + // public int updatePileConnectorInfo(PileConnectorInfo pileConnectorInfo) { + // pileConnectorInfo.setUpdateTime(DateUtils.getNowDate()); + // deleteRedisByPileSn(pileConnectorInfo.getPileSn()); + // return pileConnectorInfoMapper.updatePileConnectorInfo(pileConnectorInfo); + // } + + /** + * 批量删除充电桩枪口信息 + * + * @param ids 需要删除的充电桩枪口信息主键 + * @return 结果 + */ + // @Override + // public int deletePileConnectorInfoByIds(Integer[] ids) { + // return pileConnectorInfoMapper.deletePileConnectorInfoByIds(ids); + // } + + @Override + public int deletePileConnectorInfoByPileSnList(List pileSnList) { + return pileConnectorInfoMapper.deletePileConnectorInfoByPileSnList(pileSnList); + } + + @Override + public int batchInsertConnectorInfo(List pileConnectorInfoList) { + return pileConnectorInfoMapper.batchInsertConnectorInfo(pileConnectorInfoList); + } + + /** + * 充电接口相关信息 + * + * @param dto 前台参数 + * @return 充电接口对象结果集 + */ + @Override + public List getConnectorInfoListByParams(QueryConnectorDTO dto) { + List list = pileConnectorInfoMapper.getConnectorInfoList(dto); + // 二维码、电量、设备订单号、平台订单暂时未传 + if (Objects.nonNull(list)) { + for (PileConnectorInfoVO p : list) { + // p.setConnectorQrCodeUrl(URL + p.getPileConnectorCode()); + p.setConnectorQrCodeUrl(getPileConnectorQrCodeUrl(p.getPileConnectorCode())); + } + } + return list; + } + + /** + * 通过充电站id查询充电枪信息 + * + * @param stationId 充电站id + * @return 充电接口信息集合 + */ + @Override + public List selectConnectorListByStationId(Long stationId) { + return pileConnectorInfoMapper.selectConnectorListByStationId(stationId); + } + + /** + * 通过入参查询枪口数据 + * + * @param dto + * @return + */ + @Override + public List getConnectorInfoListByParams(QueryConnectorListDTO dto) { + int pageNum = dto.getPageNum() == 0 ? 1 : dto.getPageNum(); + int pageSize = dto.getPageSize() == 0 ? 10 : dto.getPageSize(); + List pileSns = Lists.newArrayList(); + List connectorIds = dto.getConnectorIdList(); + List stationIdList = dto.getStationIdList(); + List connectorCodeList = dto.getConnectorCodeList(); + + // 通过运营商查询站点 + if (StringUtils.isNotBlank(dto.getMerchantId())) { + // 查询站点idList + List queryStationIdList = Lists.newArrayList(); + if (CollectionUtils.isNotEmpty(queryStationIdList)) { + if (CollectionUtils.isEmpty(stationIdList)) { + stationIdList = Lists.newArrayList(); + } + stationIdList.addAll(queryStationIdList); + } + } + + // 站点不为空,拿到站点下所有的充电桩id + if (CollectionUtils.isNotEmpty(stationIdList)) { + List pileInfoVOS = pileBasicInfoService.selectPileListByStationIds(stationIdList); + if (CollectionUtils.isNotEmpty(pileInfoVOS)) { + pileSns.addAll(pileInfoVOS.stream().map(PileDetailVO::getPileSn).collect(Collectors.toList())); + } + } + + // 通过充电桩id查询 + if (CollectionUtils.isNotEmpty(dto.getPileIds())) { + // 批量查 + List pileList = pileBasicInfoMapper.selectByIdList(dto.getPileIds()); + if (CollectionUtils.isNotEmpty(pileList)) { + pileSns.addAll(pileList.stream().map(PileBasicInfo::getSn).collect(Collectors.toList())); + } + } + if (CollectionUtils.isEmpty(pileSns) && CollectionUtils.isEmpty(connectorIds) && CollectionUtils.isEmpty(connectorCodeList)) { + return Lists.newArrayList(); + } + PageHelper.startPage(pageNum, pageSize); + + List pileConnectorInfoList = pileConnectorInfoMapper.getPileConnectorInfoList(pileSns, connectorIds, connectorCodeList); + + // 查询枪口当前订单 + for (PileConnectorInfoVO pileConnectorInfoVO : pileConnectorInfoList) { + String pileConnectorCode = pileConnectorInfoVO.getPileConnectorCode(); + pileConnectorInfoVO.setConnectorQrCodeUrl(getPileConnectorQrCodeUrl(pileConnectorCode)); // 枪口号二维码 + OrderBasicInfo order = orderBasicInfoService.queryChargingByPileConnectorCode(pileConnectorCode); + if (order != null) { + pileConnectorInfoVO.setOrderCode(order.getOrderCode()); + } + } + + queryRealTimeData(pileConnectorInfoList); + return pileConnectorInfoList; + } + + /** + * 充电桩枪口的二维码 + * @param pileConnectorCode 枪口编号 如枪口编号为空,则返回前缀 https://api.jsowellcloud.com/app-xcx-h5/pile/connectorDetail/ + * @return + */ + @Override + public String getPileConnectorQrCodeUrl(String pileConnectorCode) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(QRCODE_URL_PREFIX); + stringBuilder.append("/app-xcx-h5/pile/connectorDetail/"); + if (StringUtils.isNotBlank(pileConnectorCode)) { + if (!stringBuilder.toString().endsWith("/")) { + stringBuilder.append("/"); + } + stringBuilder.append(pileConnectorCode); + } + return stringBuilder.toString(); + } + + /** + * uniapp通过入参查询枪口数据 + * + * @param dto + * @return + */ + @Override + public PageResponse getUniAppConnectorInfoListByParams(QueryConnectorListDTO dto) { + int pageNum = dto.getPageNum() == 0 ? 1 : dto.getPageNum(); + int pageSize = dto.getPageSize() == 0 ? 10 : dto.getPageSize(); + + List pileSns = Lists.newArrayList(); + List connectorIds = dto.getConnectorIdList(); + List stationIdList = dto.getStationIdList(); + List connectorCodeList = dto.getConnectorCodeList(); + + // 通过运营商查询站点 + if (StringUtils.isNotBlank(dto.getMerchantId())) { + // 查询站点idList + List queryStationIdList = Lists.newArrayList(); + if (CollectionUtils.isNotEmpty(queryStationIdList)) { + if (CollectionUtils.isEmpty(stationIdList)) { + stationIdList = Lists.newArrayList(); + } + stationIdList.addAll(queryStationIdList); + } + } + + // 站点不为空,拿到站点下所有的充电桩id + if (CollectionUtils.isNotEmpty(stationIdList)) { + List pileInfoVOS = pileBasicInfoService.selectPileListByStationIds(stationIdList); + if (CollectionUtils.isNotEmpty(pileInfoVOS)) { + pileSns.addAll(pileInfoVOS.stream().map(PileDetailVO::getPileSn).collect(Collectors.toList())); + } + } + + // 通过充电桩id查询 + if (CollectionUtils.isNotEmpty(dto.getPileIds())) { + // 批量查 + List pileList = pileBasicInfoMapper.selectByIdList(dto.getPileIds()); + if (CollectionUtils.isNotEmpty(pileList)) { + pileSns.addAll(pileList.stream().map(PileBasicInfo::getSn).collect(Collectors.toList())); + } + } + if (CollectionUtils.isEmpty(pileSns) && CollectionUtils.isEmpty(connectorIds) && CollectionUtils.isEmpty(connectorCodeList)) { + return new PageResponse(); + } + // 分页 + PageHelper.startPage(pageNum, pageSize); + + List pileConnectorInfoList = pileConnectorInfoMapper.getPileConnectorInfoList(pileSns, connectorIds, connectorCodeList); + + PageInfo pageInfo = new PageInfo<>(pileConnectorInfoList); + + queryRealTimeData(pageInfo.getList()); + + // 返回结果集 + PageResponse pageResponse = PageResponse.builder() + .pageNum(pageNum) + .pageSize(pageSize) + .list(pageInfo.getList()) + .pages(pageInfo.getPages()) + .total(pageInfo.getTotal()) + .build(); + return pageResponse; + } + + /** + * uniApp通过站点id查询枪口列表信息 + * + * @param stationId 站点id + * @return + */ + @Override + public List getUniAppConnectorList(Long stationId) { + // TODO 加缓存 + return pileConnectorInfoMapper.getUniAppConnectorList(stationId); + } + + @Override + public List selectConnectorInfoList(String pileSn) { + // 查询充电桩型号信息 + PileModelInfoVO pileModelInfoVO = pileModelInfoService.getPileModelInfoByPileSn(pileSn); + + List connectorList = selectPileConnectorInfoList(pileSn); + List connectorInfoList = Lists.newArrayList(); + if (CollectionUtils.isNotEmpty(connectorList)) { + for (PileConnectorInfo connectorInfo : connectorList) { + ConnectorInfoVO infoVO = ConnectorInfoVO.builder() + .connectorCode(StringUtils.replace(connectorInfo.getPileConnectorCode(), pileSn, "")) + .pileConnectorCode(connectorInfo.getPileConnectorCode()) + .connectorStatus(connectorInfo.getStatus()) + .build(); + if (pileModelInfoVO != null) { + infoVO.setChargingType(pileModelInfoVO.getSpeedType()); + infoVO.setRatedPower(pileModelInfoVO.getRatedPower()); + } + connectorInfoList.add(infoVO); + } + } + return connectorInfoList; + } + + /** + * 查询充电枪口的实时数据 + */ + private void queryRealTimeData(List pileConnectorInfoList) { + if (CollectionUtils.isEmpty(pileConnectorInfoList)) { + return; + } + + // 获取枪口的日志记录 + for (PileConnectorInfoVO pileConnectorInfoVO : pileConnectorInfoList) { + // 从redis中获取实时数据信息 + if (StringUtils.isNotBlank(pileConnectorInfoVO.getOrderCode())) { + List chargingRealTimeDataList = orderBasicInfoService.getChargingRealTimeData(pileConnectorInfoVO.getOrderCode()); + RealTimeMonitorData realTimeMonitorData = chargingRealTimeDataList.get(0); + BigDecimal outputVoltage = new BigDecimal(realTimeMonitorData.getOutputVoltage()); + pileConnectorInfoVO.setVoltage(outputVoltage); + BigDecimal outputCurrent = new BigDecimal(realTimeMonitorData.getOutputCurrent()); + pileConnectorInfoVO.setCurrent(outputCurrent); + pileConnectorInfoVO.setSOC(realTimeMonitorData.getSOC()); // 充电百分比 + pileConnectorInfoVO.setChargingAmount(new BigDecimal(realTimeMonitorData.getChargingAmount())); + pileConnectorInfoVO.setChargingDegree(new BigDecimal(realTimeMonitorData.getChargingDegree())); + pileConnectorInfoVO.setGunLineTemperature(realTimeMonitorData.getGunLineTemperature()); // 枪线温度 + pileConnectorInfoVO.setTimeRemaining(realTimeMonitorData.getTimeRemaining()); + pileConnectorInfoVO.setChargingTime(realTimeMonitorData.getSumChargingTime()); + // 计算实时功率(单位:kw) + BigDecimal instantPowerTemp = outputVoltage.multiply(outputCurrent); + BigDecimal instantPower = instantPowerTemp.divide(new BigDecimal(1000)); + pileConnectorInfoVO.setInstantPower(instantPower.setScale(2, BigDecimal.ROUND_HALF_UP)); + log.info("枪口实时数据:{}", JSONObject.toJSONString(pileConnectorInfoVO)); + } + + if (checkPileOffLine(pileConnectorInfoVO.getPileSn())) { + // 最后收到消息的时间在1分钟前,则返回给前端枪口离线 + pileConnectorInfoVO.setStatus(Integer.valueOf(PileConnectorDataBaseStatusEnum.OFF_NETWORK.getValue())); + // 并修改数据库状态为离线 + updateConnectorStatusByPileSn(pileConnectorInfoVO.getPileSn(), PileConnectorDataBaseStatusEnum.OFF_NETWORK.getValue()); + } + } + } + + /** + * 通过桩编号修改枪口状态 + * 仅用于登录逻辑使用 + * + * @param pileSn 充电桩编号 + * @param status 充电桩枪口状态 + */ + public int updateConnectorStatusByPileSn(String pileSn, String status) { + if (StringUtils.isBlank(pileSn) || StringUtils.isBlank(status)) { + return 0; + } + // 通过pileSn查询枪口列表 + List connectorInfoList = selectPileConnectorInfoList(pileSn); + if (CollectionUtils.isEmpty(connectorInfoList)) { + return 0; + } + int i = 0; + for (PileConnectorInfo connectorInfo : connectorInfoList) { + i = i + updateConnectorStatus(connectorInfo.getPileConnectorCode(), status); + } + return i; + } + + /** + * 修改枪口状态 + * 所有修改枪口状态的都要使用这个方法,和数据库交互只有这一个口子 + * + * @param connectorCode 枪口号 桩编号+枪口号 + * @param status 状态 0:离网 (默认);1:空闲;2:占用(未充电);3:占用(充电中);4:占用(预约锁定) ;255:故障 + */ + @Override + public int updateConnectorStatus(String connectorCode, String status) { + int num = 0; + if (StringUtils.isBlank(connectorCode) || StringUtils.isBlank(status)) { + return num; + } + String redisKey = CacheConstants.PILE_CONNECTOR_STATUS_KEY + connectorCode; + String redisStatus = redisCache.getCacheObject(redisKey); + // log.info("修改充电桩枪口状态 缓存状态:{}, 传来的状态:{}", redisStatus, status); + if (!StringUtils.equals(redisStatus, status)) { + log.info("更新枪口状态 枪口号:{}, 缓存状态:{}, 状态值:{}, 状态:{}", connectorCode, redisStatus, status, PileConnectorDataBaseStatusEnum.getStatusDescription(status)); + String pileSn = connectorCode.substring(0, connectorCode.length() - 2); + // 只修改一个枪口的状态 + num = pileConnectorInfoMapper.updateConnectorStatus(connectorCode, status); + deleteRedisByPileSn(pileSn); + redisCache.setCacheObject(redisKey, status); + } + return num; + } + + /** + * 批量获取桩状态 + * 桩的状态有 在线 离线 故障 + * + * @param pileSnList 桩编号列表 + * @return key:桩编号; value:状态 + */ + @Override + public Map getPileStatus(List pileSnList) { + Map resultMap = Maps.newHashMap(); + for (String pileSn : pileSnList) { + String pileStatus = ""; + + // 标识桩故障或者离线 + boolean flag = false; + + // 判断故障状态 + List connectorList = selectPileConnectorInfoList(pileSn); // 获取枪口信息 + List connectorStatusList = connectorList.stream().map(PileConnectorInfo::getStatus).collect(Collectors.toList()); + // 桩下面的枪口,任意一个故障,桩的状态就是故障 + if (connectorStatusList.contains(PileConnectorDataBaseStatusEnum.FAULT.getValue())) { + pileStatus = PileStatusEnum.FAULT.getValue(); + flag = true; + } + + // 判断离线状态 显示优先级 离线>故障 + // if (checkPileOffLine(pileSn)) { + // pileStatus = PileStatusEnum.OFF_LINE.getValue(); + // flag = true; + // } + + // 2023年1月10日11点32分 改成如果枪口离线,那么充电桩就是离线 + if (connectorStatusList.contains(PileConnectorDataBaseStatusEnum.OFF_NETWORK.getValue())) { + pileStatus = PileStatusEnum.OFF_LINE.getValue(); + flag = true; + } + + // 没有故障或者离线,就是在线状态 + if (!flag) { + pileStatus = PileStatusEnum.ON_LINE.getValue(); + } + + resultMap.put(pileSn, pileStatus); + } + return resultMap; + } + + @Override + public PileConnectorInfoVO getPileConnectorInfoByConnectorCode(String pileConnectorCode) { + return pileConnectorInfoMapper.getPileConnectorInfoByConnectorCode(pileConnectorCode); + } + + /** + * 判断充电桩是否离线 + * + * @param pileSn 桩编号 + * @return true离线 + */ + private boolean checkPileOffLine(String pileSn) { + // 获取桩最后连接时间,最后连接到平台的时间在1分钟之前,判定为离线 + String lastConnectionTime = redisCache.getCacheObject(CacheConstants.PILE_LAST_CONNECTION + pileSn); + if (StringUtils.isBlank(lastConnectionTime)) { + // 没有最后连接时间,返回离线 + return true; + } + long l = DateUtils.intervalTime(lastConnectionTime, DateUtils.getTime()); + return l >= 1L; + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileLicenceInfoServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileLicenceInfoServiceImpl.java new file mode 100644 index 000000000..2116b2a05 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileLicenceInfoServiceImpl.java @@ -0,0 +1,108 @@ +package com.jsowell.pile.service.impl; + +import com.google.common.collect.Lists; +import com.jsowell.common.util.DateUtils; +import com.jsowell.pile.domain.PileLicenceInfo; +import com.jsowell.pile.mapper.PileLicenceInfoMapper; +import com.jsowell.pile.service.IPileLicenceInfoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 充电桩证书信息Service业务层处理 + * + * @author jsowell + * @date 2022-08-27 + */ +@Service +public class PileLicenceInfoServiceImpl implements IPileLicenceInfoService +{ + @Autowired + private PileLicenceInfoMapper pileLicenceInfoMapper; + + /** + * 查询充电桩证书信息 + * + * @param id 充电桩证书信息主键 + * @return 充电桩证书信息 + */ + @Override + public PileLicenceInfo selectPileLicenceInfoById(Long id) + { + + return pileLicenceInfoMapper.selectPileLicenceInfoById(id); + } + + public static void main(String[] args) { + String applyTime = "00:00-05:00,10:00-13:00,10:00-13:00,10:00-13:00"; + List timeList = Lists.newArrayList(applyTime.split(",")); + System.out.println(timeList); + for (String s : timeList) { + System.out.println(s); + } + } + + /** + * 查询充电桩证书信息列表 + * + * @param pileLicenceInfo 充电桩证书信息 + * @return 充电桩证书信息 + */ + @Override + public List selectPileLicenceInfoList(PileLicenceInfo pileLicenceInfo) + { + return pileLicenceInfoMapper.selectPileLicenceInfoList(pileLicenceInfo); + } + + /** + * 新增充电桩证书信息 + * + * @param pileLicenceInfo 充电桩证书信息 + * @return 结果 + */ + @Override + public int insertPileLicenceInfo(PileLicenceInfo pileLicenceInfo) + { + pileLicenceInfo.setCreateTime(DateUtils.getNowDate()); + return pileLicenceInfoMapper.insertPileLicenceInfo(pileLicenceInfo); + } + + /** + * 修改充电桩证书信息 + * + * @param pileLicenceInfo 充电桩证书信息 + * @return 结果 + */ + @Override + public int updatePileLicenceInfo(PileLicenceInfo pileLicenceInfo) + { + pileLicenceInfo.setUpdateTime(DateUtils.getNowDate()); + return pileLicenceInfoMapper.updatePileLicenceInfo(pileLicenceInfo); + } + + /** + * 批量删除充电桩证书信息 + * + * @param ids 需要删除的充电桩证书信息主键 + * @return 结果 + */ + @Override + public int deletePileLicenceInfoByIds(Long[] ids) + { + return pileLicenceInfoMapper.deletePileLicenceInfoByIds(ids); + } + + /** + * 删除充电桩证书信息信息 + * + * @param id 充电桩证书信息主键 + * @return 结果 + */ + @Override + public int deletePileLicenceInfoById(Long id) + { + return pileLicenceInfoMapper.deletePileLicenceInfoById(id); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileMemberRelationServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileMemberRelationServiceImpl.java new file mode 100644 index 000000000..9f8ba88ee --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileMemberRelationServiceImpl.java @@ -0,0 +1,118 @@ +package com.jsowell.pile.service.impl; + +import com.google.common.collect.Lists; +import com.jsowell.common.util.DateUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.pile.domain.PileMemberRelation; +import com.jsowell.pile.mapper.PileMemberRelationMapper; +import com.jsowell.pile.service.IPileConnectorInfoService; +import com.jsowell.pile.service.IPileMemberRelationService; +import com.jsowell.pile.vo.uniapp.PersonalPileInfoVO; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 桩与用户绑定关系Service业务层处理 + * + * @author jsowell + * @date 2023-02-21 + */ +@Service +public class PileMemberRelationServiceImpl implements IPileMemberRelationService { + @Autowired + private PileMemberRelationMapper pileMemberRelationMapper; + + @Autowired + private IPileConnectorInfoService pileConnectorInfoService; + /** + * 查询桩与用户绑定关系 + * + * @param id 桩与用户绑定关系主键 + * @return 桩与用户绑定关系 + */ + @Override + public PileMemberRelation selectPileMemberRelationById(Integer id) { + return pileMemberRelationMapper.selectPileMemberRelationById(id); + } + + /** + * 查询桩与用户绑定关系列表 + * + * @param pileMemberRelation 桩与用户绑定关系 + * @return 桩与用户绑定关系 + */ + @Override + public List selectPileMemberRelationList(PileMemberRelation pileMemberRelation) { + return pileMemberRelationMapper.selectPileMemberRelationList(pileMemberRelation); + } + + /** + * 条件查询桩与用户绑定关系 + * + * @param pileMemberRelation 桩与用户绑定关系 + * @return 桩与用户绑定关系对象 + */ + @Override + public PileMemberRelation selectPileMemberRelation(PileMemberRelation pileMemberRelation) { + return pileMemberRelationMapper.selectPileMemberRelation(pileMemberRelation); + } + + /** + * 新增桩与用户绑定关系 + * + * @param pileMemberRelation 桩与用户绑定关系 + * @return 结果 + */ + @Override + public int insertPileMemberRelation(PileMemberRelation pileMemberRelation) { + pileMemberRelation.setCreateTime(DateUtils.getNowDate()); + return pileMemberRelationMapper.insertPileMemberRelation(pileMemberRelation); + } + + /** + * 修改桩与用户绑定关系 + * + * @param pileMemberRelation 桩与用户绑定关系 + * @return 结果 + */ + @Override + public int updatePileMemberRelation(PileMemberRelation pileMemberRelation) { + return pileMemberRelationMapper.updatePileMemberRelation(pileMemberRelation); + } + + /** + * 批量删除桩与用户绑定关系 + * + * @param ids 需要删除的桩与用户绑定关系主键 + * @return 结果 + */ + @Override + public int deletePileMemberRelationByIds(Integer[] ids) { + return pileMemberRelationMapper.deletePileMemberRelationByIds(ids); + } + + /** + * 删除桩与用户绑定关系信息 + * + * @param id 桩与用户绑定关系主键 + * @return 结果 + */ + @Override + public int deletePileMemberRelationById(Integer id) { + return pileMemberRelationMapper.deletePileMemberRelationById(id); + } + + @Override + public List selectPileMemberRelationByPileSn(String pileSn) { + PileMemberRelation pileMemberRelation = new PileMemberRelation(); + pileMemberRelation.setPileSn(pileSn); + return selectPileMemberRelationList(pileMemberRelation); + } + + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileMerchantInfoServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileMerchantInfoServiceImpl.java new file mode 100644 index 000000000..b30177e17 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileMerchantInfoServiceImpl.java @@ -0,0 +1,167 @@ +package com.jsowell.pile.service.impl; + +import com.jsowell.common.annotation.DataScope; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.domain.entity.SysDept; +import com.jsowell.common.util.DateUtils; +import com.jsowell.common.util.DictUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.pile.domain.PileMerchantInfo; +import com.jsowell.pile.mapper.PileMerchantInfoMapper; +import com.jsowell.pile.service.IPileMerchantInfoService; +import com.jsowell.pile.vo.base.MerchantInfoVO; +import com.jsowell.system.service.SysDeptService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Objects; + +/** + * 充电桩运营商信息Service业务层处理 + * + * @author jsowell + * @date 2022-08-27 + */ +@Slf4j +@Service +public class PileMerchantInfoServiceImpl implements IPileMerchantInfoService { + @Autowired + private PileMerchantInfoMapper pileMerchantInfoMapper; + + @Autowired + private SysDeptService sysDeptService; + + @Value("${weixin.login.appid}") + private String appid; + + /** + * 查询充电桩运营商信息 + * + * @param id 充电桩运营商信息主键 + * @return 充电桩运营商信息 + */ + @Override + public PileMerchantInfo selectPileMerchantInfoById(Long id) { + return pileMerchantInfoMapper.selectPileMerchantInfoById(id); + } + + /** + * 查询充电桩运营商信息列表 + * + * @param pileMerchantInfo 充电桩运营商信息 + * @return 充电桩运营商信息 + */ + @Override + @DataScope(deptAlias = "t") + public List selectPileMerchantInfoList(PileMerchantInfo pileMerchantInfo) { + List list = pileMerchantInfoMapper.selectPileMerchantInfoList(pileMerchantInfo); + if (Objects.nonNull(list)) { + for (PileMerchantInfo p:list) { + String status = p.getStatus(); + String merchant_status = DictUtils.getDictLabel("merchant_status", status); + p.setStatus(merchant_status); + } + } + return list; + } + + /** + * 新增充电桩运营商信息 + * + * @param pileMerchantInfo 充电桩运营商信息 + * @return 结果 + */ + @Override + @Transactional + public int insertPileMerchantInfo(PileMerchantInfo pileMerchantInfo) { + // 1. 新增sys_dept + SysDept dept = new SysDept(); + dept.setParentId(100L); + dept.setOrderNum(0); + dept.setDeptName(pileMerchantInfo.getMerchantName()); + dept.setLeader(pileMerchantInfo.getManagerName()); + dept.setPhone(pileMerchantInfo.getManagerPhone()); + dept.setStatus("0"); + sysDeptService.insertDept(dept); + + // 2. 新增pile_merchant_info + Long deptId = dept.getDeptId(); + // pileMerchantInfo.setId(deptId); + pileMerchantInfo.setDeptId(String.valueOf(deptId)); + pileMerchantInfo.setStatus(Constants.ONE); + String appId = StringUtils.isBlank(pileMerchantInfo.getAppId()) + ? appid + : pileMerchantInfo.getAppId(); + pileMerchantInfo.setAppId(appId); + return pileMerchantInfoMapper.insertPileMerchantInfo(pileMerchantInfo); + } + + /** + * 修改充电桩运营商信息 + * + * @param pileMerchantInfo 充电桩运营商信息 + * @return 结果 + */ + @Override + public int updatePileMerchantInfo(PileMerchantInfo pileMerchantInfo) { + pileMerchantInfo.setUpdateTime(DateUtils.getNowDate()); + return pileMerchantInfoMapper.updatePileMerchantInfo(pileMerchantInfo); + } + + /** + * 批量删除充电桩运营商信息 + * + * @param ids 需要删除的充电桩运营商信息主键 + * @return 结果 + */ + @Override + public int deletePileMerchantInfoByIds(Long[] ids) { + return pileMerchantInfoMapper.deletePileMerchantInfoByIds(ids); + } + + /** + * 删除充电桩运营商信息信息 + * + * @param id 充电桩运营商信息主键 + * @return 结果 + */ + @Override + public int deletePileMerchantInfoById(Long id) { + return pileMerchantInfoMapper.deletePileMerchantInfoById(id); + } + + @Override + public String getMerchantIdByAppId(String appId) { + if (StringUtils.isBlank(appId)) { + return null; + } + try { + PileMerchantInfo pileMerchantInfo = pileMerchantInfoMapper.selectPileMerchantInfoByAppId(appId); + if (pileMerchantInfo != null) { + return pileMerchantInfo.getId().toString(); + } + } catch (Exception e) { + log.error("通过appid获取运营商id error", e); + } + return null; + } + + @Override + public MerchantInfoVO getMerchantInfo(String merchantId) { + PileMerchantInfo pileMerchantInfo = selectPileMerchantInfoById(Long.parseLong(merchantId)); + if (pileMerchantInfo == null) { + return null; + } + MerchantInfoVO vo = MerchantInfoVO.builder() + .merchantId(merchantId) + .merchantName(pileMerchantInfo.getMerchantName()) + .merchantTel(pileMerchantInfo.getServicePhone()) + .deptId(pileMerchantInfo.getDeptId()) + .build(); + return vo; + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileModelInfoServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileModelInfoServiceImpl.java new file mode 100644 index 000000000..51a4d4a1f --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileModelInfoServiceImpl.java @@ -0,0 +1,113 @@ +package com.jsowell.pile.service.impl; + +import com.google.common.collect.Lists; +import com.jsowell.common.util.DateUtils; +import com.jsowell.pile.domain.PileModelInfo; +import com.jsowell.pile.mapper.PileModelInfoMapper; +import com.jsowell.pile.service.IPileModelInfoService; +import com.jsowell.pile.vo.web.PileModelInfoVO; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 充电桩型号信息Service业务层处理 + * + * @author jsowell + * @date 2022-08-26 + */ +@Service +public class PileModelInfoServiceImpl implements IPileModelInfoService { + @Autowired + private PileModelInfoMapper pileModelInfoMapper; + + /** + * 查询充电桩型号信息 + * + * @param id 充电桩型号信息主键 + * @return 充电桩型号信息 + */ + @Override + public PileModelInfo selectPileModelInfoById(Long id) { + return pileModelInfoMapper.selectPileModelInfoById(id); + } + + /** + * 查询充电桩型号信息列表 + * + * @param pileModelInfo 充电桩型号信息 + * @return 充电桩型号信息 + */ + @Override + public List selectPileModelInfoList(PileModelInfo pileModelInfo) { + return pileModelInfoMapper.selectPileModelInfoList(pileModelInfo); + } + + /** + * 新增充电桩型号信息 + * + * @param pileModelInfo 充电桩型号信息 + * @return 结果 + */ + @Override + public int insertPileModelInfo(PileModelInfo pileModelInfo) { + pileModelInfo.setCreateTime(DateUtils.getNowDate()); + return pileModelInfoMapper.insertPileModelInfo(pileModelInfo); + } + + /** + * 修改充电桩型号信息 + * + * @param pileModelInfo 充电桩型号信息 + * @return 结果 + */ + @Override + public int updatePileModelInfo(PileModelInfo pileModelInfo) { + pileModelInfo.setUpdateTime(DateUtils.getNowDate()); + return pileModelInfoMapper.updatePileModelInfo(pileModelInfo); + } + + /** + * 批量删除充电桩型号信息 + * + * @param ids 需要删除的充电桩型号信息主键 + * @return 结果 + */ + @Override + public int deletePileModelInfoByIds(Long[] ids) { + return pileModelInfoMapper.deletePileModelInfoByIds(ids); + } + + /** + * 删除充电桩型号信息信息 + * + * @param id 充电桩型号信息主键 + * @return 结果 + */ + @Override + public int deletePileModelInfoById(Long id) { + return pileModelInfoMapper.deletePileModelInfoById(id); + } + + /** + * 通过桩编号集合获取型号表中数据 + * + * @param pileSns 桩编号集合 + * @return PileModelInfo对象 + */ + @Override + public List getPileModelInfoByPileSnList(List pileSns) { + return pileModelInfoMapper.getPileModelInfoByPileSnList(pileSns); + } + + @Override + public PileModelInfoVO getPileModelInfoByPileSn(String pileSn) { + List list = getPileModelInfoByPileSnList(Lists.newArrayList(pileSn)); + if (CollectionUtils.isNotEmpty(list)) { + return list.get(0); + } + return null; + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileMsgRecordServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileMsgRecordServiceImpl.java new file mode 100644 index 000000000..e27b692e2 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileMsgRecordServiceImpl.java @@ -0,0 +1,109 @@ +package com.jsowell.pile.service.impl; + +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; +import com.jsowell.common.core.page.PageResponse; +import com.jsowell.common.util.BytesUtil; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.YKCUtils; +import com.jsowell.pile.domain.PileMsgRecord; +import com.jsowell.pile.dto.QueryPileDTO; +import com.jsowell.pile.mapper.PileMsgRecordMapper; +import com.jsowell.pile.service.IPileMsgRecordService; +import com.jsowell.pile.vo.web.PileCommunicationLogVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@Service +public class PileMsgRecordServiceImpl implements IPileMsgRecordService { + + @Autowired + private PileMsgRecordMapper pileMsgRecordMapper; + + @Override + public void save(String pileSn, String connectorCode, String frameType, String jsonMsg, String originalMsg) { + PileMsgRecord pileMsgRecord = PileMsgRecord.builder() + .pileSn(pileSn) + .connectorCode(connectorCode) + .jsonMsg(jsonMsg) + .frameType(frameType) + .originalMsg(originalMsg) + .build(); + pileMsgRecordMapper.insertSelective(pileMsgRecord); + } + + // @Override + // public List getByConnectorCodeList(List connectorCodeList) { + // if (CollectionUtils.isEmpty(connectorCodeList)) { + // return Lists.newArrayList(); + // } + // return pileMsgRecordMapper.getByConnectorCodeList(connectorCodeList); + // } + + public static void main(String[] args) { + String type = BytesUtil.bcd2Str(YKCFrameTypeCode.TRANSACTION_RECORDS_CODE.getBytes()); + System.out.println(type); + String binary = BytesUtil.binary(YKCFrameTypeCode.TRANSACTION_RECORDS_CODE.getBytes(), 16); + System.out.println(binary); + String s = YKCUtils.frameType2Str(YKCFrameTypeCode.TRANSACTION_RECORDS_CODE.getBytes()); + System.out.println(s); + + } + + @Override + public PageResponse getPileFeedList(QueryPileDTO dto) { + // 分页 + PageHelper.startPage(dto.getPageNum(), dto.getPageSize()); + List pileFeedList = pileMsgRecordMapper.getPileFeedList(dto.getPileSn()); + PageInfo pageInfo = new PageInfo<>(pileFeedList); + + List list = new ArrayList<>(); + for (PileMsgRecord pileMsgRecord : pageInfo.getList()) { + PileCommunicationLogVO vo = new PileCommunicationLogVO(); + String frameType = pileMsgRecord.getFrameType(); + + String frameTypeStr = YKCFrameTypeCode.getFrameTypeStr(frameType); + if (StringUtils.isNotBlank(frameTypeStr)) { + vo.setDescription(frameTypeStr); + } else { + vo.setDescription(pileMsgRecord.getJsonMsg()); + } + // if (StringUtils.equals(YKCUtils.frameType2Str(YKCFrameTypeCode.LOGIN_CODE.getBytes()), frameType)) { + // // 登录 + // vo.setDescription("充电桩登录认证"); + // } else if (StringUtils.equals(YKCUtils.frameType2Str(YKCFrameTypeCode.REMOTE_RESTART_CODE.getBytes()), frameType)) { + // // 远程重启 + // vo.setDescription("远程重启"); + // } else if (StringUtils.equals(YKCUtils.frameType2Str(YKCFrameTypeCode.REMOTE_CONTROL_START_CODE.getBytes()), frameType)) { + // // 远程启动充电 + // vo.setDescription("运营平台远程控制启机"); + // }else if (StringUtils.equals(YKCUtils.frameType2Str(YKCFrameTypeCode.REMOTE_STOP_CHARGING_CODE.getBytes()), frameType)) { + // // 远程停机 + // vo.setDescription("运营平台远程停机"); + // }else if (StringUtils.equals(YKCUtils.frameType2Str(YKCFrameTypeCode.TRANSACTION_RECORDS_CODE.getBytes()), frameType)) { + // // 交易记录 + // vo.setDescription("交易结算"); + // }else if (StringUtils.equals(YKCUtils.frameType2Str(YKCFrameTypeCode.PILE_LOG_OUT.getBytes()), frameType)) { + // // 退出 + // vo.setDescription("充电桩退出"); + // } + vo.setPileSn(pileMsgRecord.getPileSn()); + vo.setCreateTime(pileMsgRecord.getCreateTime()); + vo.setFrameType(frameType); + list.add(vo); + } + + PageResponse pageResponse = PageResponse.builder() + .pageNum(pageInfo.getPageNum()) + .pageSize(pageInfo.getPageSize()) + .list(list) + .total(pageInfo.getTotal()) + .pages(pageInfo.getPages()) + .build(); + return pageResponse; + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileSimInfoServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileSimInfoServiceImpl.java new file mode 100644 index 000000000..a45bd2b9f --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileSimInfoServiceImpl.java @@ -0,0 +1,137 @@ +package com.jsowell.pile.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.jsowell.common.util.DateUtils; +import com.jsowell.pile.domain.PileSimInfo; +import com.jsowell.pile.dto.QuerySimInfoDTO; +import com.jsowell.pile.mapper.PileSimInfoMapper; +import com.jsowell.pile.service.IPileSimInfoService; +import com.jsowell.pile.vo.web.SimCardInfoVO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 充电桩SIM卡信息Service业务层处理 + * + * @author jsowell + * @date 2022-08-26 + */ +@Slf4j +@Service +public class PileSimInfoServiceImpl implements IPileSimInfoService { + @Autowired + private PileSimInfoMapper pileSimInfoMapper; + + /** + * 查询充电桩SIM卡信息 + * + * @param id 充电桩SIM卡信息主键 + * @return 充电桩SIM卡信息 + */ + @Override + public PileSimInfo selectPileSimInfoById(Long id) { + return pileSimInfoMapper.selectPileSimInfoById(id); + } + + /** + * 查询充电桩SIM卡信息列表 + * + * @param pileSimInfo 充电桩SIM卡信息 + * @return 充电桩SIM卡信息 + */ + @Override + public List selectPileSimInfoList(PileSimInfo pileSimInfo) { + return pileSimInfoMapper.selectPileSimInfoList(pileSimInfo); + } + + /** + * 后管查询sim卡信息列表 + * @return + */ + @Override + public List getSimInfoList(QuerySimInfoDTO dto) { + return pileSimInfoMapper.getSimInfoList(dto); + } + + /** + * 新增充电桩SIM卡信息 + * + * @param pileSimInfo 充电桩SIM卡信息 + * @return 结果 + */ + @Override + public int insertPileSimInfo(PileSimInfo pileSimInfo) { + pileSimInfo.setCreateTime(DateUtils.getNowDate()); + log.info("新增充电桩SIM卡信息 PileSimInfo:{}", JSON.toJSONString(pileSimInfo)); + return pileSimInfoMapper.insertPileSimInfo(pileSimInfo); + } + + /** + * 修改充电桩SIM卡信息 + * + * @param pileSimInfo 充电桩SIM卡信息 + * @return 结果 + */ + @Override + public int updatePileSimInfo(PileSimInfo pileSimInfo) { + pileSimInfo.setUpdateTime(DateUtils.getNowDate()); + return pileSimInfoMapper.updatePileSimInfo(pileSimInfo); + } + + /** + * 批量删除充电桩SIM卡信息 + * + * @param ids 需要删除的充电桩SIM卡信息主键 + * @return 结果 + */ + @Override + public int deletePileSimInfoByIds(Long[] ids) { + return pileSimInfoMapper.deletePileSimInfoByIds(ids); + } + + /** + * 删除充电桩SIM卡信息信息 + * + * @param id 充电桩SIM卡信息主键 + * @return 结果 + */ + @Override + public int deletePileSimInfoById(Long id) { + return pileSimInfoMapper.deletePileSimInfoById(id); + } + + /** + * 通过桩编码查询sim卡信息 + * + * @param pileSn 桩编码 + * @return + */ + @Override + public SimCardInfoVO querySimCardInfoByPileSn(String pileSn) { + return pileSimInfoMapper.querySimCardInfoByPileSn(pileSn); + } + + /** + * 通过卡号批量查询sim卡信息 + * + * @param iccIds 卡号 + * @return + */ + @Override + public List selectSimInfoByIccIds(List iccIds) { + return pileSimInfoMapper.selectSimInfoByIccIds(iccIds); + } + + /** + * 通过卡号查询sim卡信息 + * + * @param iccId 卡号 + * @return + */ + public PileSimInfo getBasicInfoByIccId(String iccId) { + return pileSimInfoMapper.getBasicInfoByIccId(iccId); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileStationInfoServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileStationInfoServiceImpl.java new file mode 100644 index 000000000..d3e4b51e0 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/PileStationInfoServiceImpl.java @@ -0,0 +1,352 @@ +package com.jsowell.pile.service.impl; + +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.google.common.collect.Lists; +import com.jsowell.common.annotation.DataScope; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.domain.entity.SysDept; +import com.jsowell.common.core.page.PageResponse; +import com.jsowell.common.util.DateUtils; +import com.jsowell.common.util.DistanceUtils; +import com.jsowell.common.util.SecurityUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.ip.AddressUtils; +import com.jsowell.pile.domain.PileStationInfo; +import com.jsowell.pile.dto.FastCreateStationDTO; +import com.jsowell.pile.dto.QueryStationDTO; +import com.jsowell.pile.mapper.PileStationInfoMapper; +import com.jsowell.pile.service.IPileBillingTemplateService; +import com.jsowell.pile.service.IPileConnectorInfoService; +import com.jsowell.pile.service.IPileMerchantInfoService; +import com.jsowell.pile.service.IPileStationInfoService; +import com.jsowell.pile.vo.base.ConnectorInfoVO; +import com.jsowell.pile.vo.base.MerchantInfoVO; +import com.jsowell.pile.vo.base.StationInfoVO; +import com.jsowell.pile.vo.uniapp.CurrentTimePriceDetails; +import com.jsowell.pile.vo.web.PileStationVO; +import com.jsowell.system.service.SysDeptService; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +/** + * 充电站信息Service业务层处理 + * + * @author jsowell + * @date 2022-08-30 + */ +@Service +public class PileStationInfoServiceImpl implements IPileStationInfoService { + @Autowired + private PileStationInfoMapper pileStationInfoMapper; + + @Autowired + private IPileConnectorInfoService pileConnectorInfoService; + + @Autowired + private IPileBillingTemplateService pileBillingTemplateService; + + @Autowired + private IPileMerchantInfoService pileMerchantInfoService; + + @Autowired + private SysDeptService sysDeptService; + + /** + * 查询充电站信息 + * + * @param id 充电站信息主键 + * @return 充电站信息 + */ + @Override + public PileStationInfo selectPileStationInfoById(Long id) { + return pileStationInfoMapper.selectPileStationInfoById(id); + } + + /** + * 查询站点基本资料 + * @param stationId + * @return + */ + @Override + public PileStationVO getStationInfo(String stationId) { + PileStationVO vo = new PileStationVO(); + PileStationInfo pileStationInfo = selectPileStationInfoById(Long.parseLong(stationId)); + // 查计费模板 + CurrentTimePriceDetails currentTimePriceDetails = pileBillingTemplateService.getCurrentTimePriceDetails(stationId); + if (currentTimePriceDetails != null) { + vo.setElectricityPrice(new BigDecimal(currentTimePriceDetails.getElectricityPrice())); + vo.setServicePrice(new BigDecimal(currentTimePriceDetails.getServicePrice())); + } + + if (pileStationInfo != null) { + vo.setMerchantId(pileStationInfo.getMerchantId().toString()); + vo.setStationName(pileStationInfo.getStationName()); + vo.setId(pileStationInfo.getId().toString()); + vo.setAreaCode(pileStationInfo.getAreaCode()); + vo.setAddress(pileStationInfo.getAddress()); + vo.setMerchantId(pileStationInfo.getMerchantId().toString()); + // vo.setMerchantName(pileStationInfo.getmer()); + vo.setMerchantAdminName(pileStationInfo.getStationAdminName()); + vo.setStationStatus(Integer.parseInt(pileStationInfo.getStationStatus())); + vo.setStationType(pileStationInfo.getStationType()); + vo.setCreateTime(DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, pileStationInfo.getCreateTime())); + vo.setStationTel(pileStationInfo.getStationTel()); + vo.setMatchCars(pileStationInfo.getMatchCars()); + if (StringUtils.isNotBlank(pileStationInfo.getMatchCars())) { + vo.setSelectMatchCars(Lists.newArrayList(pileStationInfo.getMatchCars().split(","))); + } + vo.setStationLat(pileStationInfo.getStationLat()); + vo.setStationLng(pileStationInfo.getStationLng()); + vo.setConstruction(pileStationInfo.getConstruction()); + vo.setBusinessHours(pileStationInfo.getBusinessHours()); + // vo.setOrganizationCode(pileStationInfo.getor); + vo.setPublicFlag(pileStationInfo.getPublicFlag()); + vo.setOpenFlag(pileStationInfo.getOpenFlag()); + } + return vo; + } + + /** + * 查询充电站信息列表 + * + * @param pileStationInfo 充电站信息 + * @return 充电站信息 + */ + @Override + public List selectPileStationInfoList(PileStationInfo pileStationInfo) { + return pileStationInfoMapper.selectPileStationInfoList(pileStationInfo); + } + + /** + * 通过运营商id查询站点信息 + * + * @param merchantId 运营商id + * @return 站点信息列表 + */ + @Override + public List selectStationListByMerchantId(Long merchantId) { + return pileStationInfoMapper.selectStationListByMerchantId(merchantId); + } + + /** + * 新增充电站信息 + * + * @param pileStationInfo 充电站信息 + * @return 结果 + */ + @Override + + public int insertPileStationInfo(PileStationInfo pileStationInfo) { + pileStationInfo.setCreateTime(DateUtils.getNowDate()); + pileStationInfo.setCreateBy(SecurityUtils.getUsername()); + return pileStationInfoMapper.insertPileStationInfo(pileStationInfo); + } + + /** + * 快速建站 + * @param dto + * @return + */ + @Override + public int fastCreateStation(FastCreateStationDTO dto) { + MerchantInfoVO merchantInfo = pileMerchantInfoService.getMerchantInfo(dto.getMerchantId()); + if (merchantInfo == null) { + return 0; + } + + // 1. 新增sys_dept + SysDept dept = new SysDept(); + // 根据运营商Id查询到对应的部门id + dept.setParentId(Long.parseLong(merchantInfo.getDeptId())); + dept.setOrderNum(0); + dept.setDeptName(dto.getStationName()); + dept.setLeader(dto.getStationAdminName()); + dept.setPhone(dto.getStationTel()); + dept.setStatus("0"); + sysDeptService.insertDept(dept); + + PileStationInfo pileStationInfo = new PileStationInfo(); + // pileStationInfo.setId(dept.getDeptId()); + pileStationInfo.setDeptId(String.valueOf(dept.getDeptId())); + // 前端输入信息 + pileStationInfo.setMerchantId(Long.valueOf(dto.getMerchantId())); + pileStationInfo.setStationName(dto.getStationName()); + pileStationInfo.setAddress(dto.getAddress()); + pileStationInfo.setAreaCode(dto.getAreaCode()); + pileStationInfo.setCapacity(BigDecimal.ZERO); // 容量 + pileStationInfo.setStationTel(dto.getStationTel()); + pileStationInfo.setStationAdminName(dto.getStationAdminName()); + // 获取经纬度 + Map longitudeAndLatitude = AddressUtils.getLongitudeAndLatitude(dto.getAreaCode(), dto.getAddress()); + if (longitudeAndLatitude != null) { + pileStationInfo.setStationLng(longitudeAndLatitude.get("lng")); + pileStationInfo.setStationLat(longitudeAndLatitude.get("lat")); + } + pileStationInfo.setCreateBy(SecurityUtils.getUsername()); + int i = pileStationInfoMapper.insertPileStationInfo(pileStationInfo); + + + return i; + } + + /** + * 修改充电站信息 + * + * @param pileStationInfo 充电站信息 + * @return 结果 + */ + @Override + public int updatePileStationInfo(PileStationInfo pileStationInfo) { + pileStationInfo.setUpdateBy(SecurityUtils.getUsername()); + pileStationInfo.setUpdateTime(DateUtils.getNowDate()); + return pileStationInfoMapper.updatePileStationInfo(pileStationInfo); + } + + /** + * 批量删除充电站信息 + * + * @param ids 需要删除的充电站信息主键 + * @return 结果 + */ + @Override + public int deletePileStationInfoByIds(Long[] ids) { + return pileStationInfoMapper.deletePileStationInfoByIds(ids); + } + + /** + * 充电站列表信息 + * @param dto 前台参数 + * @return 充电站对象集合 + */ + @Override + @DataScope(deptAlias = "t3") + public List queryStationInfos(QueryStationDTO dto) { + List list = pileStationInfoMapper.queryStationInfos(dto); + // if (Objects.nonNull(list)){ + // for (PileStationVO p:list) { + // String station_type = p.getStationType(); + // p.setStationType(DictUtils.getDictLabel("station_type", station_type)); + // } + // } + return list; + } + + /** + * uniApp查询充电站信息并通过经纬度排序 + * + * @param dto 前台参数 + * @return 充电站对象集合 + */ + @Override + public PageResponse uniAppQueryStationInfoList(QueryStationDTO dto) { + int pageNum = dto.getPageNum() == 0 ? 1 : dto.getPageNum(); + int pageSize = dto.getPageSize() == 0 ? 10 : dto.getPageSize(); + + // 小程序站点列表页只展示对外开放的站点 + dto.setPublicFlag(Constants.ONE); + // 根据前台参数分页 + PageHelper.startPage(pageNum, pageSize); + List list = pileStationInfoMapper.queryStationInfos(dto); + PageInfo pageInfo = new PageInfo<>(list); + if (CollectionUtils.isEmpty(pageInfo.getList())) { + return PageResponse.builder() + .pageNum(pageInfo.getPageNum()) + .pageSize(pageInfo.getPageSize()) + .list(Lists.newArrayList()) + .pages(pageInfo.getPages()) + .total(pageInfo.getTotal()) + .build(); + } + + List stationVOList = Lists.newArrayList(); + StationInfoVO stationVO = null; + String stationLng = dto.getStationLng(); + String stationLat = dto.getStationLat(); + double distance = 0d; + for (PileStationVO pileStationVO : pageInfo.getList()) { + stationVO = new StationInfoVO(); + if (StringUtils.isNotEmpty(stationLng) && StringUtils.isNotEmpty(stationLat)) { + try{ + // 计算当前经纬度和站点之间的距离 + distance = DistanceUtils.getDistance(Double.parseDouble(stationLng), Double.parseDouble(stationLat), + Double.parseDouble(pileStationVO.getStationLng()), Double.parseDouble(pileStationVO.getStationLat())); + // 保留两位小数 + stationVO.setDistance(String.format("%.2f", distance)); + }catch (Exception e){ + stationVO.setDistance("0.00"); + } + + } + stationVO.setStationId(pileStationVO.getId()); + stationVO.setStationName(pileStationVO.getStationName()); + stationVO.setStationAddress(pileStationVO.getAddress()); + stationVO.setStationLat(pileStationVO.getStationLat()); + stationVO.setStationLng(pileStationVO.getStationLng()); + + // 站点图片 + if (StringUtils.isNotBlank(pileStationVO.getPictures())) { + stationVO.setStationImgList(Lists.newArrayList(pileStationVO.getPictures().split(","))); + } + // 枪口数量 + int fastTotal = 0; + int fastFree = 0; + int slowTotal = 0; + int slowFree = 0; + List connectorList = pileConnectorInfoService.getUniAppConnectorList(Long.parseLong(pileStationVO.getId())); + for (ConnectorInfoVO connectorVO : connectorList) { + if (StringUtils.equals(connectorVO.getChargingType(), Constants.ONE)) { + // 快充 + fastTotal += 1; + if (StringUtils.equals(connectorVO.getConnectorStatus(), Constants.ONE)) { + fastFree += 1; + } + } else { + // 慢充 + slowTotal += 1; + if (StringUtils.equals(connectorVO.getConnectorStatus(), Constants.ONE)) { + slowFree += 1; + } + } + } + stationVO.setFastTotal(fastTotal); + stationVO.setFastFree(fastFree); + stationVO.setSlowTotal(slowTotal); + stationVO.setSlowFree(slowFree); + + // 查询当前时段电费 + CurrentTimePriceDetails currentTimePriceDetails = pileBillingTemplateService.getCurrentTimePriceDetails(stationVO.getStationId()); + if (currentTimePriceDetails != null) { + stationVO.setElectricityPrice(currentTimePriceDetails.getElectricityPrice()); + stationVO.setServicePrice(currentTimePriceDetails.getServicePrice()); + stationVO.setTotalPrice(currentTimePriceDetails.getTotalPrice()); + } + stationVOList.add(stationVO); + } + + if (distance != 0.00) { + // 对集合按照距离排序,距离小的在前 + stationVOList.sort((o1, o2) -> { + Double a = Double.valueOf(o1.getDistance()); + Double b = Double.valueOf(o2.getDistance()); + return a.compareTo(b); + }); + } + + // 返回结果集 + return PageResponse.builder() + .pageNum(pageInfo.getPageNum()) + .pageSize(pageInfo.getPageSize()) + .list(stationVOList) + .pages(pageInfo.getPages()) + .total(pageInfo.getTotal()) + .build(); + } + +} + diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/WechatPayServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/WechatPayServiceImpl.java new file mode 100644 index 000000000..90b8cccc6 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/WechatPayServiceImpl.java @@ -0,0 +1,353 @@ +package com.jsowell.pile.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.Maps; +import com.jsowell.common.enums.MemberWalletEnum; +import com.jsowell.common.enums.ykc.ActionTypeEnum; +import com.jsowell.common.enums.ykc.PayModeEnum; +import com.jsowell.common.enums.ykc.ScenarioEnum; +import com.jsowell.common.util.DateUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.id.SnowflakeIdWorker; +import com.jsowell.pile.domain.MemberTransactionRecord; +import com.jsowell.pile.domain.WxpayCallbackRecord; +import com.jsowell.pile.domain.WxpayRefundCallback; +import com.jsowell.pile.dto.PaymentScenarioDTO; +import com.jsowell.pile.dto.WeixinPayDTO; +import com.jsowell.pile.service.IMemberBasicInfoService; +import com.jsowell.pile.service.IMemberTransactionRecordService; +import com.jsowell.pile.service.WechatPayService; +import com.jsowell.pile.service.WxpayCallbackRecordService; +import com.jsowell.pile.service.WxpayRefundCallbackService; +import com.jsowell.pile.vo.web.UpdateMemberBalanceDTO; +import com.jsowell.wxpay.common.WeChatPayParameter; +import com.jsowell.wxpay.response.WechatPayNotifyParameter; +import com.jsowell.wxpay.response.WechatPayNotifyResource; +import com.jsowell.wxpay.response.WechatPayRefundNotifyResource; +import com.jsowell.wxpay.response.WechatPayRefundRequest; +import com.jsowell.wxpay.response.WechatPayRefundResponse; +import com.jsowell.wxpay.utils.AesUtil; +import com.jsowell.wxpay.utils.HttpUtils; +import com.jsowell.wxpay.utils.WechatPayUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.servlet.http.HttpServletRequest; +import java.math.BigDecimal; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Map; + +/** + * 微信支付 + */ +@Slf4j +@Service +public class WechatPayServiceImpl implements WechatPayService { + + @Autowired + private WxpayCallbackRecordService wxpayCallbackRecordService; + + @Autowired + private WxpayRefundCallbackService wxpayRefundCallbackService; + + @Autowired + private IMemberBasicInfoService memberBasicInfoService; + + @Autowired + private IMemberTransactionRecordService memberTransactionRecordService; + + @Override + public Map weixinPayV3(WeixinPayDTO dto) throws Exception { + String openId = dto.getOpenId(); + //封装请求参数 + Map paramMap = new HashMap<>(); + // 支付的产品(小程序或者公众号,主要需要和微信支付绑定哦) + paramMap.put("appid", WeChatPayParameter.appId); + // 支付的商户号 + paramMap.put("mchid", WeChatPayParameter.mchId); + // 商品描述 + paramMap.put("description", dto.getDescription()); + // 商户订单号 + paramMap.put("out_trade_no", SnowflakeIdWorker.getSnowflakeId()); + // 交易结束时间 + paramMap.put("time_expire", getTimeExpire()); + // 附加数据 + paramMap.put("attach", dto.getAttach()); + // 通知地址 + paramMap.put("notify_url", WeChatPayParameter.notifyUrl); + + Map amountMap = Maps.newHashMap(); + //订单金额 单位分 + amountMap.put("total", Integer.parseInt(getMoney(dto.getAmount()))); + amountMap.put("currency", "CNY"); + paramMap.put("amount", amountMap); + // 设置小程序所需的openid + Map payerMap = Maps.newHashMap(); + payerMap.put("openid", openId); + paramMap.put("payer", payerMap); + + ObjectMapper objectMapper = new ObjectMapper(); + String body = objectMapper.writeValueAsString(paramMap); + log.info("支付的相关参数是:{}", body); + + Map stringObjectMap = HttpUtils.doPostWexin(WeChatPayParameter.unifiedOrderUrlJS, body); + try { + return WechatPayUtils.getTokenWeixin(WeChatPayParameter.appId, String.valueOf(stringObjectMap.get("prepay_id"))); + } catch (Exception e) { + log.error("微信支付v3 error", e); + } + return null; + } + + /** + * 获取过期时间 + * @return + */ + private String getTimeExpire() { + long currentTimeMillis = System.currentTimeMillis(); + currentTimeMillis = currentTimeMillis + (30 * 60 * 1000); + return DateUtils.timeStampToRfc3339(currentTimeMillis); + } + + /** + * 元转换成分 + * + * @param amount + * @return + */ + public static String getMoney(String amount) { + if (amount == null) { + return ""; + } + // 金额转化为分为单位 + // 处理包含, ¥ 或者$的金额 + String currency = amount.replaceAll("\\$|\\¥|\\,", ""); + int index = currency.indexOf("."); + int length = currency.length(); + Long amLong = 0L; + if (index == -1) { + amLong = Long.valueOf(currency + "00"); + } else if (length - index >= 3) { + amLong = Long.valueOf((currency.substring(0, index + 3)).replace(".", "")); + } else if (length - index == 2) { + amLong = Long.valueOf((currency.substring(0, index + 2)).replace(".", "") + 0); + } else { + amLong = Long.valueOf((currency.substring(0, index + 1)).replace(".", "") + "00"); + } + return amLong.toString(); + } + + /** + * 微信支付回调 + * @param request + * @param body + * @throws Exception + * @return + */ + @Override + public Map wechatPayCallbackInfo(HttpServletRequest request, WechatPayNotifyParameter body) throws Exception { + Map resultMap = Maps.newHashMap(); + //1:获取微信支付回调的获取签名信息 + String timestamp = request.getHeader("Wechatpay-Timestamp"); + String nonce = request.getHeader("Wechatpay-Nonce"); + ObjectMapper objectMapper = new ObjectMapper(); + // 2: 开始解析报文体 + String data = objectMapper.writeValueAsString(body); + String message = timestamp + "\n" + nonce + "\n" + data + "\n"; + //3:获取应答签名 + String sign = request.getHeader("Wechatpay-Signature"); + //4:获取平台对应的证书 + String serialNo = request.getHeader("Wechatpay-Serial"); + if (!WeChatPayParameter.certificateMap.containsKey(serialNo)) { + WeChatPayParameter.certificateMap = WechatPayUtils.refreshCertificate(); + } + X509Certificate x509Certificate = WeChatPayParameter.certificateMap.get(serialNo); + if (!WechatPayUtils.verify(x509Certificate, message.getBytes(), sign)) { + throw new IllegalArgumentException("微信支付签名验证失败:" + message); + } + // log.info("签名验证成功"); + WechatPayNotifyParameter.Resource resource = body.getResource(); + // 5:回调报文解密 + AesUtil aesUtil = new AesUtil(WeChatPayParameter.v3Key.getBytes()); + // 解密后json字符串 + String decryptToString = aesUtil.decryptToString( + resource.getAssociated_data().getBytes(), + resource.getNonce().getBytes(), + resource.getCiphertext()); + log.info("2-->decryptToString====>{}", decryptToString); + + //6:获取微信支付返回的信息 + WechatPayNotifyResource wechatPayNotifyResource = JSON.parseObject(decryptToString, WechatPayNotifyResource.class); + //7: 支付状态的判断 如果是success就代表支付成功 + if (StringUtils.equals(wechatPayNotifyResource.getTrade_state(), "SUCCESS")) { + // 8:获取支付的交易单号,流水号,和附属参数 + String out_trade_no = wechatPayNotifyResource.getOut_trade_no(); + // 微信支付单号 + String transaction_id = wechatPayNotifyResource.getTransaction_id(); + String attach = wechatPayNotifyResource.getAttach(); + log.info("3-->微信支付成功,商户订单号是:{}, 支付订单号:{}, 附属参数是:{}", out_trade_no, transaction_id, attach); + // 转换附属参数 + PaymentScenarioDTO paymentScenarioDTO = JSONObject.parseObject(attach, PaymentScenarioDTO.class); + String type = paymentScenarioDTO.getType(); + BigDecimal amount = new BigDecimal(wechatPayNotifyResource.getAmount().getTotal()); + if (StringUtils.equals(type, ScenarioEnum.ORDER.getValue())) { + // 1-订单支付 + resultMap.put("orderCode", paymentScenarioDTO.getOrderCode()); + resultMap.put("memberId", paymentScenarioDTO.getMemberId()); + resultMap.put("amount", amount); + resultMap.put("type", type); + } else if (StringUtils.equals(type, ScenarioEnum.BALANCE.getValue())) { + // 2-充值余额 + resultMap.put("memberId", paymentScenarioDTO.getMemberId()); + resultMap.put("amount", amount); + resultMap.put("type", type); + } + resultMap.put("out_trade_no", out_trade_no); + resultMap.put("transaction_id", transaction_id); + // 保存微信支付记录 + WxpayCallbackRecord record = new WxpayCallbackRecord(); + record.setPayScenario(type); + record.setMemberId(paymentScenarioDTO.getMemberId()); + record.setOrderCode(paymentScenarioDTO.getOrderCode()); + record.setOutTradeNo(out_trade_no); + record.setTransactionId(transaction_id); + record.setMchId(wechatPayNotifyResource.getMchid()); + record.setAppId(wechatPayNotifyResource.getAppid()); + record.setTradeType(wechatPayNotifyResource.getTrade_type()); + record.setTradeState(wechatPayNotifyResource.getTrade_state()); + record.setTradeStateDesc(wechatPayNotifyResource.getTrade_state_desc()); + record.setBankType(wechatPayNotifyResource.getBank_type()); + record.setAttach(wechatPayNotifyResource.getAttach()); + record.setSuccessTime(DateUtils.toLocalDateTime(wechatPayNotifyResource.getSuccess_time(), DateUtils.RFC3339)); + record.setPayerOpenId(wechatPayNotifyResource.getPayer().getOpenid()); + record.setPayerTotal(wechatPayNotifyResource.getAmount().getPayer_total()); + wxpayCallbackRecordService.insertSelective(record); + } + return resultMap; + } + + /** + * 获取微信退款回调信息 + * + * @param request + * @param body + * @return + */ + @Override + public Map wechatPayRefundCallbackInfo(HttpServletRequest request, WechatPayNotifyParameter body) throws Exception { + //1:获取微信支付回调的获取签名信息 + String timestamp = request.getHeader("Wechatpay-Timestamp"); + String nonce = request.getHeader("Wechatpay-Nonce"); + ObjectMapper objectMapper = new ObjectMapper(); + // 2: 开始解析报文体 + String data = objectMapper.writeValueAsString(body); + String message = timestamp + "\n" + nonce + "\n" + data + "\n"; + //3:获取应答签名 + String sign = request.getHeader("Wechatpay-Signature"); + //4:获取平台对应的证书 + String serialNo = request.getHeader("Wechatpay-Serial"); + if (!WeChatPayParameter.certificateMap.containsKey(serialNo)) { + WeChatPayParameter.certificateMap = WechatPayUtils.refreshCertificate(); + } + X509Certificate x509Certificate = WeChatPayParameter.certificateMap.get(serialNo); + if (!WechatPayUtils.verify(x509Certificate, message.getBytes(), sign)) { + throw new IllegalArgumentException("微信支付签名验证失败:" + message); + } + // log.info("签名验证成功"); + WechatPayNotifyParameter.Resource resource = body.getResource(); + // 5:回调报文解密 + AesUtil aesUtil = new AesUtil(WeChatPayParameter.v3Key.getBytes()); + // 解密后json字符串 + String decryptToString = aesUtil.decryptToString( + resource.getAssociated_data().getBytes(), + resource.getNonce().getBytes(), + resource.getCiphertext()); + log.info("微信退款回调信息:{}", decryptToString); + + WechatPayRefundNotifyResource refundNotifyResource = JSONObject.parseObject(decryptToString, WechatPayRefundNotifyResource.class); + if (refundNotifyResource == null) { + return null; + } + // 查询原支付信息,获取是订单结算退款还是余额退款 + String out_trade_no = refundNotifyResource.getOut_trade_no(); + String out_refund_no = refundNotifyResource.getOut_refund_no(); + String transaction_id = refundNotifyResource.getTransaction_id(); + String refund_id = refundNotifyResource.getRefund_id(); + + WxpayCallbackRecord wxpayCallbackRecord = wxpayCallbackRecordService.selectByOutTradeNo(out_trade_no); + if (wxpayCallbackRecord == null) { + log.info("查询原支付信息为空 OutTradeNo:{}", out_trade_no); + return null; + } + + String memberId = wxpayCallbackRecord.getMemberId(); + String orderCode = wxpayCallbackRecord.getOrderCode(); + // 退款金额 单位分 + int payer_refund = refundNotifyResource.getAmount().getPayer_refund(); + // 分转成元 + BigDecimal refundAmount = new BigDecimal(payer_refund).divide(new BigDecimal(100), 2, BigDecimal.ROUND_HALF_UP); + if (StringUtils.equals(wxpayCallbackRecord.getPayScenario(), ScenarioEnum.BALANCE.getValue())) { + // 这笔支付订单原来是充值余额的,退款成功了,需要扣掉会员的本金金额 + UpdateMemberBalanceDTO dto = new UpdateMemberBalanceDTO(); + dto.setMemberId(memberId); + dto.setUpdatePrincipalBalance(refundAmount); // 更新会员本金金额,单位元 + dto.setType(MemberWalletEnum.TYPE_OUT.getValue()); + dto.setSubType(MemberWalletEnum.SUBTYPE_USER_REFUND.getValue()); + memberBasicInfoService.updateMemberBalance(dto); + } + + // 保存微信退款回调信息 + WxpayRefundCallback record = WxpayRefundCallback.builder() + .memberId(memberId) + .orderCode(orderCode) + .outTradeNo(out_trade_no) + .outRefundNo(out_refund_no) + .transactionId(transaction_id) + .mchId(refundNotifyResource.getMchid()) + .refundId(refund_id) + .refundStatus(refundNotifyResource.getRefund_status()) + .successTime(refundNotifyResource.getSuccess_time()) + .userReceivedAccount(refundNotifyResource.getUser_received_account()) + .payerTotal(refundNotifyResource.getAmount().getPayer_total() + "") + .payerRefund(payer_refund + "") // 微信支付接收单位分 + .amountTotal(refundNotifyResource.getAmount().getTotal() + "") + .amountRefund(refundNotifyResource.getAmount().getRefund() + "") + .build(); + wxpayRefundCallbackService.insertSelective(record); + + // 余额支付订单 记录会员交易流水 + MemberTransactionRecord memberTransactionRecord = MemberTransactionRecord.builder() + .orderCode(orderCode) + .scenarioType(wxpayCallbackRecord.getPayScenario()) + .memberId(memberId) + .actionType(ActionTypeEnum.REVERSE.getValue()) + .payMode(PayModeEnum.PAYMENT_OF_WECHATPAY.getValue()) + .amount(refundAmount) // 记录会员交易流水,单位元 + .outTradeNo(out_trade_no) + .transactionId(transaction_id) + .outRefundNo(out_refund_no) + .refundId(refund_id) + .build(); + memberTransactionRecordService.insertSelective(memberTransactionRecord); + return null; + } + + /** + * 微信支付 申请退款接口 + */ + @Override + public WechatPayRefundResponse ApplyForWechatPayRefundV3(WechatPayRefundRequest request) throws JsonProcessingException { + ObjectMapper objectMapper = new ObjectMapper(); + String body = objectMapper.writeValueAsString(request); + log.info("申请退款的相关参数是:{}", body); + Map stringObjectMap = HttpUtils.doPostWexin(WeChatPayParameter.refundJsUrl, body); + log.info("申请退款的返回参数是:{}", stringObjectMap); + return JSON.parseObject(JSON.toJSONString(stringObjectMap), WechatPayRefundResponse.class); + } + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/WxpayCallbackRecordServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/WxpayCallbackRecordServiceImpl.java new file mode 100644 index 000000000..cb10f7639 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/WxpayCallbackRecordServiceImpl.java @@ -0,0 +1,86 @@ +package com.jsowell.pile.service.impl; + +import com.jsowell.pile.domain.WxpayCallbackRecord; +import com.jsowell.pile.mapper.WxpayCallbackRecordMapper; +import com.jsowell.pile.service.WxpayCallbackRecordService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; + +@Slf4j +@Service +public class WxpayCallbackRecordServiceImpl implements WxpayCallbackRecordService { + + @Resource + private WxpayCallbackRecordMapper wxpayCallbackRecordMapper; + + @Override + public int deleteByPrimaryKey(Integer id) { + return wxpayCallbackRecordMapper.deleteByPrimaryKey(id); + } + + @Override + public int insert(WxpayCallbackRecord record) { + return wxpayCallbackRecordMapper.insert(record); + } + + @Override + public int insertSelective(WxpayCallbackRecord record) { + return wxpayCallbackRecordMapper.insertSelective(record); + } + + @Override + public WxpayCallbackRecord selectByPrimaryKey(Integer id) { + return wxpayCallbackRecordMapper.selectByPrimaryKey(id); + } + + @Override + public int updateByPrimaryKeySelective(WxpayCallbackRecord record) { + return wxpayCallbackRecordMapper.updateByPrimaryKeySelective(record); + } + + @Override + public int updateByPrimaryKey(WxpayCallbackRecord record) { + return wxpayCallbackRecordMapper.updateByPrimaryKey(record); + } + + @Override + public WxpayCallbackRecord selectByOrderCode(String orderCode) { + WxpayCallbackRecord wxpayCallbackRecord = null; + try { + wxpayCallbackRecord = wxpayCallbackRecordMapper.selectByOrderCode(orderCode); + } catch (Exception e) { + log.error("根据订单号查询微信支付记录:{}", orderCode, e); + } + return wxpayCallbackRecord; + } + + @Override + public WxpayCallbackRecord selectByOutTradeNo(String outTradeNo) { + return wxpayCallbackRecordMapper.selectByOutTradeNo(outTradeNo); + } + + @Override + public List queryBalanceRechargeRecordOfTheLatestYear(String memberId) { + LocalDateTime now = LocalDateTime.now(); + LocalDate localDate = now.minusYears(1).toLocalDate(); + LocalTime localTime = now.toLocalTime(); + LocalDateTime lastYear = LocalDateTime.of(localDate, localTime); + // 查询最近一年的余额充值记录 + return wxpayCallbackRecordMapper.selectBalanceRechargeRecord(memberId, lastYear); + } + + public static void main(String[] args) { + LocalDateTime now = LocalDateTime.now(); + LocalDate localDate = now.minusYears(1).toLocalDate(); + LocalTime localTime = now.toLocalTime(); + LocalDateTime of = LocalDateTime.of(localDate, localTime); + System.out.println(of); + } + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/WxpayRefundCallbackServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/WxpayRefundCallbackServiceImpl.java new file mode 100644 index 000000000..fa2eed663 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/WxpayRefundCallbackServiceImpl.java @@ -0,0 +1,41 @@ +package com.jsowell.pile.service.impl; + +import com.jsowell.pile.domain.WxpayRefundCallback; +import com.jsowell.pile.mapper.WxpayRefundCallbackMapper; +import com.jsowell.pile.service.WxpayRefundCallbackService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +@Service +public class WxpayRefundCallbackServiceImpl implements WxpayRefundCallbackService { + + @Resource + private WxpayRefundCallbackMapper wxpayRefundCallbackMapper; + + @Override + public int deleteByPrimaryKey(Integer id) { + return wxpayRefundCallbackMapper.deleteByPrimaryKey(id); + } + + @Override + public int insertSelective(WxpayRefundCallback record) { + return wxpayRefundCallbackMapper.insertSelective(record); + } + + @Override + public WxpayRefundCallback selectByPrimaryKey(Integer id) { + return wxpayRefundCallbackMapper.selectByPrimaryKey(id); + } + + @Override + public int updateByPrimaryKeySelective(WxpayRefundCallback record) { + return wxpayRefundCallbackMapper.updateByPrimaryKeySelective(record); + } + + @Override + public int updateByPrimaryKey(WxpayRefundCallback record) { + return wxpayRefundCallbackMapper.updateByPrimaryKey(record); + } + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/transaction/dto/BillingTemplateTransactionDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/transaction/dto/BillingTemplateTransactionDTO.java new file mode 100644 index 000000000..37ba17081 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/transaction/dto/BillingTemplateTransactionDTO.java @@ -0,0 +1,32 @@ +package com.jsowell.pile.transaction.dto; + +import com.jsowell.pile.domain.PileBillingDetail; +import com.jsowell.pile.domain.PileBillingTemplate; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class BillingTemplateTransactionDTO { + // 计费模板基本信息 + private PileBillingTemplate billingTemplate; + + // 计费模板详情 + private List detailList; + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("billingTemplate", billingTemplate) + .append("detailList", detailList) + .toString(); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/transaction/dto/MemberTransactionDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/transaction/dto/MemberTransactionDTO.java new file mode 100644 index 000000000..27eb03399 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/transaction/dto/MemberTransactionDTO.java @@ -0,0 +1,18 @@ +package com.jsowell.pile.transaction.dto; + +import com.jsowell.pile.domain.MemberBasicInfo; +import com.jsowell.pile.domain.MemberWalletInfo; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MemberTransactionDTO { + private MemberBasicInfo memberBasicInfo; + + private MemberWalletInfo memberWalletInfo; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/transaction/dto/OrderTransactionDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/transaction/dto/OrderTransactionDTO.java new file mode 100644 index 000000000..87aa3c601 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/transaction/dto/OrderTransactionDTO.java @@ -0,0 +1,24 @@ +package com.jsowell.pile.transaction.dto; + +import com.jsowell.pile.domain.OrderBasicInfo; +import com.jsowell.pile.domain.OrderDetail; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class OrderTransactionDTO { + /** + * 订单基本信息 + */ + private OrderBasicInfo orderBasicInfo; + + /** + * 订单详情 + */ + private OrderDetail orderDetail; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/transaction/dto/PileTransactionDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/transaction/dto/PileTransactionDTO.java new file mode 100644 index 000000000..fa2bddf23 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/transaction/dto/PileTransactionDTO.java @@ -0,0 +1,20 @@ +package com.jsowell.pile.transaction.dto; + +import com.jsowell.pile.domain.PileBasicInfo; +import com.jsowell.pile.domain.PileConnectorInfo; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PileTransactionDTO { + private List pileBasicInfoList; + + private List pileConnectorInfoList; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/transaction/service/TransactionService.java b/jsowell-pile/src/main/java/com/jsowell/pile/transaction/service/TransactionService.java new file mode 100644 index 000000000..b617a8136 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/transaction/service/TransactionService.java @@ -0,0 +1,155 @@ +package com.jsowell.pile.transaction.service; + +import com.google.common.collect.Lists; +import com.jsowell.common.constant.CacheConstants; +import com.jsowell.common.core.redis.RedisCache; +import com.jsowell.common.util.StringUtils; +import com.jsowell.pile.mapper.MemberBasicInfoMapper; +import com.jsowell.pile.mapper.MemberWalletInfoMapper; +import com.jsowell.pile.mapper.OrderBasicInfoMapper; +import com.jsowell.pile.mapper.PileBasicInfoMapper; +import com.jsowell.pile.mapper.PileBillingTemplateMapper; +import com.jsowell.pile.mapper.PileConnectorInfoMapper; +import com.jsowell.pile.transaction.dto.BillingTemplateTransactionDTO; +import com.jsowell.pile.transaction.dto.MemberTransactionDTO; +import com.jsowell.pile.transaction.dto.OrderTransactionDTO; +import com.jsowell.pile.transaction.dto.PileTransactionDTO; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.Objects; + +/** + * 用来入库的service + */ +@Slf4j +@Service +public class TransactionService { + @Resource + private PileBasicInfoMapper pileBasicInfoMapper; + + @Resource + private PileConnectorInfoMapper pileConnectorInfoMapper; + + @Resource + private PileBillingTemplateMapper pileBillingTemplateMapper; + + @Autowired + private OrderBasicInfoMapper orderBasicInfoMapper; + + @Autowired + private MemberBasicInfoMapper memberBasicInfoMapper; + + @Autowired + private MemberWalletInfoMapper memberWalletInfoMapper; + + @Autowired + private RedisCache redisCache; + + /** + * 批量新增充电桩和充电桩接口 + * @param dto + * @return + */ + @Transactional(readOnly = false, propagation = Propagation.REQUIRED) + public int doCreatePileTransaction(PileTransactionDTO dto) { + int result = 0; + if (CollectionUtils.isNotEmpty(dto.getPileBasicInfoList())) { + result = pileBasicInfoMapper.batchInsertPileBasicInfo(dto.getPileBasicInfoList()); + } + if (CollectionUtils.isNotEmpty(dto.getPileConnectorInfoList())) { + pileConnectorInfoMapper.batchInsertConnectorInfo(dto.getPileConnectorInfoList()); + } + return result; + } + + /** + * 新增计费模板和详情 + */ + @Transactional(readOnly = false, propagation = Propagation.REQUIRED) + public void doCreateBillingTemplate(BillingTemplateTransactionDTO dto) { + log.info("新增计费模板和详情 param:{}", dto.toString()); + if (dto.getBillingTemplate() == null || CollectionUtils.isEmpty(dto.getDetailList())) { + return; + } + // 保存计费模板基本信息 + pileBillingTemplateMapper.insertPileBillingTemplate(dto.getBillingTemplate()); + // log.info("自增id:{}", dto.getBillingTemplate().getId()); + // 保存计费模板详情 + pileBillingTemplateMapper.batchPileBillingDetail(dto.getDetailList()); + } + + /** + * 更新计费模板 + * @param dto + */ + @Transactional(readOnly = false, propagation = Propagation.REQUIRED) + public void doUpdateBillingTemplate(BillingTemplateTransactionDTO dto) { + if (dto.getBillingTemplate() == null || CollectionUtils.isEmpty(dto.getDetailList())) { + return; + } + // 保存计费模板基本信息 + pileBillingTemplateMapper.updatePileBillingTemplate(dto.getBillingTemplate()); + // 计费模板 先删后插 + pileBillingTemplateMapper.deletePileBillingDetailByTemplateCode(dto.getBillingTemplate().getTemplateCode()); + // 保存计费模板详情 + pileBillingTemplateMapper.batchPileBillingDetail(dto.getDetailList()); + } + + /** + * 插入订单 + */ + @Transactional(readOnly = false, propagation = Propagation.REQUIRED) + public void doCreateOrder(OrderTransactionDTO dto) { + if (Objects.nonNull(dto.getOrderBasicInfo())) { + orderBasicInfoMapper.insertOrderBasicInfo(dto.getOrderBasicInfo()); + } + + if (Objects.nonNull(dto.getOrderDetail())) { + orderBasicInfoMapper.batchOrderDetail(Lists.newArrayList(dto.getOrderDetail())); + } + } + + /** + * 更新订单信息 + * @param dto + */ + @Transactional(readOnly = false, propagation = Propagation.REQUIRED) + public void doUpdateOrder(OrderTransactionDTO dto) { + String orderCode = null; + if (Objects.nonNull(dto.getOrderBasicInfo())) { + orderCode = dto.getOrderBasicInfo().getOrderCode(); + orderBasicInfoMapper.updateOrderBasicInfo(dto.getOrderBasicInfo()); + } + + if (Objects.nonNull(dto.getOrderDetail())) { + orderCode = dto.getOrderDetail().getOrderCode(); + orderBasicInfoMapper.updateOrderDetail(dto.getOrderDetail()); + } + // 清缓存 + if (StringUtils.isNotBlank(orderCode)) { + String redisKey = CacheConstants.GET_ORDER_INFO_BY_ORDER_CODE + orderCode; + redisCache.deleteObject(redisKey); + } + } + + /** + * 新建会员 + * @param dto + */ + @Transactional(readOnly = false, propagation = Propagation.REQUIRED) + public void createMember(MemberTransactionDTO dto) { + if (Objects.nonNull(dto.getMemberBasicInfo())) { + memberBasicInfoMapper.insertMemberBasicInfo(dto.getMemberBasicInfo()); + } + if (Objects.nonNull(dto.getMemberWalletInfo())) { + memberWalletInfoMapper.insertSelective(dto.getMemberWalletInfo()); + } + } + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/base/ConnectorInfoVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/base/ConnectorInfoVO.java new file mode 100644 index 000000000..52ddbdf8e --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/base/ConnectorInfoVO.java @@ -0,0 +1,44 @@ +package com.jsowell.pile.vo.base; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 枪口信息详情 + * + * @author JS-ZZA + * @date 2022/11/2 16:18 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ConnectorInfoVO { + /** + * 枪口号 + */ + private String connectorCode; + + /** + * 枪口编号 + */ + private String pileConnectorCode; + + /** + * 枪口状态 + */ + private String connectorStatus; + + /** + * 充电类型(1-快充,2-慢充) + */ + private String chargingType; + + /** + * 额定功率 + */ + private String ratedPower; + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/base/MerchantInfoVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/base/MerchantInfoVO.java new file mode 100644 index 000000000..a3498279d --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/base/MerchantInfoVO.java @@ -0,0 +1,33 @@ +package com.jsowell.pile.vo.base; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MerchantInfoVO { + /** + * 商户id + */ + private String merchantId; + + /** + * 商户名称 + */ + private String merchantName; + + /** + * 商户电话 + */ + private String merchantTel; + + /** + * 部门id + */ + private String deptId; + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/base/PileInfoVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/base/PileInfoVO.java new file mode 100644 index 000000000..d8cb381d8 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/base/PileInfoVO.java @@ -0,0 +1,52 @@ +package com.jsowell.pile.vo.base; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PileInfoVO { + /** + * 充电桩id + */ + private String pileId; + + /** + * 充电桩编号 + */ + private String pileSn; + + /** + * 所属运营商id + */ + private String merchantId; + + /** + * 所属运营商名称 + */ + private String merchantName; + + /** + * 所属站点id + */ + private String stationId; + + /** + * 额定电压 + */ + private String ratedVoltage; + + /** + * 额定电流 + */ + private String ratedCurrent; + + /** + * 额定功率 + */ + private String ratedPower; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/base/StationInfoVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/base/StationInfoVO.java new file mode 100644 index 000000000..b229e0727 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/base/StationInfoVO.java @@ -0,0 +1,87 @@ +package com.jsowell.pile.vo.base; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 返回给前端的站点信息vo + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class StationInfoVO { + /** + * 站点id + */ + private String stationId; + + /** + * 站点名称 + */ + private String stationName; + + /** + * 站点地址 + */ + private String stationAddress; + + /** + * 站点图片 + */ + private List stationImgList; + + /** + * 距离 单位千米 + */ + private String distance; + + /** + * 电费 每度单价 + */ + private String electricityPrice; + + /** + * 服务费 每度单价 + */ + private String servicePrice; + + /** + * 电费 + 服务费 每度单价 + */ + private String totalPrice; + + /** + * 快充枪口总数 + */ + private int fastTotal; + + /** + * 快充枪口空闲数 + */ + private int fastFree; + + /** + * 慢充枪口总数 + */ + private int slowTotal; + + /** + * 慢充枪口空闲数 + */ + private int slowFree; + + /** + * 经度 + */ + private String stationLng; + + /** + * 纬度 + */ + private String stationLat; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/BillingPriceVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/BillingPriceVO.java new file mode 100644 index 000000000..bd1e818c2 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/BillingPriceVO.java @@ -0,0 +1,50 @@ +package com.jsowell.pile.vo.uniapp; + +import com.jsowell.common.enums.ykc.BillingTimeEnum; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class BillingPriceVO { + /** + * 是否当前时间 + * 1-是 0-否 + */ + private String isCurrentTime; + + /** + * 开始时间 + */ + private String startTime; + + /** + * 结束时间 + */ + private String endTime; + + /** + * 电费 + */ + private String electricityPrice; + + /** + * 服务费 + */ + private String servicePrice; + + /** + * 总费用 + */ + private String totalPrice; + + /** + * 时段类型(1-尖时;2-峰时;3-平时;4-谷时) + * @see BillingTimeEnum + */ + private String timeType; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/CurrentTimePriceDetails.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/CurrentTimePriceDetails.java new file mode 100644 index 000000000..c15c2ed96 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/CurrentTimePriceDetails.java @@ -0,0 +1,42 @@ +package com.jsowell.pile.vo.uniapp; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CurrentTimePriceDetails { + /** + * 当前时间 + */ + private String dateTime; + + /** + * 站点id + */ + private String stationId; + + /** + * 计费模板编号 + */ + private String templateCode; + + /** + * 电费 每度单价 + */ + private String electricityPrice; + + /** + * 服务费 每度单价 + */ + private String servicePrice; + + /** + * 电费 + 服务费 每度单价 + */ + private String totalPrice; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/MemberVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/MemberVO.java new file mode 100644 index 000000000..3f447ced9 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/MemberVO.java @@ -0,0 +1,56 @@ +package com.jsowell.pile.vo.uniapp; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +/** + * 用户信息VO + * + * @author JS-ZZA + * @date 2022/11/19 13:31 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class MemberVO { + /** + * 会员Id + */ + private String memberId; + + /** + * 状态 + */ + private String status; + + /** + * 昵称 + */ + private String nickName; + + /** + * 手机号码 + */ + private String mobileNumber; + + /** + * 本金金额 + */ + private BigDecimal principalBalance; + + /** + * 赠送金额 + */ + private BigDecimal giftBalance; + + /** + * 总账户余额 + */ + private BigDecimal totalAccountAmount; + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/MemberWalletLogVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/MemberWalletLogVO.java new file mode 100644 index 000000000..0c6c60709 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/MemberWalletLogVO.java @@ -0,0 +1,47 @@ +package com.jsowell.pile.vo.uniapp; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +/** + * 会员钱包余额明细相关 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MemberWalletLogVO { + /** + * 会员id + */ + private String memberId; + + /** + * 交易类型 1-进账;2-出账 + */ + private String type; + + /** + * 子类型 10-充值, 11-赠送, 12-订单结算退款,20-后管扣款, 21-订单付款, 22-用户退款 + */ + private String subType; + + /** + * 出账/入账金额 + */ + private BigDecimal amount; + + /** + * 交易时间 + */ + private String transactionTime; + + /** + * 余额类型(1-本金,2-赠送) + */ + private String category; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/OrderVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/OrderVO.java new file mode 100644 index 000000000..b9fc40895 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/OrderVO.java @@ -0,0 +1,84 @@ +package com.jsowell.pile.vo.uniapp; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 订单信息VO + * + * @author JS-ZZA + * @date 2022/11/24 14:08 + */ +@Data +public class OrderVO { + /** + * 订单号 + */ + private String orderCode; + + /** + * 桩编号 + */ + private String pileSn; + + /** + * 枪口号 + */ + private String connectorCode; + + /** + * 枪口编码 + */ + private String pileConnectorCode; + + /** + * 站点名称 + */ + private String stationName; + + /** + * 充电度数 + */ + private String chargingDegree; + + /** + * 订单金额 + */ + private BigDecimal orderAmount; + + /** + * 订单状态 + */ + private String orderStatus; + + /** + * 订单异常原因 + */ + private String reason; + + /** + * 支付状态 + */ + private String payStatus; + + /** + * 用户支付金额 + */ + private BigDecimal payAmount; + + /** + * 充电开始时间 + */ + private String startTime; + + /** + * 充电结束时间 + */ + private String endTime; + + /** + * 充电时长 + */ + private String chargingTime; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/PersonPileConnectorSumInfoVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/PersonPileConnectorSumInfoVO.java new file mode 100644 index 000000000..9d3883138 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/PersonPileConnectorSumInfoVO.java @@ -0,0 +1,38 @@ +package com.jsowell.pile.vo.uniapp; + +import lombok.Data; + +/** + * 个人桩枪口累计数据 + * + * @author JS-ZZA + * @date 2023/2/25 8:42 + */ +@Data +public class PersonPileConnectorSumInfoVO { + private String memberId; + + private String startTime; + + private String endTime; + + /** + * 充电开始时间 + */ + private String chargeStartTime; + + /** + * 充电结束时间 + */ + private String chargeEndTime; + + /** + * 累计充电量 + */ + private String sumChargingDegree; + + /** + * 累计充电时长 + */ + private String sumChargingTime; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/PersonPileRealTimeVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/PersonPileRealTimeVO.java new file mode 100644 index 000000000..88c43f441 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/PersonPileRealTimeVO.java @@ -0,0 +1,48 @@ +package com.jsowell.pile.vo.uniapp; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 个人桩查询枪口实时数据 + * + * @author JS-ZZA + * @date 2023/2/23 16:21 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class PersonPileRealTimeVO { + /** + * 启动时间 + */ + private String startTime; + + /** + * 已充度数 + */ + private String chargingDegree; + + /** + * 充电时长 + */ + private String chargingTime; + + /** + * 实时电流 + */ + private String instantCurrent; + + /** + * 实时电压 + */ + private String instantVoltage; + + /** + * 实时功率 + */ + private String instantPower; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/PersonalPileInfoVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/PersonalPileInfoVO.java new file mode 100644 index 000000000..806ffd747 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/PersonalPileInfoVO.java @@ -0,0 +1,62 @@ +package com.jsowell.pile.vo.uniapp; + +import lombok.Data; + +/** + * 个人桩信息VO + * + * @author JS-ZZA + * @date 2023/2/21 16:53 + */ +@Data +public class PersonalPileInfoVO { + /** + * 桩编码 + */ + private String pileSn; + + /** + * 会员id + */ + private String memberId; + + /** + * 身份类型(1-管理员;2-普通用户) + */ + private String type; + + /** + * 桩状态 + */ + private String pileStatus; + + /** + * 型号 + */ + private String modelName; + + /** + * 额定功率 + */ + private String ratedPower; + + /** + * 额定电流 + */ + private String ratedCurrent; + + /** + * 额定电压 + */ + private String ratedVoltage; + + /** + * 充电类型(1-快充;2-慢充) + */ + private String speedType; + + /** + * 枪口数量 + */ + private String connectorNum; +} \ No newline at end of file diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/PileConnectorDetailVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/PileConnectorDetailVO.java new file mode 100644 index 000000000..9511f68b1 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/PileConnectorDetailVO.java @@ -0,0 +1,54 @@ +package com.jsowell.pile.vo.uniapp; + +import lombok.Data; + +/** + * 充电枪详情VO + * 包含充电桩和枪口的一些信息 + * 可以作为公共对象 + */ +@Data +public class PileConnectorDetailVO { + /** + * 充电桩id + */ + private String pileId; + + /** + * 充电桩编号 + */ + private String pileSn; + + /** + * 站点id + */ + private String stationId; + + /** + * 枪口id + */ + private String connectorId; + + /** + * 充电枪编号 + */ + private String pileConnectorCode; + + /** + * 枪口状态 + * 状态 0:离网 (默认);1:空闲;2:占用(未充电);3:占用(充电中);4:占用(预约锁定) ;255:故障 + */ + private String connectorStatus; + + /** + * 经营类型(1-运营桩;2-个人桩) + */ + private String businessType; + + /** + * 软件协议(1-云快充;2-永联) + */ + private String softwareProtocol; + + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/PileConnectorVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/PileConnectorVO.java new file mode 100644 index 000000000..422fb151a --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/PileConnectorVO.java @@ -0,0 +1,39 @@ +package com.jsowell.pile.vo.uniapp; + +import com.jsowell.pile.vo.base.ConnectorInfoVO; +import com.jsowell.pile.vo.base.MerchantInfoVO; +import com.jsowell.pile.vo.base.PileInfoVO; +import com.jsowell.pile.vo.web.PileStationVO; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PileConnectorVO { + + // 充电枪信息列表 + private List connectorInfoList; + + // 计费模板信息 + // private BillingTemplateVO billingTemplate; + + // 充电桩信息 + private PileInfoVO pileInfo; + + // 运营商信息 + private MerchantInfoVO merchantInfo; + + // 站点信息 + private PileStationVO stationInfo; + + /** + * 价格列表 + */ + private List billingPriceList; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/PileDetailVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/PileDetailVO.java new file mode 100644 index 000000000..438d010ff --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/PileDetailVO.java @@ -0,0 +1,41 @@ +package com.jsowell.pile.vo.uniapp; + +import com.jsowell.pile.vo.base.ConnectorInfoVO; +import com.jsowell.pile.vo.web.BillingTemplateVO; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PileDetailVO { + /** + * 站点id + */ + private String stationId; + + /** + * 站点名称 + */ + private String stationName; + + /** + * 站点电话 + */ + private String stationTel; + + /** + * 枪口列表 + */ + private List connectorList; + + /** + * 计费模板信息 + */ + private BillingTemplateVO billingTemplate; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/SendMessageVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/SendMessageVO.java new file mode 100644 index 000000000..9a7a8a6a0 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/SendMessageVO.java @@ -0,0 +1,55 @@ +package com.jsowell.pile.vo.uniapp; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * uniapp发送消息VO + * + * @author JS-ZZA + * @date 2023/2/15 10:37 + */ +@Data +public class SendMessageVO { + /** + * 订单号 + */ + public String orderCode; + + /** + * 站点名称 + */ + public String stationName; + + /** + * 充电开始时间 + */ + public String chargeStartTime; + + /** + * 站点Id + */ + public String stationId; + + /** + * 订单金额 + */ + public String orderAmount; + + /** + * 用户openId + */ + public String openId; + + /** + * 充电结束时间 + */ + public String chargeStopTime; + + /** + * 充电停止原因 + */ + public String stopReason; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/UniAppOrderVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/UniAppOrderVO.java new file mode 100644 index 000000000..705683c85 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/UniAppOrderVO.java @@ -0,0 +1,128 @@ +package com.jsowell.pile.vo.uniapp; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class UniAppOrderVO { + /** + * 订单编号 + */ + private String orderCode; + + /** + * 桩编号 + */ + private String pileSn; + + /** + * 枪口号 + */ + private String connectorCode; + + /** + * 枪口编码 + */ + private String pileConnectorCode; + + /** + * 订单状态 + */ + private String orderStatus; + + /** + * 订单状态描述 + */ + private String orderStatusDescribe; + + /** + * 当前SOC + */ + private String SOC; + + /** + * 枪口状态 + */ + private String pileConnectorStatus; + + /** + * 充电金额 + */ + private String chargingAmount; + + /** + * 充电电量 + */ + private String chargingDegree; + + /** + * 充电时长 + */ + private String sumChargingTime; + + /** + * 剩余时长 + */ + private String timeRemaining; + + /** + * 实时电流 + */ + private String outputCurrent; + + /** + * 实时电压 + */ + private String outputVoltage; + + /** + * 实时充电功率 + */ + private String outputPower; + + /** + * 实时温度 + */ + private String batteryMaxTemperature; + + /** + * 充电实时数据列表 + */ + private List chargingDataList; + + @Data + public static class ChargingData { + private String dateTime; + /** + * 输出电压 + */ + private String outputVoltage; + + /** + * 输出电流 + */ + private String outputCurrent; + + /** + * 功率 + */ + private String power; + + /** + * SOC 待机置零;交流桩置零 + */ + private String SOC; + + /** + * 电池组最高温度 + */ + private String batteryMaxTemperature; + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/BillingDetailVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/BillingDetailVO.java new file mode 100644 index 000000000..bf8962b71 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/BillingDetailVO.java @@ -0,0 +1,27 @@ +package com.jsowell.pile.vo.web; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class BillingDetailVO { + // 时段类型(1-尖时;2-峰时;3-平时;4-谷时) + private String timeType; + + // 电费(每度单价) + private BigDecimal electricityPrice; + + // 服务费(每度单价) + private BigDecimal servicePrice; + + // 适用时间段 + private List applyTime; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/BillingTemplateVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/BillingTemplateVO.java new file mode 100644 index 000000000..3d15d66ba --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/BillingTemplateVO.java @@ -0,0 +1,150 @@ +package com.jsowell.pile.vo.web; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.jsowell.common.util.StringUtils; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class BillingTemplateVO { + // 计费模板id + private String templateId; + + // 计费模板编号 + private String templateCode; + + // 计费模板名称 + private String templateName; + + // 计费模板备注 + private String remark; + + // 发布时间 + private String publishTime; + + // 设备类型 1-电动汽车桩;2-电动自行车桩 + private String deviceType; + + // 尖时段电费 + private BigDecimal sharpElectricityPrice; + // 尖时段服务费 + private BigDecimal sharpServicePrice; + // 尖时段时间 逗号分割的时段段 + private String sharpApplyDate; + + // 峰时段电费 + private BigDecimal peakElectricityPrice; + // 峰时段电费 + private BigDecimal peakServicePrice; + // 峰时段时间 逗号分割的时段段 + private String peakApplyDate; + + // 平时段电费 + private BigDecimal flatElectricityPrice; + // 平时段服务费 + private BigDecimal flatServicePrice; + // 平时段时间 逗号分割的时段段 + private String flatApplyDate; + + // 谷时段电费 + private BigDecimal valleyElectricityPrice; + // 谷时段服务费 + private BigDecimal valleyServicePrice; + // 平时段时间 逗号分割的时段段 + private String valleyApplyDate; + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("templateId", templateId) + .append("templateCode", templateCode) + .append("templateName", templateName) + .append("publishTime", publishTime) + .append("remark", remark) + .append("deviceType", deviceType) + .append("sharpElectricityPrice", sharpElectricityPrice) + .append("sharpServicePrice", sharpServicePrice) + .append("sharpApplyDate", sharpApplyDate) + .append("peakElectricityPrice", peakElectricityPrice) + .append("peakServicePrice", peakServicePrice) + .append("peakApplyDate", peakApplyDate) + .append("flatElectricityPrice", flatElectricityPrice) + .append("flatServicePrice", flatServicePrice) + .append("flatApplyDate", flatApplyDate) + .append("valleyElectricityPrice", valleyElectricityPrice) + .append("valleyServicePrice", valleyServicePrice) + .append("valleyApplyDate", valleyApplyDate) + .toString(); + } + + public Map> getTimeMap() { + Map> resultMap = Maps.newHashMap(); + if (StringUtils.isNotBlank(this.getSharpApplyDate())) { + resultMap.put(0x00, Lists.newArrayList(this.getSharpApplyDate().split(","))); + } + if (StringUtils.isNotBlank(this.getPeakApplyDate())) { + resultMap.put(0x01, Lists.newArrayList(this.getPeakApplyDate().split(","))); + } + if (StringUtils.isNotBlank(this.getFlatApplyDate())) { + resultMap.put(0x02, Lists.newArrayList(this.getFlatApplyDate().split(","))); + } + if (StringUtils.isNotBlank(this.getValleyApplyDate())) { + resultMap.put(0x03, Lists.newArrayList(this.getValleyApplyDate().split(","))); + } + return resultMap; + } + + public List getBillingDetailList() { + List resultList = Lists.newArrayList(); + if (StringUtils.isNotBlank(this.getSharpApplyDate())) { + List list = Lists.newArrayList(this.getSharpApplyDate().split(",")); + resultList.add(BillingDetailVO.builder() + .timeType("1") + .applyTime(list) + .electricityPrice(this.sharpElectricityPrice) + .servicePrice(this.sharpServicePrice) + .build()); + } + if (StringUtils.isNotBlank(this.getPeakApplyDate())) { + List list = Lists.newArrayList(this.getPeakApplyDate().split(",")); + resultList.add(BillingDetailVO.builder() + .timeType("2") + .applyTime(list) + .electricityPrice(this.peakElectricityPrice) + .servicePrice(this.peakServicePrice) + .build()); + } + if (StringUtils.isNotBlank(this.getFlatApplyDate())) { + List list = Lists.newArrayList(this.getFlatApplyDate().split(",")); + resultList.add(BillingDetailVO.builder() + .timeType("3") + .applyTime(list) + .electricityPrice(this.flatElectricityPrice) + .servicePrice(this.flatServicePrice) + .build()); + } + if (StringUtils.isNotBlank(this.getValleyApplyDate())) { + List list = Lists.newArrayList(this.getValleyApplyDate().split(",")); + resultList.add(BillingDetailVO.builder() + .timeType("4") + .applyTime(list) + .electricityPrice(this.valleyElectricityPrice) + .servicePrice(this.valleyServicePrice) + .build()); + } + return resultList; + } + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/EchoBillingTemplateVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/EchoBillingTemplateVO.java new file mode 100644 index 000000000..a28e8342e --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/EchoBillingTemplateVO.java @@ -0,0 +1,10 @@ +package com.jsowell.pile.vo.web; + +import com.jsowell.pile.dto.CreateOrUpdateBillingTemplateDTO; + +/** + * 用于计费模板修改页面回显的vo + * 和创建计费模板的结构一样 + */ +public class EchoBillingTemplateVO extends CreateOrUpdateBillingTemplateDTO { +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/IndexGeneralSituationVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/IndexGeneralSituationVO.java new file mode 100644 index 000000000..0412a5b5e --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/IndexGeneralSituationVO.java @@ -0,0 +1,37 @@ +package com.jsowell.pile.vo.web; + +import lombok.Data; + +/** + * 首页基础信息数据展示VO + * + * @author JS-ZZA + * @date 2023/2/3 10:36 + */ +@Data +public class IndexGeneralSituationVO { + /** + * 总充电电量 + */ + private String totalChargingDegree; + + /** + * 总充电费用 + */ + private String totalChargingAmount; + + /** + * 总充电笔数 + */ + private String totalChargingQuantity; + + /** + * 总设备数量 + */ + private String totalPileQuantity; + + /** + * 总客户余额 + */ + private String totalMemberAmount; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/IndexOrderInfoVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/IndexOrderInfoVO.java new file mode 100644 index 000000000..3b211ab7a --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/IndexOrderInfoVO.java @@ -0,0 +1,48 @@ +package com.jsowell.pile.vo.web; + +import lombok.Data; + +/** + * 后管首页订单信息VO + * + * @author JS-ZZA + * @date 2023/2/4 9:09 + */ +@Data +public class IndexOrderInfoVO { + /** + * 日期 + */ + private String date; + + /** + * 总用电量 + */ + private String totalElectricity; + + /** + * 总订单金额 + */ + private String totalOrderAmount; + + /** + * 尖时段总用电量 + */ + private String totalSharpUsedElectricity; + + /** + * 峰时段总用电量 + */ + private String totalPeakUsedElectricity; + + /** + * 平时段总用电量 + */ + private String totalFlatUsedElectricity; + + /** + * 谷时段总用电量 + */ + private String totalValleyUsedElectricity; + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/MemberTransactionVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/MemberTransactionVO.java new file mode 100644 index 000000000..da939fc67 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/MemberTransactionVO.java @@ -0,0 +1,70 @@ +package com.jsowell.pile.vo.web; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MemberTransactionVO { + /** + * 充电订单号 + */ + private String orderCode; + + /** + * 场景类型(order, balance) + */ + private String scenarioType; + + /** + * 会员id + */ + private String memberId; + + /** + * 操作类型(forward-正向, reverse-逆向) + */ + private String actionType; + + /** + * 支付类型(wx, balance) + */ + private String payMode; + + /** + * 金额 + */ + private BigDecimal amount; + + /** + * 外部商户订单号 + */ + private String outTradeNo; + + /** + * 外部支付订单号 + */ + private String transactionId; + + /** + * 外部退款单号 + */ + private String outRefundNo; + + /** + * 外部支付退款单号 + */ + private String refundId; + + /** + * 创建时间 + */ + private LocalDateTime createTime; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/OrderDetailInfoVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/OrderDetailInfoVO.java new file mode 100644 index 000000000..ecb07ba9c --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/OrderDetailInfoVO.java @@ -0,0 +1,73 @@ +package com.jsowell.pile.vo.web; + +import com.jsowell.pile.vo.base.PileInfoVO; +import com.jsowell.pile.vo.uniapp.MemberVO; +import lombok.Data; + +import java.util.List; + +/** + * 订单详情信息VO + */ +@Data +public class OrderDetailInfoVO { + // 订单信息 + private OrderInfo orderInfo; + + // 用户信息 + private MemberVO memberInfo; + + // 设备信息 + private PileInfoVO pileInfo; + + // 支付信息 + private List payRecordList; + + // 枪口信息 + private List realTimeMonitorDataList; + + // 订单相关实时数据 + private OrderRealTimeInfo orderRealTimeInfo; + + @Data + public static class OrderInfo { + private String orderCode; // 订单编号 + private String orderStatus; // 订单状态 + private String startTime; // 充电开始时间 + private String endTime; // 充电结束时间 + private String stopReasonMsg; // 停止原因 + private String createTime; // 订单创建时间 + private String startSOC; // 开始SOC + private String endSOC; // 结束SOC + } + + @Data + public static class PayRecord { + private String payAmount; // 支付金额 + private String payStatus; // 支付状态 + private String payMode; // 支付方式(1-余额支付;2-微信支付;3-支付宝支付) + private String payModeDesc; // 支付方式描述 + private String payTime; // 支付时间 + private String outTradeNo; // 微信商户订单号 real_time_monitor_data + private String transactionId; // 微信支付订单号 + } + + @Data + public static class RealTimeMonitorData{ + private String instantCurrent; // 实时电流 + private String instantVoltage; // 实时电压 + private String instantPower; // 实时功率 + private String SOC; + private String time; + } + + @Data + public static class OrderRealTimeInfo{ + private String orderAmount; // 订单金额 + private String totalElectricityAmount; // 总电费 + private String totalServiceAmount; // 总服务费 + private String chargedDegree; // 已充度数 + private String SOC; // soc + private String chargingTime; // 充电时长(分钟) + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/OrderListVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/OrderListVO.java new file mode 100644 index 000000000..d9789d013 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/OrderListVO.java @@ -0,0 +1,159 @@ +package com.jsowell.pile.vo.web; + +import com.jsowell.common.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 后管订单列表页面 + * + * @author JS-ZZA + * @date 2022/12/1 9:59 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class OrderListVO { + private String id; + + /** + * 订单号 + */ + @Excel(name = "订单号") + private String orderCode; + + /** + * 订单状态 + */ + // @Excel(name = "订单状态") + private String orderStatus; + + /** + * 订单状态描述 + */ + @Excel(name = "订单状态描述") + private String orderStatusDescribe; + + /** + * 会员id + */ + @Excel(name = "会员id") + private String memberId; + + /** + * 昵称 + */ + @Excel(name = "昵称") + private String nickName; + + /** + * 手机号码 + */ + @Excel(name = "手机号码") + private String mobileNumber; + + /** + * 站点id + */ + // @Excel(name = "站点id") + private String stationId; + + /** + * 站点名称 + */ + @Excel(name = "站点名称") + private String stationName; + + /** + * 桩编码 + */ + @Excel(name = "充电桩桩编码") + private String pileSn; + + /** + * 枪口号 + */ + @Excel(name = "枪口号") + private String connectorCode; + + /** + * 桩枪口编号 + */ + @Excel(name = "充电桩枪口编号") + private String pileConnectorCode; + + /** + * 启动方式(0-后管启动;1-用户app启动) + */ + @Excel(name = "启动方式", dictType = "start_mode") + private String startMode; + + /** + * 支付方式(1-余额支付;3-白名单支付;4-微信支付;5-支付宝支付) + */ + @Excel(name = "支付方式", dictType = "pay_mode") + private String payMode; + + /** + * 支付状态 + */ + @Excel(name = "支付状态", dictType = "pay_status") + private String payStatus; + + /** + * 起始soc + */ + @Excel(name = "起始soc", suffix = "%") + private String startSoc; + + /** + * 终止soc + */ + @Excel(name = "终止soc", suffix = "%") + private String endSoc; + + /** + * 开始充电时间 + */ + @Excel(name = "开始充电时间") + private String chargeStartTime; + + /** + * 结束充电时间 + */ + @Excel(name = "结束充电时间") + private String chargeEndTime; + + /** + * 支付金额 + */ + @Excel(name = "支付金额") + private String payAmount; + + /** + * 订单金额 + */ + @Excel(name = "订单金额") + private String orderAmount; + + /** + * 创建日期 + */ + @Excel(name = "创建日期") + private String createTime; + + /** + * 充电度数 + */ + @Excel(name = "充电度数") + private String chargingDegree; + + /** + * 微信商户单号 + */ + @Excel(name = "微信商户单号") + private String outTradeNo; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/OrderRealTimeInfoVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/OrderRealTimeInfoVO.java new file mode 100644 index 000000000..4f9389575 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/OrderRealTimeInfoVO.java @@ -0,0 +1,37 @@ +package com.jsowell.pile.vo.web; + +import lombok.Data; + +/** + * 订单实时数据(后管订单详情页使用) + * + * @author JS-ZZA + * @date 2023/2/18 11:43 + */ +@Data +public class OrderRealTimeInfoVO { + /** + * 订单金额 + */ + private String orderAmount; + + /** + * 总电费 + */ + private String totalElectricityAmount; + + /** + * 总服务费 + */ + private String totalServiceAmount; + + /** + * 已充度数 + */ + private String chargedDegree; + + /** + * soc + */ + private String SOC; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/OrderTotalDataVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/OrderTotalDataVO.java new file mode 100644 index 000000000..dbc38956c --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/OrderTotalDataVO.java @@ -0,0 +1,23 @@ +package com.jsowell.pile.vo.web; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class OrderTotalDataVO { + // 开始时间 + private String dateDescription; + + // 总消费金额 + private BigDecimal sumOrderAmount; + + // 总用电量 + private BigDecimal sumUsedElectricity; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/PileCommunicationLogVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/PileCommunicationLogVO.java new file mode 100644 index 000000000..86b54ef0d --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/PileCommunicationLogVO.java @@ -0,0 +1,39 @@ +package com.jsowell.pile.vo.web; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 桩通讯日志 + * + * @author JS-ZZA + * @date 2023/1/12 14:57 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class PileCommunicationLogVO { + /** + * 桩号 + */ + private String pileSn; + + /** + * 时间 + */ + private String createTime; + + /** + * 帧类型 + */ + private String frameType; + + /** + * 描述 + */ + private String description; + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/PileConnectorInfoVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/PileConnectorInfoVO.java new file mode 100644 index 000000000..3f5fa89cb --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/PileConnectorInfoVO.java @@ -0,0 +1,159 @@ +package com.jsowell.pile.vo.web; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +/** + * 查询充电枪返回前台参数 + * + * @author JS-ZZA + * @date 2022/8/31 16:40 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PileConnectorInfoVO { + /** + * 充电枪口id + */ + private String connectorId; + + /** + * 枪口编号,由充电桩SN+01生成 + */ + private String pileConnectorCode; + + /** + * 经营类型(1-运营桩;2-个人桩) + */ + private String businessType; + + /** + * 充电二维码 + */ + private String connectorQrCodeUrl; + + /** + * 状态 0:离网 (默认);1:空闲;2:占用(未充电);3:占用(充电中);4:占用(预约锁定) ;255:故障 + */ + private Integer status; + + /** + * 站点id + */ + private String stationId; + + /** + * 运营商id + */ + private String merchantId; + + /** + * 运营商名称 + */ + private String merchantName; + + /** + * 充电桩编号 + */ + private String pileSn; + + /** + * 类型 + * 1-直流接口 汽车桩+快充 + * 2-交流接口 汽车桩+慢充 + * 3-插座接口 电单车桩 + */ + private String type; + + /** + * 即时功率 + */ + private BigDecimal instantPower; + + /** + * 电量 + */ + private BigDecimal electricity; + + /** + * SOC + */ + private String SOC; + + /** + * 设备订单号 + */ + private String equipmentOrderNum; + + /** + * 平台订单 + */ + private String orderCode; + + /** + * --------------------------------- + * 充电时长 + */ + private String chargingTime; + + /** + * 电压 + */ + private BigDecimal voltage; + + /** + * 电流 + */ + private BigDecimal current; + + /** + * 温度 + */ + private String gunLineTemperature; + + /** + * 用户信息 + */ + private String userInfo; + + /** + * 订单id + */ + private String orderId; + + /** + * 车牌号 + */ + private String carNo; + + /** + * 站点名称 + */ + private String stationName; + + /** + * 已充金额 + */ + private BigDecimal chargingAmount; + + /** + * 充电度数 + */ + private BigDecimal chargingDegree; + + /** + * 桩额定功率 + */ + private String ratedPower; + + /** + * 剩余时间 + */ + private String timeRemaining; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/PileDetailVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/PileDetailVO.java new file mode 100644 index 000000000..efe48b351 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/PileDetailVO.java @@ -0,0 +1,111 @@ +package com.jsowell.pile.vo.web; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 返回前端参数 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PileDetailVO { + /** + * 桩id + */ + private String pileId; + + /** + * sn号 + */ + private String pileSn; + + /** + * 状态 0-未知;1-在线;2-离线;3-故障 + */ + private String status; + + /** + * 站点名称 + */ + private String stationName; + + /** + * 站点id + */ + private String stationId; + + /** + * 运营商名称 + */ + private String merchantName; + + /** + * 运营商id + */ + private String merchantId; + + /** + * 设备类型(1-快充,2-慢充,3-电单车) + */ + private String pileType; + + /** + * 枪数量 + */ + private Integer gunNum; + + /** + * 接口标准 + */ + private String interfaceStandard; + + /** + * 额定功率 + */ + private Integer ratedPower; + + /** + * 额定电流 + */ + private Integer ratedCurrent; + + /** + * sim卡号 + */ + private String ICCID; + + /** + * sim卡商 + */ + private String simSupplier; + + /** + * sim卡运营商 + */ + private String operator; + + /** + * 注册时间 + */ + private String registrationTime; + + /** + * 到期时间 + */ + private String expireTime; + + /** + * 使用车辆描述 + */ + private String matchCars; + + /** + * 二维码字符串 + */ + private String qrCodeURL; + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/PileModelInfoVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/PileModelInfoVO.java new file mode 100644 index 000000000..d2664d4ef --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/PileModelInfoVO.java @@ -0,0 +1,88 @@ +package com.jsowell.pile.vo.web; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * + * + * @author JS-ZZA + * @date 2022/10/21 14:29 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class PileModelInfoVO { + /** + * 桩编号 + */ + private String pileSn; + + /** + * 型号id + */ + private String modelId; + + /** + * 型号名称 + */ + private String modelName; + + /** + * 额定功率 + */ + private String ratedPower; + + /** + * 额定电流 + */ + private String ratedCurrent; + + /** + * 额定电压 + */ + private String ratedVoltage; + + /** + * 充电类型 + */ + private String speedType; + + /** + * 充电桩类型(1-汽车桩,2-电单车) + */ + private String chargerPileType; + + /** + * 充电枪数量 + */ + private String connectorNum; + + /** + * 充电接口标准 + */ + private String interfaceStandard; + + /** + * 创建人 + */ + private String createBy; + + /** + * 创建时间 + */ + private String createTime; + + /** + * 更新人 + */ + private String updateBy; + + /** + * 更新时间 + */ + private String updateTime; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/PileStationVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/PileStationVO.java new file mode 100644 index 000000000..4d256513d --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/PileStationVO.java @@ -0,0 +1,150 @@ +package com.jsowell.pile.vo.web; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 站点管理列表反参 + * + * @author JS-ZZA + * @date 2022/9/1 13:28 + */ +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PileStationVO { + /** + * 站点id + */ + private String id; + + /** + * 站点名称 + */ + private String stationName; + + /** + * 省市辖区编码 + */ + private String areaCode; + + /** + * 地区 + */ + private String area; + + /** + * 地址 + */ + private String address; + + /** + * 充电设备数量 + */ + private Integer pileNum; + + /** + * 运营商ID + */ + private String merchantId; + + /** + * 运营商名称 + */ + private String merchantName; + + /** + * 运营商管理员 + */ + private String merchantAdminName; + + /** + * 站点状态 + */ + private Integer stationStatus; + + /** + * 站点类型 + */ + private String stationType; + + /** + * 创建时间 + */ + private String createTime; + + /** + * ---------------------------- + * 站点电话 + */ + private String stationTel; + + /** + * 适用车型描述 + */ + private String matchCars; + + private List selectMatchCars; + + /** + * 经度 + */ + private String stationLng; + + /** + * 纬度 + */ + private String stationLat; + + /** + * 建设场所 + */ + private String construction; + + /** + * 营业时间描述 + */ + private String businessHours; + + /** + * 组织结构代码 + */ + private String organizationCode; + + /** + * 是否对外开放 + */ + private String publicFlag; + + /** + * 是否营业中 + */ + private String openFlag; + + /** + * 当前经纬度距离 + */ + private String distance; + + /** + * 站点照片 使用英文逗号(,)拼接 + */ + private String pictures; + + /** + * 电费 + */ + private BigDecimal electricityPrice; + + /** + * 服务费 + */ + private BigDecimal servicePrice; + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/SimCardInfoVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/SimCardInfoVO.java new file mode 100644 index 000000000..8c05ca13d --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/SimCardInfoVO.java @@ -0,0 +1,63 @@ +package com.jsowell.pile.vo.web; + +import lombok.Data; + +/** + * sim卡信息VO + * + * @author JS-ZZA + * @date 2022/12/3 10:31 + */ +@Data +public class SimCardInfoVO { + /** + * sim卡Id + */ + private String id; + + /** + * 套餐名称 + */ + private String name; + + /** + * 桩编码 + */ + private String pileSn; + + /** + * ICCID + */ + private String iccId; + + /** + * sim卡状态 (0-沉默;1-正在使用;2-正常使用) + */ + private String simCardStatus; + + /** + * 总计流量 + */ + private String totalData; + + /** + * 剩余流量 + */ + private String surplusData; + + /** + * 到期时间 + */ + private String expireTime; + + /** + * 运营商 + */ + private String operator; + + /** + * Sim卡供应商 + */ + private String simSupplier; + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/SimCardVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/SimCardVO.java new file mode 100644 index 000000000..e6e725502 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/SimCardVO.java @@ -0,0 +1,84 @@ +package com.jsowell.pile.vo.web; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.math.BigDecimal; + +/** + * Sim卡相关信息VO + * 1卡的运营商 2运营商(移动、联通)3状态 4到期日期 5月充量/剩余流量 6续费 + * + * @author JS-ZZA + * @date 2022/12/7 11:00 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SimCardVO { + /** + * 卡号 + */ + private String iccId; + + /** + * 卡商 + */ + private String simCardFactory; + + /** + * 运营商 移动/电信/联通/其他 + */ + private String simCardOperator; + + /** + * sim卡状态 0-正常,1-强制断网,2-客户断网,3-超套停,4-服务结束,5-提请销卡,6-销卡, (XunZhong) + * 7-未开卡,8-沉默期,9-已停机,10-待激活,11-已回收,12-未知(WuLian) + */ + private String simCardStatus; + + /** + * 套餐名称 + */ + private String name; + + /** + * 过期时间 + */ + private String expiredTime; + + /** + * 月充量 + */ + private BigDecimal packageCapacity; + + /** + * 已用流量 + */ + private BigDecimal usedFlowRate; + + /** + * 剩余流量 + */ + private BigDecimal residualFlowRate; + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("iccId", iccId) + .append("simCardFactory", simCardFactory) + .append("simCardOperator", simCardOperator) + .append("simCardStatus", simCardStatus) + .append("name", name) + .append("expiredTime", expiredTime) + .append("packageCapacity", packageCapacity) + .append("usedFlowRate", usedFlowRate) + .append("residualFlowRate", residualFlowRate) + .toString(); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/SimRenewResultVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/SimRenewResultVO.java new file mode 100644 index 000000000..83477ba1f --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/SimRenewResultVO.java @@ -0,0 +1,37 @@ +package com.jsowell.pile.vo.web; + +import lombok.Data; + +/** + * sim卡续费VO + * + * @author JS-ZZA + * @date 2022/12/21 11:12 + */ +@Data +public class SimRenewResultVO { + /** + * 卡号 + */ + private String iccId; + + /** + * 卡商 + */ + private String simSuppler; + + /** + * 续费周期 + */ + private int cycleNumber; + + /** + * 续费结果 + */ + private boolean result; + + /** + * 失败原因 + */ + private String reason; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/UpdateMemberBalanceDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/UpdateMemberBalanceDTO.java new file mode 100644 index 000000000..599b3136d --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/UpdateMemberBalanceDTO.java @@ -0,0 +1,61 @@ +package com.jsowell.pile.vo.web; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class UpdateMemberBalanceDTO { + /** + * 会员id + */ + private String memberId; + + /** + * 更新类型 + * 1-进账;2-出账 + */ + private String type; + + /** + * 子类型 + * 进账:10-充值, 11-赠送, 12-订单结算退款 + * 出账:20-后管扣款, 21-订单付款, 22-用户退款 + */ + private String subType; + + /** + * 充值/扣款 本金金额 + */ + private BigDecimal updatePrincipalBalance; + + /** + * 充值/扣款 赠送金额 + */ + private BigDecimal updateGiftBalance; + + /** + * 关联订单 + * 只有支付订单和订单退款时才有值 + */ + private String relatedOrderCode; + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("memberId", memberId) + .append("type", type) + .append("subType", subType) + .append("updatePrincipalBalance", updatePrincipalBalance) + .append("updateGiftBalance", updateGiftBalance) + .toString(); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/WuLianSimData.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/WuLianSimData.java new file mode 100644 index 000000000..23b2d02c1 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/WuLianSimData.java @@ -0,0 +1,93 @@ +package com.jsowell.pile.vo.web; + +import lombok.Data; + +/** + * 物联智能Sim卡信息 + * + * @author JS-ZZA + * @date 2023/1/4 11:56 + */ +@Data +public class WuLianSimData { + /** + * 到期时间 + */ + private String cardEndTime; + + /** + * 开始计费时间 + */ + private String cardFeeTime; + + /** + * 卡状态 + * + * 0-未开卡 + * 2-沉默期 + * 4-已停机 + * 5-已断网 + * 8-待激活 + * 9-正常使用 + * 20-期满,关停 + * 21-已回收状态 + * 80-未知 + * 99-已删除 + */ + private String cardStatus; + + /** + * 卡号 + */ + private String iccId; + + /** + * IMSI号 + */ + private String imsi; + + /** + * 剩余周期 + */ + private String leftPeriod; + + /** + * 卡 msisdn + */ + private String msisdn; + + /** + * 备注 + */ + private String note; + + /** + * 卡套餐可使用流量(M) + */ + private String packageCanUsage; + + /** + * 套餐已使用流量(M) + */ + private String packageHasUsage; + + /** + * 套餐 ID + */ + private String packageId; + + /** + * 套餐名称 + */ + private String packageName; + + /** + * 本期结束时间 + */ + private String periodEndTime; + + /** + * 本期开始时间 + */ + private String periodStartTime; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/WuLianSimRenewVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/WuLianSimRenewVO.java new file mode 100644 index 000000000..7e6f06bc1 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/WuLianSimRenewVO.java @@ -0,0 +1,44 @@ +package com.jsowell.pile.vo.web; + +import lombok.Data; + +/** + * 物联平台Sim卡续费结果 + * + * @author JS-ZZA + * @date 2023/2/18 9:31 + */ +@Data +public class WuLianSimRenewVO { + + /** + * 返回code + */ + private String code; + + /** + * 返回的msg + */ + private String msg; + + /** + * 下单成功之后的订单号 + */ + private String orderNo; + + /** + * 订单状态 + * 0.待付款 1.待审核 2.审核通过 3.审核不通过 + */ + private String orderStatus; + + /** + * 外部订单号 + */ + private String outOrderNo; + + /** + * 创建时间 + */ + private String createTime; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/XunZhongSimData.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/XunZhongSimData.java new file mode 100644 index 000000000..e4406bbf7 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/XunZhongSimData.java @@ -0,0 +1,50 @@ +package com.jsowell.pile.vo.web; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +@Data +public class XunZhongSimData { + private String code; + private String message; + private String iccid; + private String carrier_type; + private String msisdn; + private String imsi; + private String activated_at; + private String life_cycle; + private String net_status; + private String bind_status; + private String power_status; + private String online_status; + private String is_need_verified; + private String authentication_status; + private String service_end_time; + private String agent_name; + private String agent_id; + private List current_products; + private List future_products; + + @Data + public static class Products { + private String user_business_type; + private String code; + private String name; + private String service_cycle; + private String service_cycle_unit; + private BigDecimal package_capacity; + private String capacity_unit; + private BigDecimal voice_capacity; + private String subscribed_at; + private String effective_time; + private String expiration_time; + private String cycles; + private String cycle_list; + private String current_cycle_begin_time; + private String current_cycle_end_time; + private BigDecimal current_cycle_usage; + private BigDecimal current_cycle_voice_usage; + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/wxpay/common/WeChatPayParameter.java b/jsowell-pile/src/main/java/com/jsowell/wxpay/common/WeChatPayParameter.java new file mode 100644 index 000000000..3cb853b30 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/wxpay/common/WeChatPayParameter.java @@ -0,0 +1,72 @@ +package com.jsowell.wxpay.common; + +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @Author 徐柯 + * @Description 定义微信支付参数配置类 + * @Date 21:06 2021/5/10 + * @Param + * @return + **/ +public class WeChatPayParameter { + /** + * 微信商户号 + */ + public static String mchId; + + /** + * 商户在微信公众平台申请服务号对应的APPID + */ + public static String appId; + + /** + * 回调报文解密V3密钥key + */ + public static String v3Key; + + /** + * 微信获取平台证书列表地址 + */ + public static String certificatesUrl; + + /** + * 微信APP下单URL + */ + public static String unifiedOrderUrl; + /** + * 微信小程序的单URL + */ + public static String unifiedOrderUrlJS; + + /** + * 异步接收微信支付结果通知的回调地址 + */ + public static String notifyUrl; + + /** + * 微信证书私钥 + */ + public static PrivateKey privateKey; + + /** + * 微信商家api序列号 + */ + public static String mchSerialNo; + + // 定义全局容器 保存微信平台证书公钥 + public static Map certificateMap = new ConcurrentHashMap<>(); + + /** + * jsapi支付 申请退款api + */ + public static String refundJsUrl; + + /** + * 异步接收微信退款结果通知的回调地址 + */ + public static String refundNotifyUrl; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/wxpay/config/WechatPayConfig.java b/jsowell-pile/src/main/java/com/jsowell/wxpay/config/WechatPayConfig.java new file mode 100644 index 000000000..511c166f2 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/wxpay/config/WechatPayConfig.java @@ -0,0 +1,98 @@ +package com.jsowell.wxpay.config; + + +import com.jsowell.wxpay.common.WeChatPayParameter; +import com.jsowell.wxpay.utils.WechatPayUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.CommandLineRunner; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +@Order(value = 2) +@Component +public class WechatPayConfig implements CommandLineRunner { + /** + * 公众号appid + */ + @Value("${wechat.appId}") + private String wechatAppId; + + /** + * 商户号id + */ + @Value("${wechat.mchId}") + private String wechatMchId; + + /** + * 商户序列号 + */ + @Value("${wechat.mchSerialNo}") + private String mchSerialNo; + /** + * 支付key + */ + @Value("${wechat.v3Key}") + private String wechatV3Key; + + /** + * 微信支付回调url + */ + @Value("${wechat.callback}") + private String payCallbackUrl; + + /** + * 微信退款回调url + */ + @Value("${wechat.refundCallback}") + private String refundCallbackUrl; + + /** + * 统一下单url + */ + @Value("${wechat.unifiedOrder.url}") + private String wechatUnifiedOrderUrl; + + /** + * 统一下单url + */ + @Value("${wechat.unifiedOrder.jsurl}") + private String wechatUnifiedOrderUrlJS; + + /** + * jsapi申请退款url + */ + @Value("${wechat.refund.jsurl}") + private String wechatRefundJsUrl; + + /** + * 平台证书列表地址 + */ + @Value("${wechat.certificates.url}") + private String wechatCertificatesUrl; + + /** + * 商户私钥路径 + */ + @Value("${wechat.key.path}") + private String wechatKeyPath; + + @Override + public void run(String... args) throws Exception { + // System.out.println(">>>>>>>>>>>>>>>服务启动执行,执行加载数据等操作 MyStartupRunner1 order 2 <<<<<<<<<<<<<"); + //微信支付 + WeChatPayParameter.mchId = wechatMchId; + WeChatPayParameter.appId = wechatAppId; + WeChatPayParameter.v3Key = wechatV3Key; + WeChatPayParameter.certificatesUrl = wechatCertificatesUrl; + WeChatPayParameter.unifiedOrderUrl = wechatUnifiedOrderUrl; + WeChatPayParameter.unifiedOrderUrlJS = wechatUnifiedOrderUrlJS; + WeChatPayParameter.notifyUrl = payCallbackUrl; + WeChatPayParameter.refundJsUrl = wechatRefundJsUrl; + WeChatPayParameter.refundNotifyUrl = refundCallbackUrl; + //加载商户私钥 + WeChatPayParameter.privateKey = WechatPayUtils.getPrivateKey(wechatKeyPath); + WeChatPayParameter.mchSerialNo = mchSerialNo; + //获取平台证书 + WeChatPayParameter.certificateMap = WechatPayUtils.refreshCertificate(); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/wxpay/config/WeixinLoginProperties.java b/jsowell-pile/src/main/java/com/jsowell/wxpay/config/WeixinLoginProperties.java new file mode 100644 index 000000000..31a38be68 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/wxpay/config/WeixinLoginProperties.java @@ -0,0 +1,34 @@ +package com.jsowell.wxpay.config; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class WeixinLoginProperties implements InitializingBean { + + @Value("${weixin.login.gateway}") + private String gateway; + + @Value("${weixin.login.appid}") + private String appid; + + @Value("${weixin.login.appsecret}") + private String appsecret; + + @Value("${weixin.login.redirectUrl}") + private String redirectUrl; + + public static String WX_OPEN_GATEWAY; + public static String WX_OPEN_APP_ID; + public static String WX_OPEN_APP_SECRET; + public static String WX_OPEN_REDIRECT_URL; + + @Override + public void afterPropertiesSet() throws Exception { + WX_OPEN_APP_ID = appid; + WX_OPEN_GATEWAY = gateway; + WX_OPEN_APP_SECRET = appsecret; + WX_OPEN_REDIRECT_URL = redirectUrl; + } +} \ No newline at end of file diff --git a/jsowell-pile/src/main/java/com/jsowell/wxpay/dto/AppletTemplateMessageSendDTO.java b/jsowell-pile/src/main/java/com/jsowell/wxpay/dto/AppletTemplateMessageSendDTO.java new file mode 100644 index 000000000..570d2043f --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/wxpay/dto/AppletTemplateMessageSendDTO.java @@ -0,0 +1,70 @@ +package com.jsowell.wxpay.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 小程序模板消息发送模板 + */ +@Data +public class AppletTemplateMessageSendDTO implements Serializable { + private static final long serialVersionUID = 2234575457450378840L; + + // 场景类型(1-开始充电;2-结束充电) + private String type; + + //接收者(用户)的 openid + private String touser; + + //所需下发的订阅模板id + private String template_id; + + //所跳转的页面 + private String page; + + //模板消息内容 + private Object data; + + private StartChargingMessage startChargingMessage; + + private StopChargingMessage stopChargingMessage; + + @Data + public static class StartChargingMessage { + + /** + * 站点名称 + * thing5 + */ + private String stationName; + + /** + * 开始时间 + * time2 + */ + private String startTime; + + } + + @Data + public static class StopChargingMessage { + /** + * 充电费用 + * amount17 + */ + private String chargingAmount; + + /** + * 结束时间 + * time3 + */ + private String endTime; + + /** + * 结束原因 + * thing7 + */ + private String endReason; + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/wxpay/dto/WeChatRefundDTO.java b/jsowell-pile/src/main/java/com/jsowell/wxpay/dto/WeChatRefundDTO.java new file mode 100644 index 000000000..8a56f09cf --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/wxpay/dto/WeChatRefundDTO.java @@ -0,0 +1,35 @@ +package com.jsowell.wxpay.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class WeChatRefundDTO { + /** + * 会员id + */ + private String memberId; + + /** + * 订单编号 + */ + private String orderCode; + + /** + * 退款类型 + * 1-订单结算退款 2-用户余额退款 + */ + private String refundType; + + /** + * 退款金额 + */ + private BigDecimal refundAmount; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/wxpay/dto/WechatSendMsgDTO.java b/jsowell-pile/src/main/java/com/jsowell/wxpay/dto/WechatSendMsgDTO.java new file mode 100644 index 000000000..ad346ffe6 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/wxpay/dto/WechatSendMsgDTO.java @@ -0,0 +1,27 @@ +package com.jsowell.wxpay.dto; + +import lombok.Data; + +/** + * 小程序下发消息DTO + * + * @author JS-ZZA + * @date 2023/2/15 9:50 + */ +@Data +public class WechatSendMsgDTO { + /** + * 用户code + */ + private String code; + + /** + * 用户openid + */ + private String openId; + + /** + * 订单号 + */ + private String orderCode; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/wxpay/response/WechatPayNotifyParameter.java b/jsowell-pile/src/main/java/com/jsowell/wxpay/response/WechatPayNotifyParameter.java new file mode 100644 index 000000000..658a0a3fe --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/wxpay/response/WechatPayNotifyParameter.java @@ -0,0 +1,67 @@ +package com.jsowell.wxpay.response; + +import lombok.Data; + +/** + * 微信支付通知参数 + */ +@Data +public class WechatPayNotifyParameter { + /** + * 通知ID 通知的唯一ID + */ + private String id; + + /** + * 通知创建时间 格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE + */ + private String create_time; + + /** + * 通知数据类型 支付成功通知为encrypt-resource + */ + private String resource_type; + + /** + * 通知类型 支付成功通知的类型为TRANSACTION.SUCCESS + */ + private String event_type; + + /** + * 回调摘要 回调摘要 示例值:支付成功 + */ + private String summary; + + /** + * 通知数据 通知资源数据json格式 + */ + private Resource resource; + + @Data + public static class Resource { + /** + * 原始类型 原始回调类型,为transaction + */ + private String original_type; + + /** + * 加密算法类型 对开启结果数据进行加密的加密算法,目前只支持AEAD_AES_256_GCM + */ + private String algorithm; + + /** + * 数据密文 Base64编码后的开启/停用结果数据密文 + */ + private String ciphertext; + + /** + * 附加数据 + */ + private String associated_data; + + /** + * 随机串 加密使用的随机串 + */ + private String nonce; + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/wxpay/response/WechatPayNotifyResource.java b/jsowell-pile/src/main/java/com/jsowell/wxpay/response/WechatPayNotifyResource.java new file mode 100644 index 000000000..0c197aeb4 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/wxpay/response/WechatPayNotifyResource.java @@ -0,0 +1,112 @@ +package com.jsowell.wxpay.response; + +import lombok.Data; + +@Data +public class WechatPayNotifyResource { + + /** + * 商户号 + */ + private String mchid; + + /** + * 应用ID + */ + private String appid; + + /** + * 商户订单号 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一。 + */ + private String out_trade_no; + + /** + * 微信支付订单号 微信支付系统生成的订单号。 + */ + private String transaction_id; + + /** + * 交易类型 + * JSAPI:公众号支付 + * NATIVE:扫码支付 + * APP:APP支付 + * MICROPAY:付款码支付 + * MWEB:H5支付 + * FACEPAY:刷脸支付 + */ + private String trade_type; + + /** + * 交易状态,枚举值: + * SUCCESS:支付成功 + * REFUND:转入退款 + * NOTPAY:未支付 + * CLOSED:已关闭 + * REVOKED:已撤销(付款码支付) + * USERPAYING:用户支付中(付款码支付) + * PAYERROR:支付失败(其他原因,如银行返回失败) + */ + private String trade_state; + + /** + * 交易状态描述 + */ + private String trade_state_desc; + + /** + * 付款银行 银行类型,采用字符串类型的银行标识。 + * https://pay.weixin.qq.com/wiki/doc/apiv3/terms_definition/chapter1_1_3.shtml#part-6 + */ + private String bank_type; + + /** + * 附加数据 + */ + private String attach; + + /** + * 支付完成时间 支付完成时间,遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE + */ + private String success_time; + + /** + * 支付者信息 + */ + private Payer payer; + + /** + * 订单金额 + */ + private Amount amount; + + @Data + public static class Payer { + /** + * 用户标识 用户在直连商户appid下的唯一标识。 + */ + private String openid; + } + + @Data + public static class Amount { + /** + * 总金额 订单总金额,单位为分。 + */ + private String total; + + /** + * 用户支付金额 用户支付金额,单位为分。 + */ + private String payer_total; + + /** + * 货币类型 CNY:人民币,境内商户号仅支持人民币。 + */ + private String currency; + + /** + * 用户支付币种 + */ + private String payer_currency; + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/wxpay/response/WechatPayRefundNotifyResource.java b/jsowell-pile/src/main/java/com/jsowell/wxpay/response/WechatPayRefundNotifyResource.java new file mode 100644 index 000000000..6bd6a3427 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/wxpay/response/WechatPayRefundNotifyResource.java @@ -0,0 +1,83 @@ +package com.jsowell.wxpay.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 微信退款通知参数 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class WechatPayRefundNotifyResource { + /** + * 直连商户号 + */ + private String mchid; + + /** + * 商户订单号 + */ + private String out_trade_no; + + /** + * 微信支付订单号 + */ + private String transaction_id; + + /** + * 商户退款单号 + */ + private String out_refund_no; + + /** + * 微信支付退款单号 + */ + private String refund_id; + + /** + * 退款状态 + */ + private String refund_status; + + /** + * 退款成功时间 + */ + private String success_time; + + /** + * 退款入账账户 + */ + private String user_received_account; + + /** + * 金额信息 + */ + private Amount amount; + + @Data + public static class Amount { + /** + * 订单金额 + */ + private int total; + + /** + * 退款金额 + */ + private int refund; + + /** + * 用户支付金额 + */ + private int payer_total; + + /** + * 用户退款金额 + */ + private int payer_refund; + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/wxpay/response/WechatPayRefundRequest.java b/jsowell-pile/src/main/java/com/jsowell/wxpay/response/WechatPayRefundRequest.java new file mode 100644 index 000000000..5f21a557e --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/wxpay/response/WechatPayRefundRequest.java @@ -0,0 +1,59 @@ +package com.jsowell.wxpay.response; + +import lombok.Data; + +@Data +public class WechatPayRefundRequest { + /** + * 微信支付订单号 + */ + private String transaction_id; + + /** + * 商户订单号 + */ + private String out_trade_no; + + /** + * 商户退款单号 + */ + private String out_refund_no; + + /** + * 退款原因 + */ + private String reason; + + /** + * 退款结果回调url + */ + private String notify_url; + + /** + * 退款资金来源 + */ + private String funds_account; + + /** + * 金额信息 + */ + private Amount amount; + + @Data + public static class Amount { + /** + * 退款金额 + */ + private int refund; + + /** + * 原订单金额 + */ + private int total; + + /** + * 退款币种 + */ + private String currency = "CNY"; + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/wxpay/response/WechatPayRefundResponse.java b/jsowell-pile/src/main/java/com/jsowell/wxpay/response/WechatPayRefundResponse.java new file mode 100644 index 000000000..156ed5d40 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/wxpay/response/WechatPayRefundResponse.java @@ -0,0 +1,161 @@ +package com.jsowell.wxpay.response; + +import lombok.Data; + +import java.util.List; + +@Data +public class WechatPayRefundResponse { + + /** + * 微信支付退款单号 + */ + private String refund_id; + + /** + * 商户退款单号 + */ + private String out_refund_no; + + /** + * 微信支付订单号 + */ + private String transaction_id; + + /** + * 商户订单号 + */ + private String out_trade_no; + + /** + * 退款渠道 + * 枚举值: + * ORIGINAL:原路退款 + * BALANCE:退回到余额 + * OTHER_BALANCE:原账户异常退到其他余额账户 + * OTHER_BANKCARD:原银行卡异常退到其他银行卡 + */ + private String channel; + + /** + * 退款入账账户 + * 取当前退款单的退款入账方,有以下几种情况: + * 1)退回银行卡:{银行名称}{卡类型}{卡尾号} + * 2)退回支付用户零钱:支付用户零钱 + * 3)退还商户:商户基本账户商户结算银行账户 + * 4)退回支付用户零钱通:支付用户零钱通 + */ + private String user_received_account; + + /** + * 退款成功时间 + */ + private String success_time; + + /** + * 退款创建时间 + */ + private String create_time; + + /** + * 退款状态 + * 退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台-交易中心,手动处理此笔退款。 + * 枚举值: + * SUCCESS:退款成功 + * CLOSED:退款关闭 + * PROCESSING:退款处理中 + * ABNORMAL:退款异常 + */ + private String status; + + /** + * 资金账户 + * 退款所使用资金对应的资金账户类型 + * 枚举值: + * UNSETTLED : 未结算资金 + * AVAILABLE : 可用余额 + * UNAVAILABLE : 不可用余额 + * OPERATION : 运营户 + * BASIC : 基本账户(含可用余额和不可用余额) + */ + private String funds_account; + + /** + * 金额详细信息 + */ + private Amount amount; + + @Data + public static class Amount { + /** + * 订单金额 + */ + private int total; + + /** + * 退款金额 + */ + private int refund; + + /** + * 退款出资账户及金额 + */ + private List from; + + /** + * 用户支付金额 + */ + private int payer_total; + + /** + * 用户退款金额 + */ + private int payer_refund; + + /** + * 应结退款金额 + */ + private int settlement_refund; + + /** + * 应结订单金额 + */ + private int settlement_total; + + /** + * 优惠退款金额 + */ + private int discount_refund; + + /** + * 退款币种 + */ + private String currency; + + /** + * 手续费退款金额 + */ + private int refund_fee; + } + + /** + * 退款出资的账户类型及金额信息 + */ + @Data + public static class From { + /** + * 出资账户类型 + * 下面枚举值多选一。 + * 枚举值: + * AVAILABLE : 可用余额 + * UNAVAILABLE : 不可用余额 + */ + private String account; + + /** + * 出资金额 + */ + private int amount; + } + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/wxpay/service/WxAppletRemoteService.java b/jsowell-pile/src/main/java/com/jsowell/wxpay/service/WxAppletRemoteService.java new file mode 100644 index 000000000..f69aa56a2 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/wxpay/service/WxAppletRemoteService.java @@ -0,0 +1,288 @@ +package com.jsowell.wxpay.service; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.redis.RedisCache; +import com.jsowell.common.enums.ykc.ReturnCodeEnum; +import com.jsowell.common.exception.BusinessException; +import com.jsowell.common.util.DateUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.http.HttpUtils; +import com.jsowell.pile.service.IOrderBasicInfoService; +import com.jsowell.pile.vo.uniapp.SendMessageVO; +import com.jsowell.wxpay.config.WeixinLoginProperties; +import com.jsowell.wxpay.dto.AppletTemplateMessageSendDTO; +import com.jsowell.wxpay.dto.WechatSendMsgDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@Service +public class WxAppletRemoteService { + + private Logger log = LoggerFactory.getLogger(WxAppletRemoteService.class); + + private static final String WX_APPLET_URl = "https://api.weixin.qq.com/cgi-bin"; + + private static final String GET_USER_PHONE_NUMBER_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber"; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private RedisCache redisCache; + + @Autowired + private IOrderBasicInfoService orderBasicInfoService; + + @Value("${weixin.login.appid}") + private String appid; + + @Value("${weixin.login.appsecret}") + private String secret; + + @Value("${weixin.sendMsg.startChargingTmpId}") + private String startChargingTmpId; + + @Value("${weixin.sendMsg.stopChargingTmpId}") + private String stopChargingTmpId; + + /** + * 获取accessToken + * + * @return + */ + public String getAccessToken() { + // String appid = Constants.APP_ID; + // String secret = Constants.APP_SECRET; + + // 这里我是从配置文件中取得appid和appsecret + // appid = properties.getAppId(); + // secret = properties.getAppSecret(); + + //查询token是否存在 + String redisKey = "AccessToken_" + appid; + // 使用缓存先查询AccessToken是否存在 + String accessToken = redisCache.getCacheObject(redisKey); + // 存在直接返回,不存在重新获取AccessToken + if (!Strings.isNullOrEmpty(accessToken)) { + return accessToken; + } + // 获取AccessToken的url + String grantType = "client_credential"; + // https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid= + String url = WX_APPLET_URl + "/token?grant_type=" + grantType + "&appid=" + appid + "&secret=" + secret; + // 获取到AccessToken + String token = HttpUtils.sendGet(url); + Map map = null; + try { + map = objectMapper.readValue(token, Map.class); + } catch (IOException e) { + log.error("小程序异常通知-获取AccessToken-转化异常", e); + } + String access_token = String.valueOf(map.get("access_token")); + // 把AccessToken存入缓存中,并设置过期时间,因为access_token的过期时间是两小时,我们缓存的时间一定要小于两小时, + redisCache.setCacheObject(redisKey, access_token, 300); + if (map.get("errcode") != null || map.get("errmsg") != null) { + String errcode = String.valueOf(map.get("errcode")); + String errmsg = String.valueOf(map.get("errmsg")); + if (!errcode.equals("0")) { + log.error("获取token失败:code=" + errcode + "msg=" + errmsg); + return null; + } + } + return access_token; + } + + /** + * 获取手机号 + * + * @param code + * @return + */ + public String getMobileNumberByCode(String code) { + if (StringUtils.isBlank(code)) { + throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR); + } + // 请求获取令牌 + String access_token = getAccessToken(); + // 通过token,code(前端getPhoneNum按钮返回的code,而不是登录的code)发送post请求到官方获取到手机号码 + String postUrl = GET_USER_PHONE_NUMBER_URL + "?access_token=" + access_token; + JSONObject paramJson = new JSONObject(); + paramJson.put("code", code); + String postResult = HttpUtils.sendPost(postUrl, paramJson.toJSONString()); + JSONObject postResultJson = JSONObject.parseObject(postResult); + String errCode = postResultJson.getString("errcode"); + + if (!StringUtils.equals(errCode, Constants.ZERO)) { + // errCode不为0,表示有错误 + String errMsg = postResultJson.getString("errmsg"); + log.info("发送Post请求失败,错误消息:{}", errMsg); + throw new BusinessException(errCode, errMsg); + } + JSONObject phoneInfoJson = (JSONObject) postResultJson.get("phone_info"); + return phoneInfoJson.getString("phoneNumber"); + } + + /** + * 获取openId + * + * @param code + * @return + */ + public String getOpenIdByCode(String code) { + String baseAccessTokenUrl = WeixinLoginProperties.WX_OPEN_GATEWAY + + "?appid=%s" + + "&secret=%s" + + "&js_code=%s" + + "&grant_type=authorization_code"; + + log.info("appid:{},appscrect:{}", WeixinLoginProperties.WX_OPEN_APP_ID, WeixinLoginProperties.WX_OPEN_APP_SECRET); + + String accessTokenUrl = String.format(baseAccessTokenUrl, WeixinLoginProperties.WX_OPEN_APP_ID, WeixinLoginProperties.WX_OPEN_APP_SECRET, code); + + //2:执行请求,获取微信请求返回得数据 + String result = HttpUtils.sendGet(accessTokenUrl); + + // 3: 对微信返回得数据进行转换 + Map resultMap = JSONObject.parseObject(result, HashMap.class); + log.info("微信返回的日志信息是:code:{},resultMap:{}", code, resultMap); + if (resultMap.get("errcode") != null) { + throw new BusinessException("22006", "微信登录出错!"); + } + + // 4: 解析微信用户得唯一凭证openid + String openid = (String) resultMap.get("openid"); + if (StringUtils.isBlank(openid)) { + throw new BusinessException("22009", "登录失败,尝试刷新重新扫码登录!!!"); + } + + // 5:封装返回 + return openid; + } + + /** + * 开始充电发送消息 + * @param dto + * @return + */ + public Map startChargingSendMsg(WechatSendMsgDTO dto) { + // 通过code查询openId并set + String openId = getOpenIdByCode(dto.getCode()); + if (StringUtils.isBlank(openId)) { + return null; + } + AppletTemplateMessageSendDTO msgInfo = new AppletTemplateMessageSendDTO(); + msgInfo.setType("1"); // 1-开始充电推送消息 + msgInfo.setTouser(openId); + // 通过orderCode查询到充电站点和开始时间并set + String orderCode = dto.getOrderCode(); + SendMessageVO sendMessageVO = orderBasicInfoService.selectOrderInfoByOrderCode(orderCode); + + AppletTemplateMessageSendDTO.StartChargingMessage startChargingMessage = new AppletTemplateMessageSendDTO.StartChargingMessage(); + msgInfo.setStartChargingMessage(startChargingMessage); + if (StringUtils.isBlank(sendMessageVO.getChargeStartTime())) { + startChargingMessage.setStartTime(DateUtils.dateTimeNow("yyyy-MM-dd HH:mm")); + }else { + startChargingMessage.setStartTime(sendMessageVO.getChargeStartTime()); // 开始时间 + } + startChargingMessage.setStationName(sendMessageVO.getStationName()); // 站点名称 + return uniAppSendMsg(msgInfo); + } + + + /** + * 停止充电发送消息 + * @param dto + * @return + */ + public Map stopChargingSendMsg(WechatSendMsgDTO dto) { + // 通过订单号查询订单金额 + AppletTemplateMessageSendDTO msgInfo = new AppletTemplateMessageSendDTO(); + SendMessageVO sendMessageVO = orderBasicInfoService.selectOrderInfoByOrderCode(dto.getOrderCode()); + + msgInfo.setType("2"); // 2-结束充电推送消息 + msgInfo.setTouser(sendMessageVO.getOpenId()); + // 封装对象并调用发送消息的方法 + AppletTemplateMessageSendDTO.StopChargingMessage stopChargingMessage = new AppletTemplateMessageSendDTO.StopChargingMessage(); + msgInfo.setStopChargingMessage(stopChargingMessage); + + stopChargingMessage.setChargingAmount(sendMessageVO.getOrderAmount()); + stopChargingMessage.setEndReason(sendMessageVO.getStopReason()); + if (StringUtils.isBlank(sendMessageVO.getChargeStopTime())) { + stopChargingMessage.setEndTime(DateUtils.dateTimeNow("yyyy-MM-dd HH:mm")); + }else { + stopChargingMessage.setEndTime(sendMessageVO.getChargeStopTime()); + } + return uniAppSendMsg(msgInfo); + } + + /** + * 小程序发送消息方法 + * @param dto + * @return + */ + private Map uniAppSendMsg(AppletTemplateMessageSendDTO dto) { + // 判断是什么场景调用此方法(1-开始充电推送消息;2-充电结束推送消息) + String type = dto.getType(); + // 根据不同的场景set不同的对象 + if (StringUtils.equals("1", type)) { + // 开始充电 + String templateId = startChargingTmpId; + dto.setTemplate_id(templateId); + // dto.setPage("跳转的页面"); + Map map = new HashMap<>(); + map.put("thing5", ImmutableMap.of("value", dto.getStartChargingMessage().getStationName())); // 充电站名称 + map.put("time2", ImmutableMap.of("value", dto.getStartChargingMessage().getStartTime())); // 开始时间 + dto.setData(map); + } else if (StringUtils.equals("2", type)) { + // 结束充电 + String templateId = stopChargingTmpId; + dto.setTemplate_id(templateId); + // dto.setPage("跳转的页面"); + Map map = new HashMap<>(); + map.put("amount17", ImmutableMap.of("value", dto.getStopChargingMessage().getChargingAmount())); // 充电金额 + map.put("time3", ImmutableMap.of("value", dto.getStopChargingMessage().getEndTime())); // 结束时间 + map.put("thing7", ImmutableMap.of("value", dto.getStopChargingMessage().getEndReason())); // 结束原因 + dto.setData(map); + } + // 调用下面的发送消息接口 + return uniformMessageSend(dto); + } + + /** + * 同一消息发送接口 + * AppletTemplateMessageSendDTO 是一个传输类 + */ + public Map uniformMessageSend(AppletTemplateMessageSendDTO data) { + String token = getAccessToken(); + // 调用发型接口 + String url = WX_APPLET_URl + "/message/subscribe/send?access_token=" + token; + String returnData = HttpUtils.sendPost(url, JSON.toJSONString(data)); + Map map = null; + try { + map = objectMapper.readValue(returnData, Map.class); + } catch (IOException e) { + log.error("小程序异常通知-同一消息发送-转化异常", e); + } + String errcode = String.valueOf(map.get("errcode")); + String errmsg = String.valueOf(map.get("errmsg")); + if (!errcode.equals(Constants.ZERO)) { + log.error("消息发送失败:code=" + errcode + "msg=" + errmsg); + } + Map resultMap = new HashMap<>(); + resultMap.put("code", errcode); + resultMap.put("message", errmsg); + return resultMap; + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/wxpay/utils/AesUtil.java b/jsowell-pile/src/main/java/com/jsowell/wxpay/utils/AesUtil.java new file mode 100644 index 000000000..b5f041f8b --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/wxpay/utils/AesUtil.java @@ -0,0 +1,45 @@ +package com.jsowell.wxpay.utils; + +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +public class AesUtil { + + static final int KEY_LENGTH_BYTE = 32; + static final int TAG_LENGTH_BIT = 128; + private final byte[] aesKey; + + public AesUtil(byte[] key) { + if (key.length != KEY_LENGTH_BYTE) { + throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节"); + } + this.aesKey = key; + } + + public String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext) + throws GeneralSecurityException, IOException { + try { + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + + SecretKeySpec key = new SecretKeySpec(aesKey, "AES"); + GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce); + + cipher.init(Cipher.DECRYPT_MODE, key, spec); + cipher.updateAAD(associatedData); + + return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "utf-8"); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new IllegalStateException(e); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + throw new IllegalArgumentException(e); + } + } +} \ No newline at end of file diff --git a/jsowell-pile/src/main/java/com/jsowell/wxpay/utils/BufferedImageLuminanceSource.java b/jsowell-pile/src/main/java/com/jsowell/wxpay/utils/BufferedImageLuminanceSource.java new file mode 100644 index 000000000..0864a0497 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/wxpay/utils/BufferedImageLuminanceSource.java @@ -0,0 +1,87 @@ +package com.jsowell.wxpay.utils; + +import com.google.zxing.LuminanceSource; + +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; + +public class BufferedImageLuminanceSource extends LuminanceSource { + + private final BufferedImage image; + private final int left; + private final int top; + + public BufferedImageLuminanceSource(BufferedImage image) { + this(image, 0, 0, image.getWidth(), image.getHeight()); + } + + public BufferedImageLuminanceSource(BufferedImage image, int left, int top, int width, int height) { + super(width, height); + + int sourceWidth = image.getWidth(); + int sourceHeight = image.getHeight(); + if (left + width > sourceWidth || top + height > sourceHeight) { + throw new IllegalArgumentException("Crop rectangle does not fit within image data."); + } + + for (int y = top; y < top + height; y++) { + for (int x = left; x < left + width; x++) { + if ((image.getRGB(x, y) & 0xFF000000) == 0) { + image.setRGB(x, y, 0xFFFFFFFF); // = white + } + } + } + + this.image = new BufferedImage(sourceWidth, sourceHeight, BufferedImage.TYPE_BYTE_GRAY); + this.image.getGraphics().drawImage(image, 0, 0, null); + this.left = left; + this.top = top; + } + + public byte[] getRow(int y, byte[] row) { + if (y < 0 || y >= getHeight()) { + throw new IllegalArgumentException("Requested row is outside the image: " + y); + } + int width = getWidth(); + if (row == null || row.length < width) { + row = new byte[width]; + } + image.getRaster().getDataElements(left, top + y, width, 1, row); + return row; + } + + public byte[] getMatrix() { + int width = getWidth(); + int height = getHeight(); + int area = width * height; + byte[] matrix = new byte[area]; + image.getRaster().getDataElements(left, top, width, height, matrix); + return matrix; + } + + public boolean isCropSupported() { + return true; + } + + public LuminanceSource crop(int left, int top, int width, int height) { + return new BufferedImageLuminanceSource(image, this.left + left, this.top + top, width, height); + } + + public boolean isRotateSupported() { + return true; + } + + public LuminanceSource rotateCounterClockwise() { + int sourceWidth = image.getWidth(); + int sourceHeight = image.getHeight(); + AffineTransform transform = new AffineTransform(0.0, -1.0, 1.0, 0.0, 0.0, sourceWidth); + BufferedImage rotatedImage = new BufferedImage(sourceHeight, sourceWidth, BufferedImage.TYPE_BYTE_GRAY); + Graphics2D g = rotatedImage.createGraphics(); + g.drawImage(image, transform, null); + g.dispose(); + int width = getWidth(); + return new BufferedImageLuminanceSource(rotatedImage, top, sourceWidth - (left + width), getHeight(), width); + } + +} \ No newline at end of file diff --git a/jsowell-pile/src/main/java/com/jsowell/wxpay/utils/HttpUtils.java b/jsowell-pile/src/main/java/com/jsowell/wxpay/utils/HttpUtils.java new file mode 100644 index 000000000..d9c0535aa --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/wxpay/utils/HttpUtils.java @@ -0,0 +1,137 @@ +package com.jsowell.wxpay.utils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.http.HttpEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +public class HttpUtils { + + private static final Logger log = LoggerFactory.getLogger(HttpUtils.class); + + private static final ObjectMapper JSON = new ObjectMapper(); + + /** + * get方法 + * + * @param url + * @return + */ + public static JsonNode doGet(String url) { + CloseableHttpClient httpClient = HttpClientBuilder.create().build(); + HttpGet httpget = new HttpGet(url); + httpget.addHeader("Content-Type", "application/json;charset=UTF-8"); + httpget.addHeader("Accept", "application/json"); + try { + String token = WechatPayUtils.getToken("GET", new URL(url), ""); + httpget.addHeader("Authorization", token); + CloseableHttpResponse httpResponse = httpClient.execute(httpget); + if (httpResponse.getStatusLine().getStatusCode() == 200) { + String jsonResult = EntityUtils.toString(httpResponse.getEntity()); + return JSON.readTree(jsonResult); + } else { + log.warn(EntityUtils.toString(httpResponse.getEntity())); + } + } catch (Exception e) { + log.error("HttpUtils.doGet error", e); + } finally { + try { + httpClient.close(); + } catch (Exception e) { + log.error("httpClient.close() error", e); + } + } + return null; + } + + /** + * 封装post + * + * @return + */ + public static Map doPost(String url, String body) { + CloseableHttpClient httpClient = HttpClients.createDefault(); + HttpPost httpPost = new HttpPost(url); + httpPost.addHeader("Content-Type", "application/json;chartset=utf-8"); + httpPost.addHeader("Accept", "application/json"); + try { + String token = WechatPayUtils.getToken("POST", new URL(url), body); + httpPost.addHeader("Authorization", token); + if (body == null) { + throw new IllegalArgumentException("data参数不能为空"); + } + StringEntity stringEntity = new StringEntity(body, "utf-8"); + httpPost.setEntity(stringEntity); + + CloseableHttpResponse httpResponse = httpClient.execute(httpPost); + HttpEntity httpEntity = httpResponse.getEntity(); + + if (httpResponse.getStatusLine().getStatusCode() == 200) { + String jsonResult = EntityUtils.toString(httpEntity); + return JSON.readValue(jsonResult, HashMap.class); + } else { + log.warn("微信支付错误信息" + EntityUtils.toString(httpEntity)); + } + } catch (Exception e) { + log.error("HttpUtils.doPost error", e); + } finally { + try { + httpClient.close(); + } catch (Exception e) { + log.error("httpClient.close() error", e); + } + } + return null; + } + + /** + * 封装post + * + * @return + */ + public static Map doPostWexin(String url, String body) { + CloseableHttpClient httpClient = HttpClients.createDefault(); + HttpPost httpPost = new HttpPost(url); + httpPost.addHeader("Content-Type", "application/json;chartset=utf-8"); + httpPost.addHeader("Accept", "application/json"); + try { + String token = WechatPayUtils.getToken("POST", new URL(url), body); + httpPost.addHeader("Authorization", token); + if (body == null) { + throw new IllegalArgumentException("data参数不能为空"); + } + StringEntity stringEntity = new StringEntity(body, "utf-8"); + httpPost.setEntity(stringEntity); + CloseableHttpResponse httpResponse = httpClient.execute(httpPost); + HttpEntity httpEntity = httpResponse.getEntity(); + if (httpResponse.getStatusLine().getStatusCode() == 200) { + String jsonResult = EntityUtils.toString(httpEntity); + return JSON.readValue(jsonResult, HashMap.class); + } else { + log.error("微信支付错误信息:{}", EntityUtils.toString(httpEntity)); + } + } catch (Exception e) { + log.error("HttpUtils.doPostWexin error", e); + } finally { + try { + httpClient.close(); + } catch (Exception e) { + log.error("httpClient.close() error", e); + } + } + return null; + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/wxpay/utils/QRCodeUtil.java b/jsowell-pile/src/main/java/com/jsowell/wxpay/utils/QRCodeUtil.java new file mode 100644 index 000000000..3a63095ab --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/wxpay/utils/QRCodeUtil.java @@ -0,0 +1,147 @@ +package com.jsowell.wxpay.utils; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.DecodeHintType; +import com.google.zxing.EncodeHintType; +import com.google.zxing.MultiFormatReader; +import com.google.zxing.MultiFormatWriter; +import com.google.zxing.Result; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.HybridBinarizer; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.geom.RoundRectangle2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.OutputStream; +import java.util.Hashtable; + +public class QRCodeUtil { + private static final String CHARSET = "utf-8"; + private static final String FORMAT_NAME = "JPG"; + // 二维码尺寸 + private static final int QRCODE_SIZE = 300; + // LOGO宽度 + private static final int WIDTH = 90; + // LOGO高度 + private static final int HEIGHT = 90; + + private static BufferedImage createImage(String content, String imgPath, boolean needCompress) throws Exception { + Hashtable hints = new Hashtable(); + hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); + hints.put(EncodeHintType.CHARACTER_SET, CHARSET); + hints.put(EncodeHintType.MARGIN, 1); + BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, QRCODE_SIZE, QRCODE_SIZE, + hints); + int width = bitMatrix.getWidth(); + int height = bitMatrix.getHeight(); + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF); + } + } + if (imgPath == null || "".equals(imgPath)) { + return image; + } + // 插入图片 + QRCodeUtil.insertImage(image, imgPath, needCompress); + return image; + } + + private static void insertImage(BufferedImage source, String imgPath, boolean needCompress) throws Exception { + File file = new File(imgPath); + if (!file.exists()) { + System.err.println("" + imgPath + " 该文件不存在!"); + return; + } + Image src = ImageIO.read(new File(imgPath)); + int width = src.getWidth(null); + int height = src.getHeight(null); + if (needCompress) { // 压缩LOGO + if (width > WIDTH) { + width = WIDTH; + } + if (height > HEIGHT) { + height = HEIGHT; + } + Image image = src.getScaledInstance(width, height, Image.SCALE_SMOOTH); + BufferedImage tag = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics g = tag.getGraphics(); + g.drawImage(image, 0, 0, null); // 绘制缩小后的图 + g.dispose(); + src = image; + } + // 插入LOGO + Graphics2D graph = source.createGraphics(); + int x = (QRCODE_SIZE - width) / 2; + int y = (QRCODE_SIZE - height) / 2; + graph.drawImage(src, x, y, width, height, null); + Shape shape = new RoundRectangle2D.Float(x, y, width, width, 6, 6); + graph.setStroke(new BasicStroke(3f)); + graph.draw(shape); + graph.dispose(); + } + + public static void encode(String content, String imgPath, String destPath, boolean needCompress) throws Exception { + BufferedImage image = QRCodeUtil.createImage(content, imgPath, needCompress); + mkdirs(destPath); + ImageIO.write(image, FORMAT_NAME, new File(destPath)); + } + + public static BufferedImage encode(String content, String imgPath, boolean needCompress) throws Exception { + BufferedImage image = QRCodeUtil.createImage(content, imgPath, needCompress); + return image; + } + + public static void mkdirs(String destPath) { + File file = new File(destPath); + // 当文件夹不存在时,mkdirs会自动创建多层目录,区别于mkdir.(mkdir如果父目录不存在则会抛出异常) + if (!file.exists() && !file.isDirectory()) { + file.mkdirs(); + } + } + + public static void encode(String content, String imgPath, String destPath) throws Exception { + QRCodeUtil.encode(content, imgPath, destPath, false); + } + + + public static void encode(String content, String destPath) throws Exception { + QRCodeUtil.encode(content, null, destPath, false); + } + + public static void encode(String content, String imgPath, OutputStream output, boolean needCompress) + throws Exception { + BufferedImage image = QRCodeUtil.createImage(content, imgPath, needCompress); + ImageIO.write(image, FORMAT_NAME, output); + } + + public static void encode(String content, OutputStream output) throws Exception { + QRCodeUtil.encode(content, null, output, false); + } + + public static String decode(File file) throws Exception { + BufferedImage image; + image = ImageIO.read(file); + if (image == null) { + return null; + } + BufferedImageLuminanceSource source = new BufferedImageLuminanceSource(image); + BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); + Result result; + Hashtable hints = new Hashtable(); + hints.put(DecodeHintType.CHARACTER_SET, CHARSET); + result = new MultiFormatReader().decode(bitmap, hints); + String resultStr = result.getText(); + return resultStr; + } + + public static String decode(String path) throws Exception { + return QRCodeUtil.decode(new File(path)); + } + +} \ No newline at end of file diff --git a/jsowell-pile/src/main/java/com/jsowell/wxpay/utils/WechatPayUtils.java b/jsowell-pile/src/main/java/com/jsowell/wxpay/utils/WechatPayUtils.java new file mode 100644 index 000000000..d6fb71c6e --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/wxpay/utils/WechatPayUtils.java @@ -0,0 +1,243 @@ +package com.jsowell.wxpay.utils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.jsowell.wxpay.common.WeChatPayParameter; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + + +/** + * 参考官网 https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml + */ +public class WechatPayUtils { + /** + * 获取私钥。 + * + * @param filename 私钥文件路径 (required) + * @return 私钥对象 + */ + public static PrivateKey getPrivateKey(String filename) throws IOException { + // System.out.println("filename:" + filename); + String content = new String(Files.readAllBytes(Paths.get(filename)), "utf-8"); + try { + String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replaceAll("\\s+", ""); + + KeyFactory kf = KeyFactory.getInstance("RSA"); + return kf.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey))); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("当前Java环境不支持RSA", e); + } catch (InvalidKeySpecException e) { + throw new RuntimeException("无效的密钥格式"); + } + } + + /** + * 生成token 也就是生成签名 + * + * @param method + * @param url + * @param body + * @return + * @throws Exception + */ + public static String getToken(String method, URL url, String body) throws Exception { + String nonceStr = getNonceStr(); + long timestamp = System.currentTimeMillis() / 1000; + String message = buildMessage(method, url, timestamp, nonceStr, body); + String signature = sign(message.getBytes("utf-8")); + + return "WECHATPAY2-SHA256-RSA2048 " + "mchid=\"" + WeChatPayParameter.mchId + "\"," + + "nonce_str=\"" + nonceStr + "\"," + + "timestamp=\"" + timestamp + "\"," + + "serial_no=\"" + WeChatPayParameter.mchSerialNo + "\"," + + "signature=\"" + signature + "\""; + } + + /** + * 生成token + * + * @param method + * @param url + * @param body + * @return + * @throws Exception + */ + public static Map getTokenWeixin(String method, URL url, String body, String prepay_id) throws Exception { + String nonceStr = getNonceStr(); + long timestamp = System.currentTimeMillis() / 1000; + String message = buildMessage(method, url, timestamp, nonceStr, body); + String signature = sign(message.getBytes("utf-8")); + + Map map = new HashMap<>(); + map.put("timeStamp", String.valueOf(timestamp)); + map.put("nonceStr", nonceStr); + map.put("package", "prepay_id=" + prepay_id); + map.put("signType", "RSA"); + map.put("paySign", signature); + + return map; + } + + + /** + * 生成签名 + * + * @param message + * @return + * @throws Exception + */ + public static String sign(byte[] message) throws Exception { + Signature sign = Signature.getInstance("SHA256withRSA"); + sign.initSign(WeChatPayParameter.privateKey); + sign.update(message); + return Base64.getEncoder().encodeToString(sign.sign()); + } + + /** + * 生成签名串 + * + * @param method + * @param url + * @param timestamp + * @param nonceStr + * @param body + * @return + */ + public static String buildMessage(String method, URL url, long timestamp, String nonceStr, String body) { + String canonicalUrl = url.getPath(); + if (url.getQuery() != null) { + canonicalUrl += "?" + url.getQuery(); + } + return method + "\n" + + canonicalUrl + "\n" + + timestamp + "\n" + + nonceStr + "\n" + + body + "\n"; + } + + /** + * 生成随机数 + * + * @return + */ + public static String getNonceStr() { + return UUID.randomUUID().toString() + .replaceAll("-", "") + .substring(0, 32); + } + + /** + * 获取平台证书 + * + * @return + */ + public static Map refreshCertificate() throws Exception { + Map certificateMap = new HashMap(); + // 1: 执行get请求 + JsonNode jsonNode = HttpUtils.doGet(WeChatPayParameter.certificatesUrl); + // 2: 获取平台验证的相关参数信息 + JsonNode data = jsonNode.get("data"); + if (data != null) { + for (int i = 0; i < data.size(); i++) { + JsonNode encrypt_certificate = data.get(i).get("encrypt_certificate"); + //对关键信息进行解密 + AesUtil aesUtil = new AesUtil(WeChatPayParameter.v3Key.getBytes()); + String associated_data = encrypt_certificate.get("associated_data").toString().replaceAll("\"", ""); + String nonce = encrypt_certificate.get("nonce").toString().replaceAll("\"", ""); + String ciphertext = encrypt_certificate.get("ciphertext").toString().replaceAll("\"", ""); + //证书内容 + String certStr = aesUtil.decryptToString(associated_data.getBytes(), nonce.getBytes(), ciphertext); + //证书内容转成证书对象 + CertificateFactory cf = CertificateFactory.getInstance("X509"); + X509Certificate x509Cert = (X509Certificate) cf.generateCertificate( + new ByteArrayInputStream(certStr.getBytes("utf-8")) + ); + String serial_no = data.get(i).get("serial_no").toString().replaceAll("\"", ""); + certificateMap.put(serial_no, x509Cert); + } + } + return certificateMap; + } + + /** + * 验证签名 + * + * @param certificate + * @param message + * @param signature + * @return + */ + public static boolean verify(X509Certificate certificate, byte[] message, String signature) { + try { + Signature sign = Signature.getInstance("SHA256withRSA"); + sign.initVerify(certificate); + sign.update(message); + return sign.verify(Base64.getDecoder().decode(signature)); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("当前Java环境不支持SHA256withRSA", e); + } catch (SignatureException e) { + throw new RuntimeException("签名验证过程发生了错误", e); + } catch (InvalidKeyException e) { + throw new RuntimeException("无效的证书", e); + } + } + + /** + * 参考网站 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_4.shtml + * + * @param appId + * @param prepay_id + * @return + * @throws IOException + * @throws SignatureException + * @throws NoSuchAlgorithmException + * @throws InvalidKeyException + */ + public static HashMap getTokenWeixin(String appId, String prepay_id) throws Exception { + // 获取随机字符串 + String nonceStr = getNonceStr(); + // 获取微信小程序支付package + String packagestr = "prepay_id=" + prepay_id; + long timestamp = System.currentTimeMillis() / 1000; + //签名,使用字段appId、timeStamp、nonceStr、package计算得出的签名值 + String message = buildMessageTwo(appId, timestamp, nonceStr, packagestr); + //获取对应的签名 + String signature = sign(message.getBytes("utf-8")); + // 组装返回 + HashMap map = new HashMap<>(); + map.put("timeStamp", String.valueOf(timestamp)); + map.put("nonceStr", nonceStr); + map.put("package", packagestr); + map.put("signType", "RSA"); + map.put("paySign", signature); + return map; + } + + private static String buildMessageTwo(String appId, long timestamp, String nonceStr, String packag) { + return appId + "\n" + + timestamp + "\n" + + nonceStr + "\n" + + packag + "\n"; + } + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/wxpay/vo/R.java b/jsowell-pile/src/main/java/com/jsowell/wxpay/vo/R.java new file mode 100644 index 000000000..69dc0f45c --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/wxpay/vo/R.java @@ -0,0 +1,85 @@ +package com.jsowell.wxpay.vo; + +import lombok.Data; +import lombok.ToString; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * @ClassName BaseController + * @Author :xuke + * @Date :2021/1/17 13:01 + * @Description:通用数据返回格式 + * @Version: 1.0 + */ +@Data +@ToString +public class R implements Serializable { + + private Boolean success; + + private Integer code; + + private String message; + + private Map data = new HashMap(); + + + private R() { + } + + public static R ok() { + R r = new R(); + r.setSuccess(ResultCodeEnum.SUCCESS.getSuccess()); + r.setCode(ResultCodeEnum.SUCCESS.getCode()); + r.setMessage(ResultCodeEnum.SUCCESS.getMessage()); + return r; + } + + public static R error() { + R r = new R(); + r.setSuccess(ResultCodeEnum.UNKNOWN_REASON.getSuccess()); + r.setCode(ResultCodeEnum.UNKNOWN_REASON.getCode()); + r.setMessage(ResultCodeEnum.UNKNOWN_REASON.getMessage()); + return r; + } + + public static R setResult(ResultCodeEnum resultCodeEnum) { + R r = new R(); + r.setSuccess(resultCodeEnum.getSuccess()); + r.setCode(resultCodeEnum.getCode()); + r.setMessage(resultCodeEnum.getMessage()); + return r; + } + + public R success(Boolean success) { + this.setSuccess(success); + return this; + } + + public R message(String message) { + this.setMessage(message); + return this; + } + + public R code(Integer code) { + this.setCode(code); + return this; + } + + public R data(String key, Object value) { + this.data.put(key, value); + return this; + } + + public R data(Map map) { + this.setData(map); + return this; + } + + public Map toMap(){ + return this.data; + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/wxpay/vo/ResultCodeEnum.java b/jsowell-pile/src/main/java/com/jsowell/wxpay/vo/ResultCodeEnum.java new file mode 100644 index 000000000..e26cef670 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/wxpay/vo/ResultCodeEnum.java @@ -0,0 +1,42 @@ +package com.jsowell.wxpay.vo; + +import lombok.Getter; + +@Getter +public enum ResultCodeEnum { + + SUCCESS(true, 20000,"成功"), + UNKNOWN_REASON(false, 20001, "未知错误"), + + LOGIN_PHONE_ERRROR(false, 20002, "手机号码不能为空"), + ACCOUNT_PHONE_ERRROR(false, 20002, "账号信息不能为空"), + LOGIN_PHONE_PATTARN_ERRROR(false, 20003, "手机号码格式不正确"), + VALIDATION_CODE_ERROR(false, 20004, "验证码不正确"), + LOGIN_CODE_ERROR(false, 20005, "短信验证码不能为空"), + LOGIN_CAPATA_ERROR(false, 20006, "图形验证码不能为空"), + LOGIN_CODE_FAIL_ERROR(false, 20007, "短信验证码失效,请重新发送"), + LOGIN_CODE_INPUT_ERROR(false, 20008, "输入的短信码有误"), + PHONE_ERROR_MSG(false, 20009, "该手机号未绑定账户"), + USER_FORBIDDEN(false, 20010, "该用户已被禁用,请联系平台客服"), + LOGIN_PWD_ERROR(false, 200011, "密码不允许为空"), + LOGIN_PWD_INPUT_ERROR(false, 200012, "密码输入有误"), + LOGIN_PWD_ACCOUNT_INPUT_ERROR(false, 200012, "输入的账号或者密码有误"), + LOGIN_PWD_NO_INPUT_ERROR(false, 200013, "检测到没有完善密码信息"), + + + BAD_SQL_GRAMMAR(false, 21001, "sql语法错误"), + JSON_PARSE_ERROR(false, 21002, "json解析异常"), + PARAM_ERROR(false, 21003, "参数不正确"), + USER_PWD_ERROR(false, 21003, "尚未找到对应的用户信息"); + + + private Boolean success; + private Integer code; + private String message; + + private ResultCodeEnum(Boolean success, Integer code, String message) { + this.success = success; + this.code = code; + this.message = message; + } +} \ No newline at end of file diff --git a/jsowell-pile/src/main/resources/mapper/mapper/pile/MemberTransactionRecordMapper.xml b/jsowell-pile/src/main/resources/mapper/mapper/pile/MemberTransactionRecordMapper.xml new file mode 100644 index 000000000..89fe4cf1d --- /dev/null +++ b/jsowell-pile/src/main/resources/mapper/mapper/pile/MemberTransactionRecordMapper.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + id, order_code, scenario_type, member_id, action_type, pay_mode, amount, out_trade_no, + transaction_id, out_refund_no, refund_id, create_time + + + + + insert into member_transaction_record + + + id, + + + order_code, + + + scenario_type, + + + member_id, + + + action_type, + + + pay_mode, + + + amount, + + + out_trade_no, + + + transaction_id, + + + out_refund_no, + + + refund_id, + + + create_time, + + + + + #{id,jdbcType=INTEGER}, + + + #{orderCode,jdbcType=VARCHAR}, + + + #{scenarioType,jdbcType=VARCHAR}, + + + #{memberId,jdbcType=VARCHAR}, + + + #{actionType,jdbcType=VARCHAR}, + + + #{payMode,jdbcType=VARCHAR}, + + + #{amount,jdbcType=VARCHAR}, + + + #{outTradeNo,jdbcType=VARCHAR}, + + + #{transactionId,jdbcType=VARCHAR}, + + + #{outRefundNo,jdbcType=VARCHAR}, + + + #{refundId,jdbcType=VARCHAR}, + + + #{createTime,jdbcType=TIMESTAMP}, + + + + + + \ No newline at end of file diff --git a/jsowell-pile/src/main/resources/mapper/pile/MemberBasicInfoMapper.xml b/jsowell-pile/src/main/resources/mapper/pile/MemberBasicInfoMapper.xml new file mode 100644 index 000000000..61bc083d6 --- /dev/null +++ b/jsowell-pile/src/main/resources/mapper/pile/MemberBasicInfoMapper.xml @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + select + + from member_basic_info + + + + + id, member_id, open_id, nick_name,logic_card, physics_card, status, avatar_url, mobile_number, merchant_id, remark, + create_time, create_by, update_time, update_by, del_flag + + + + + + + + insert into member_basic_info + + member_id, + open_id, + nick_name, + logic_card, + physics_card, + status, + avatar_url, + mobile_number, + merchant_id, + remark, + create_time, + create_by, + update_time, + update_by, + + + #{memberId}, + #{openId}, + #{nickName}, + #{logicCard}, + #{physicsCard}, + #{status}, + #{avatarUrl}, + #{mobileNumber}, + #{merchantId}, + #{remark}, + #{createTime}, + #{createBy}, + #{updateTime}, + #{updateBy}, + + + + + update member_basic_info + + member_id = #{memberId}, + open_id = #{openId}, + nick_name = #{nickName}, + logic_card = #{logicCard}, + physics_card = #{physicsCard}, + status = #{status}, + avatar_url = #{avatarUrl}, + mobile_number = #{mobileNumber}, + merchant_id = #{merchantId}, + remark = #{remark}, + create_time = #{createTime}, + create_by = #{createBy}, + update_time = #{updateTime}, + update_by = #{updateBy}, + del_flag = #{delFlag}, + + where id = #{id} + + + + delete from member_basic_info where id in + + #{id} + + + + + + + + + + + update member_wallet_info + + version = version + 1, + principal_balance = #{newPrincipalBalance}, + gift_balance = #{newGiftBalance}, + + where member_id = #{memberId,jdbcType=VARCHAR} + and version = #{version,jdbcType=INTEGER} + + + + + + + + \ No newline at end of file diff --git a/jsowell-pile/src/main/resources/mapper/pile/MemberWalletInfoMapper.xml b/jsowell-pile/src/main/resources/mapper/pile/MemberWalletInfoMapper.xml new file mode 100644 index 000000000..3e2572c73 --- /dev/null +++ b/jsowell-pile/src/main/resources/mapper/pile/MemberWalletInfoMapper.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + id, member_id, principal_balance, gift_balance, version, create_by, create_time, + update_by, update_time, del_flag + + + + + delete from member_wallet_info + where id = #{id,jdbcType=INTEGER} + + + + insert into member_wallet_info (id, member_id, principal_balance, + gift_balance, version, create_by, + create_time, update_by, update_time, + del_flag) + values (#{id,jdbcType=INTEGER}, #{memberId,jdbcType=VARCHAR}, #{principalBalance,jdbcType=DECIMAL}, + #{giftBalance,jdbcType=DECIMAL}, #{version,jdbcType=INTEGER}, #{createBy,jdbcType=VARCHAR}, + #{createTime,jdbcType=TIMESTAMP}, #{updateBy,jdbcType=VARCHAR}, #{updateTime,jdbcType=TIMESTAMP}, + #{delFlag,jdbcType=CHAR}) + + + + insert into member_wallet_info + + + id, + + + member_id, + + + principal_balance, + + + gift_balance, + + + version, + + + create_by, + + + create_time, + + + update_by, + + + update_time, + + + del_flag, + + + + + #{id,jdbcType=INTEGER}, + + + #{memberId,jdbcType=VARCHAR}, + + + #{principalBalance,jdbcType=DECIMAL}, + + + #{giftBalance,jdbcType=DECIMAL}, + + + #{version,jdbcType=INTEGER}, + + + #{createBy,jdbcType=VARCHAR}, + + + #{createTime,jdbcType=TIMESTAMP}, + + + #{updateBy,jdbcType=VARCHAR}, + + + #{updateTime,jdbcType=TIMESTAMP}, + + + #{delFlag,jdbcType=CHAR}, + + + + + + update member_wallet_info + + + member_id = #{memberId,jdbcType=VARCHAR}, + + + principal_balance = #{principalBalance,jdbcType=DECIMAL}, + + + gift_balance = #{giftBalance,jdbcType=DECIMAL}, + + + version = #{version,jdbcType=INTEGER}, + + + create_by = #{createBy,jdbcType=VARCHAR}, + + + create_time = #{createTime,jdbcType=TIMESTAMP}, + + + update_by = #{updateBy,jdbcType=VARCHAR}, + + + update_time = #{updateTime,jdbcType=TIMESTAMP}, + + + del_flag = #{delFlag,jdbcType=CHAR}, + + + where id = #{id,jdbcType=INTEGER} + + + + update member_wallet_info + set member_id = #{memberId,jdbcType=VARCHAR}, + principal_balance = #{principalBalance,jdbcType=DECIMAL}, + gift_balance = #{giftBalance,jdbcType=DECIMAL}, + version = #{version,jdbcType=INTEGER}, + create_by = #{createBy,jdbcType=VARCHAR}, + create_time = #{createTime,jdbcType=TIMESTAMP}, + update_by = #{updateBy,jdbcType=VARCHAR}, + update_time = #{updateTime,jdbcType=TIMESTAMP}, + del_flag = #{delFlag,jdbcType=CHAR} + where id = #{id,jdbcType=INTEGER} + + + + \ No newline at end of file diff --git a/jsowell-pile/src/main/resources/mapper/pile/MemberWalletLogMapper.xml b/jsowell-pile/src/main/resources/mapper/pile/MemberWalletLogMapper.xml new file mode 100644 index 000000000..598c3df0a --- /dev/null +++ b/jsowell-pile/src/main/resources/mapper/pile/MemberWalletLogMapper.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + id, member_id, `type`, sub_type, amount, category, create_by, create_time + + + + insert into member_wallet_log (member_id, `type`, + sub_type, amount, category, related_order_code, create_by) + values + + ( + #{item.memberId,jdbcType=VARCHAR}, #{item.type,jdbcType=VARCHAR}, #{item.subType,jdbcType=VARCHAR}, + #{item.amount,jdbcType=DECIMAL}, #{item.category,jdbcType=CHAR}, #{item.relatedOrderCode,jdbcType=VARCHAR}, + #{item.createBy,jdbcType=VARCHAR} + ) + + + + + \ No newline at end of file diff --git a/jsowell-pile/src/main/resources/mapper/pile/OrderAbnormalRecordMapper.xml b/jsowell-pile/src/main/resources/mapper/pile/OrderAbnormalRecordMapper.xml new file mode 100644 index 000000000..0d1315f36 --- /dev/null +++ b/jsowell-pile/src/main/resources/mapper/pile/OrderAbnormalRecordMapper.xml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select id, order_code, pile_sn, connector_code, start_time, end_time, sharp_price, sharp_used_electricity, sharp_plan_loss_electricity, sharp_amount, peak_price, peak_used_electricity, peak_plan_loss_electricity, peak_amount, flat_price, flat_used_electricity, flat_plan_loss_electricity, flat_amount, valley_price, valley_used_electricity, valley_plan_loss_electricity, valley_amount, ammeter_total_start, ammeter_total_end, total_electricity, plan_loss_total_electricity, consumption_amount, vin_code, transaction_identifier, transaction_time, stop_reason_msg, logic_card, create_time from order_abnormal_record + + + + + + + + insert into order_abnormal_record + + order_code, + pile_sn, + connector_code, + start_time, + end_time, + sharp_price, + sharp_used_electricity, + sharp_plan_loss_electricity, + sharp_amount, + peak_price, + peak_used_electricity, + peak_plan_loss_electricity, + peak_amount, + flat_price, + flat_used_electricity, + flat_plan_loss_electricity, + flat_amount, + valley_price, + valley_used_electricity, + valley_plan_loss_electricity, + valley_amount, + ammeter_total_start, + ammeter_total_end, + total_electricity, + plan_loss_total_electricity, + consumption_amount, + vin_code, + transaction_identifier, + transaction_time, + stop_reason_msg, + logic_card, + create_time, + + + #{orderCode}, + #{pileSn}, + #{connectorCode}, + #{startTime}, + #{endTime}, + #{sharpPrice}, + #{sharpUsedElectricity}, + #{sharpPlanLossElectricity}, + #{sharpAmount}, + #{peakPrice}, + #{peakUsedElectricity}, + #{peakPlanLossElectricity}, + #{peakAmount}, + #{flatPrice}, + #{flatUsedElectricity}, + #{flatPlanLossElectricity}, + #{flatAmount}, + #{valleyPrice}, + #{valleyUsedElectricity}, + #{valleyPlanLossElectricity}, + #{valleyAmount}, + #{ammeterTotalStart}, + #{ammeterTotalEnd}, + #{totalElectricity}, + #{planLossTotalElectricity}, + #{consumptionAmount}, + #{vinCode}, + #{transactionIdentifier}, + #{transactionTime}, + #{stopReasonMsg}, + #{logicCard}, + #{createTime}, + + + + + update order_abnormal_record + + order_code = #{orderCode}, + pile_sn = #{pileSn}, + connector_code = #{connectorCode}, + start_time = #{startTime}, + end_time = #{endTime}, + sharp_price = #{sharpPrice}, + sharp_used_electricity = #{sharpUsedElectricity}, + sharp_plan_loss_electricity = #{sharpPlanLossElectricity}, + sharp_amount = #{sharpAmount}, + peak_price = #{peakPrice}, + peak_used_electricity = #{peakUsedElectricity}, + peak_plan_loss_electricity = #{peakPlanLossElectricity}, + peak_amount = #{peakAmount}, + flat_price = #{flatPrice}, + flat_used_electricity = #{flatUsedElectricity}, + flat_plan_loss_electricity = #{flatPlanLossElectricity}, + flat_amount = #{flatAmount}, + valley_price = #{valleyPrice}, + valley_used_electricity = #{valleyUsedElectricity}, + valley_plan_loss_electricity = #{valleyPlanLossElectricity}, + valley_amount = #{valleyAmount}, + ammeter_total_start = #{ammeterTotalStart}, + ammeter_total_end = #{ammeterTotalEnd}, + total_electricity = #{totalElectricity}, + plan_loss_total_electricity = #{planLossTotalElectricity}, + consumption_amount = #{consumptionAmount}, + vin_code = #{vinCode}, + transaction_identifier = #{transactionIdentifier}, + transaction_time = #{transactionTime}, + stop_reason_msg = #{stopReasonMsg}, + logic_card = #{logicCard}, + create_time = #{createTime}, + + where id = #{id} + + + + delete from order_abnormal_record where id = #{id} + + + + delete from order_abnormal_record where id in + + #{id} + + + \ No newline at end of file diff --git a/jsowell-pile/src/main/resources/mapper/pile/OrderBasicInfoMapper.xml b/jsowell-pile/src/main/resources/mapper/pile/OrderBasicInfoMapper.xml new file mode 100644 index 000000000..a080e84ab --- /dev/null +++ b/jsowell-pile/src/main/resources/mapper/pile/OrderBasicInfoMapper.xml @@ -0,0 +1,775 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select + + from order_basic_info + + + + id, + order_code, + order_status, + member_id, + station_id, + pile_sn, + connector_code, + pile_connector_code, + start_mode, + pay_mode, + pay_status, + pay_amount, + pay_time, + order_amount, + charge_start_time, + charge_end_time, + start_soc, + end_soc, + reason, + settlement_time, + refund_amount, + create_by, + create_time, + update_by, + update_time, + del_flag + + + + id, + order_code, + total_used_electricity, + total_order_amount, + total_electricity_amount, + total_service_amount, + sharp_used_electricity, + sharp_electricity_price, + sharp_service_price, + peak_used_electricity, + peak_electricity_price, + peak_service_price, + flat_used_electricity, + flat_electricity_price, + flat_service_price, + valley_used_electricity, + valley_electricity_price, + valley_service_price, + create_by, + create_time, + update_by, + update_time, + del_flag + + + + + + + + insert into order_basic_info + + + order_code, + + + order_status, + + + member_id, + + + station_id, + + + pile_sn, + + + connector_code, + + + pile_connector_code, + + + start_mode, + + + pay_mode, + + + pay_status, + + + pay_amount, + + + pay_time, + + + order_amount, + + + charge_start_time, + + + charge_end_time, + + + start_soc, + + + end_soc, + + + reason, + + + settlement_time, + + + refund_amount, + + + create_by, + + + create_time, + + + update_by, + + + update_time, + + + del_flag, + + + + + #{orderCode}, + + + #{orderStatus}, + + + #{memberId}, + + + #{stationId}, + + + #{pileSn,jdbcType=VARCHAR}, + + + #{connectorCode}, + + + #{pileConnectorCode}, + + + #{startMode}, + + + #{payMode}, + + + #{payStatus}, + + + #{payAmount}, + + + #{payTime}, + + + #{orderAmount}, + + + #{chargeStartTime}, + + + #{chargeEndTime}, + + + #{startSOC}, + + + #{endSOC}, + + + #{reason}, + + + #{settlementTime}, + + + #{refundAmount}, + + + #{createBy}, + + + #{createTime}, + + + #{updateBy}, + + + #{updateTime}, + + + #{delFlag}, + + + + + + update order_basic_info + + + order_code = #{orderCode}, + + + order_status = #{orderStatus}, + + + member_id = #{memberId}, + + + station_id = #{stationId}, + + + pile_sn = #{pileSn}, + + + connector_code = #{connectorCode}, + + + pile_connector_code = #{pileConnectorCode}, + + + start_mode = #{startMode}, + + + pay_mode = #{payMode}, + + + pay_status = #{payStatus}, + + + pay_amount = #{payAmount}, + + + pay_time = #{payTime}, + + + order_amount = #{orderAmount}, + + + charge_start_time = #{chargeStartTime}, + + + charge_end_time = #{chargeEndTime}, + + + start_soc = #{startSOC}, + + + end_soc = #{endSOC}, + + + reason = #{reason}, + + + settlement_time = #{settlementTime}, + + + refund_amount = #{refundAmount}, + + + create_by = #{createBy}, + + + create_time = #{createTime}, + + + update_by = #{updateBy}, + + + update_time = #{updateTime}, + + + del_flag = #{delFlag}, + + + where id = #{id} + + + + delete + from order_basic_info where id in + + #{id} + + + + + delete + from order_detail where order_code in + + #{orderCode} + + + + + delete + from order_detail + where order_code = #{orderCode} + + + + insert into order_detail(id, order_code, total_used_electricity, total_order_amount, total_electricity_amount, + total_service_amount, sharp_used_electricity, sharp_electricity_price, + sharp_service_price, + peak_used_electricity, peak_electricity_price, peak_service_price, + flat_used_electricity, flat_electricity_price, + flat_service_price, valley_used_electricity, valley_electricity_price, + valley_service_price, create_by, + update_by) values + + (#{item.id}, #{item.orderCode}, #{item.totalUsedElectricity}, #{item.totalOrderAmount}, + #{item.totalElectricityAmount}, #{item.totalServiceAmount}, #{item.sharpUsedElectricity}, + #{item.sharpElectricityPrice}, #{item.sharpServicePrice}, #{item.peakUsedElectricity}, + #{item.peakElectricityPrice}, #{item.peakServicePrice}, #{item.flatUsedElectricity}, + #{item.flatElectricityPrice}, #{item.flatServicePrice}, #{item.valleyUsedElectricity}, + #{item.valleyElectricityPrice}, #{item.valleyServicePrice}, #{item.createBy}, #{item.updateBy}) + + + + + update order_detail + + + order_code = #{orderCode}, + + + total_used_electricity = #{totalUsedElectricity}, + + + total_order_amount = #{totalOrderAmount}, + + + total_electricity_amount = #{totalElectricityAmount}, + + + total_service_amount = #{totalServiceAmount}, + + + sharp_used_electricity = #{sharpUsedElectricity}, + + + sharp_electricity_price = #{sharpElectricityPrice}, + + + sharp_service_price = #{sharpServicePrice}, + + + peak_used_electricity = #{peakUsedElectricity}, + + + peak_electricity_price = #{peakElectricityPrice}, + + + peak_service_price = #{peakServicePrice}, + + + flat_used_electricity = #{flatUsedElectricity}, + + + flat_electricity_price = #{flatElectricityPrice}, + + + flat_service_price = #{flatServicePrice}, + + + valley_used_electricity = #{valleyUsedElectricity}, + + + valley_electricity_price = #{valleyElectricityPrice}, + + + valley_service_price = #{valleyServicePrice}, + + + create_by = #{createBy}, + + + create_time = #{createTime}, + + + update_by = #{updateBy}, + + + update_time = #{updateTime}, + + + del_flag = #{delFlag}, + + + where id = #{id} + + + + + + + + + + + + + update order_basic_info set order_status = #{orderStatus,jdbcType=VARCHAR} + where del_flag = '0' + and order_code = #{orderCode,jdbcType=VARCHAR} + + + + + + + + + + update order_basic_info set order_status = #{orderStatus,jdbcType=VARCHAR} + where del_flag = '0' + and id in + + #{orderId,jdbcType=VARCHAR} + + + + + + + + + + \ No newline at end of file diff --git a/jsowell-pile/src/main/resources/mapper/pile/OrderPayRecordMapper.xml b/jsowell-pile/src/main/resources/mapper/pile/OrderPayRecordMapper.xml new file mode 100644 index 000000000..340961104 --- /dev/null +++ b/jsowell-pile/src/main/resources/mapper/pile/OrderPayRecordMapper.xml @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + id, order_code, pay_mode, pay_amount, refund_amount, create_by, create_time, update_by, update_time, + del_flag + + + + + delete from order_pay_record + where id = #{id,jdbcType=INTEGER} + + + + insert into order_pay_record (order_code, pay_mode, pay_amount, refund_amount, + create_by, create_time, update_by, + update_time, del_flag) + values (#{orderCode,jdbcType=VARCHAR}, #{payMode,jdbcType=VARCHAR}, #{payAmount,jdbcType=DECIMAL}, #{refundAmount,jdbcType=DECIMAL}, + #{createBy,jdbcType=VARCHAR}, #{createTime,jdbcType=TIMESTAMP}, #{updateBy,jdbcType=VARCHAR}, + #{updateTime,jdbcType=TIMESTAMP}, #{delFlag,jdbcType=CHAR}) + + + + insert into order_pay_record + + + order_code, + + + pay_mode, + + + pay_amount, + + + refund_amount, + + + create_by, + + + create_time, + + + update_by, + + + update_time, + + + del_flag, + + + + + #{orderCode,jdbcType=VARCHAR}, + + + #{payMode,jdbcType=VARCHAR}, + + + #{payAmount,jdbcType=DECIMAL}, + + + #{refundAmount,jdbcType=DECIMAL}, + + + #{createBy,jdbcType=VARCHAR}, + + + #{createTime,jdbcType=TIMESTAMP}, + + + #{updateBy,jdbcType=VARCHAR}, + + + #{updateTime,jdbcType=TIMESTAMP}, + + + #{delFlag,jdbcType=CHAR}, + + + + + + update order_pay_record + + + order_code = #{orderCode,jdbcType=VARCHAR}, + + + pay_mode = #{payMode,jdbcType=VARCHAR}, + + + pay_amount = #{payAmount,jdbcType=DECIMAL}, + + + refund_amount = #{refundAmount,jdbcType=DECIMAL}, + + + create_by = #{createBy,jdbcType=VARCHAR}, + + + create_time = #{createTime,jdbcType=TIMESTAMP}, + + + update_by = #{updateBy,jdbcType=VARCHAR}, + + + update_time = #{updateTime,jdbcType=TIMESTAMP}, + + + del_flag = #{delFlag,jdbcType=CHAR}, + + + where id = #{id,jdbcType=INTEGER} + + + + update order_pay_record + set order_code = #{orderCode,jdbcType=VARCHAR}, + pay_mode = #{payMode,jdbcType=VARCHAR}, + pay_amount = #{payAmount,jdbcType=DECIMAL}, + refund_amount = #{refundAmount,jdbcType=DECIMAL}, + create_by = #{createBy,jdbcType=VARCHAR}, + create_time = #{createTime,jdbcType=TIMESTAMP}, + update_by = #{updateBy,jdbcType=VARCHAR}, + update_time = #{updateTime,jdbcType=TIMESTAMP}, + del_flag = #{delFlag,jdbcType=CHAR} + where id = #{id,jdbcType=INTEGER} + + + + insert into order_pay_record + (order_code, pay_mode, pay_amount, create_by) + values + + ( + #{item.orderCode,jdbcType=VARCHAR}, #{item.payMode,jdbcType=VARCHAR}, #{item.payAmount,jdbcType=DECIMAL}, + #{item.createBy,jdbcType=VARCHAR} + ) + + + + + \ No newline at end of file diff --git a/jsowell-pile/src/main/resources/mapper/pile/PileBasicInfoMapper.xml b/jsowell-pile/src/main/resources/mapper/pile/PileBasicInfoMapper.xml new file mode 100644 index 000000000..97dbaa675 --- /dev/null +++ b/jsowell-pile/src/main/resources/mapper/pile/PileBasicInfoMapper.xml @@ -0,0 +1,345 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + id, sn, business_type, software_protocol, production_date, licence_id, model_id, sim_id, + merchant_id, station_id, fault_reason, create_by, create_time, update_by, update_time, del_flag, remark + + + + select + from pile_basic_info + + + + + + + + + + insert into pile_basic_info + + sn, + business_type, + software_protocol, + production_date, + licence_id, + model_id, + sim_id, + merchant_id, + station_id, + fault_reason, + create_by, + create_time, + update_by, + update_time, + del_flag, + remark, + + + #{sn}, + #{businessType}, + #{softwareProtocol}, + #{productionDate}, + #{licenceId}, + #{modelId}, + #{simId}, + #{merchantId}, + #{stationId}, + #{faultReason}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + #{delFlag}, + #{remark}, + + + + + update pile_basic_info + + sn = #{sn}, + business_type = #{businessType}, + software_protocol = #{softwareProtocol}, + production_date = #{productionDate}, + licence_id = #{licenceId}, + model_id = #{modelId}, + sim_id = #{simId}, + merchant_id = #{merchantId}, + station_id = #{stationId}, + fault_reason = #{faultReason}, + create_by = #{createBy}, + create_time = #{createTime}, + update_by = #{updateBy}, + update_time = #{updateTime}, + del_flag = #{delFlag}, + remark = #{remark}, + + where id = #{id} + + + + delete from pile_basic_info where id = #{id} + + + + delete from pile_basic_info where id in + + #{id} + + + + + + + insert into pile_basic_info + (sn, business_type, software_protocol, production_date, licence_id, model_id, sim_id, + merchant_id, station_id, fault_reason, create_by, update_by, del_flag, remark) + values + + ( + #{item.sn,jdbcType=VARCHAR}, + #{item.businessType,jdbcType=VARCHAR}, + #{item.softwareProtocol,jdbcType=VARCHAR}, + #{item.productionDate,jdbcType=TIMESTAMP}, + #{item.licenceId,jdbcType=BIGINT}, + #{item.modelId,jdbcType=BIGINT}, + #{item.simId,jdbcType=BIGINT}, + #{item.merchantId,jdbcType=BIGINT}, + #{item.stationId,jdbcType=BIGINT}, + #{item.faultReason,jdbcType=VARCHAR}, + #{item.createBy,jdbcType=VARCHAR}, + #{item.updateBy,jdbcType=VARCHAR}, + #{item.delFlag,jdbcType=VARCHAR}, + #{item.remark,jdbcType=VARCHAR} + ) + + + + + update pile_basic_info + + merchant_id = #{merchantId}, + station_id = #{stationId}, + model_id = #{modelId}, + update_by = #{updateBy}, + update_time = #{updateTime}, + + where id in + + #{pileId,jdbcType=BIGINT} + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jsowell-pile/src/main/resources/mapper/pile/PileBillingTemplateMapper.xml b/jsowell-pile/src/main/resources/mapper/pile/PileBillingTemplateMapper.xml new file mode 100644 index 000000000..5efe1f55d --- /dev/null +++ b/jsowell-pile/src/main/resources/mapper/pile/PileBillingTemplateMapper.xml @@ -0,0 +1,433 @@ + + + + + + + + + + + + + + + + + + + + + id, template_code, name, remark, type, station_id, public_flag, + create_time, update_by, update_time, del_flag + + + + + + + + + + + + + + + + + + + id, template_code, time_type, electricity_price, service_price, apply_time, create_by, + create_time, update_by, update_time, del_flag + + + + + + + + + + + + + + + + + + + + select + + from pile_billing_template + + + + + + + + insert into pile_billing_template + + + template_code, + + + public_flag, + + + name, + + + remark, + + + type, + + + station_id, + + + create_by, + + + create_time, + + + update_by, + + + update_time, + + + del_flag, + + + + + #{templateCode}, + + + #{publicFlag}, + + + #{name}, + + + #{remark}, + + + #{type}, + + + #{stationId}, + + + #{createBy}, + + + #{createTime}, + + + #{updateBy}, + + + #{updateTime}, + + + #{delFlag}, + + + + + + update pile_billing_template + + + template_code = #{templateCode}, + + + public_flag = #{publicFlag}, + + + name = #{name}, + + + remark = #{remark}, + + + type = #{type}, + + + station_id = #{stationId}, + + + publish_time = #{publishTime}, + + + create_by = #{createBy}, + + + create_time = #{createTime}, + + + update_by = #{updateBy}, + + + update_time = #{updateTime}, + + + del_flag = #{delFlag}, + + + where id = #{id} + + + + delete + from pile_billing_template + where id = #{id} + + + + delete + from pile_billing_template where id in + + #{id} + + + + + delete + from pile_billing_detail + where template_code in + + #{templateCode} + + + + + delete + from pile_billing_detail + where template_code = #{templateCode} + + + + insert into pile_billing_detail + (id, template_code, time_type, electricity_price, service_price, apply_time, + create_by, update_by) + values + + (#{item.id}, #{item.templateCode}, #{item.timeType}, #{item.electricityPrice}, #{item.servicePrice}, + #{item.applyTime}, #{item.createBy}, #{item.updateBy}) + + + + + + + + + + + + + + + insert into pile_billing_relation + (pile_sn, billing_template_code, station_id) + values + + (#{item.pileSn}, #{item.billingTemplateCode}, #{item.stationId}) + + + + + delete + from pile_billing_relation + where pile_sn in + + #{pileSn,jdbcType=VARCHAR} + + + + + \ No newline at end of file diff --git a/jsowell-pile/src/main/resources/mapper/pile/PileConnectorInfoMapper.xml b/jsowell-pile/src/main/resources/mapper/pile/PileConnectorInfoMapper.xml new file mode 100644 index 000000000..8144bc722 --- /dev/null +++ b/jsowell-pile/src/main/resources/mapper/pile/PileConnectorInfoMapper.xml @@ -0,0 +1,239 @@ + + + + + + + + + + + + + + + + + + + select id, name, pile_connector_code, status, pile_sn, create_by, create_time, update_by, update_time, del_flag from pile_connector_info + + + + + + + + insert into pile_connector_info + + name, + pile_connector_code, + status, + pile_sn, + create_by, + create_time, + update_by, + update_time, + del_flag, + + + #{name}, + #{pileConnectorCode}, + #{status}, + #{pileSn}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + #{delFlag}, + + + + + update pile_connector_info + + name = #{name}, + pile_connector_code = #{pileConnectorCode}, + status = #{status}, + pile_sn = #{pileSn}, + create_by = #{createBy}, + create_time = #{createTime}, + update_by = #{updateBy}, + update_time = #{updateTime}, + del_flag = #{delFlag}, + + where id = #{id} + + + + delete from pile_connector_info where id = #{id} + + + + delete from pile_connector_info where id in + + #{id} + + + + + delete from pile_connector_info where pile_sn in + + #{pileSn,jdbcType=VARCHAR} + + + + + + + insert into pile_connector_info + (name,pile_connector_code,status,pile_sn,create_by,update_by,del_flag) + values + + ( + #{item.name,jdbcType=VARCHAR}, + #{item.pileConnectorCode,jdbcType=VARCHAR}, + #{item.status,jdbcType=VARCHAR}, + #{item.pileSn,jdbcType=VARCHAR}, + #{item.createBy,jdbcType=VARCHAR}, + #{item.updateBy,jdbcType=VARCHAR}, + #{item.delFlag,jdbcType=VARCHAR} + ) + + + + + + + + + + + update pile_connector_info + set status = #{connectorStatus,jdbcType=VARCHAR} + where pile_connector_code = #{connectorCode,jdbcType=VARCHAR} + + + + update pile_connector_info + set status = #{connectorStatus,jdbcType=VARCHAR} + where pile_sn = #{pileSn,jdbcType=VARCHAR} + + + + \ No newline at end of file diff --git a/jsowell-pile/src/main/resources/mapper/pile/PileLicenceInfoMapper.xml b/jsowell-pile/src/main/resources/mapper/pile/PileLicenceInfoMapper.xml new file mode 100644 index 000000000..1daaf7d57 --- /dev/null +++ b/jsowell-pile/src/main/resources/mapper/pile/PileLicenceInfoMapper.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + select id, licence_name, merchant_id, status, expire_time, create_by, create_time, update_by, update_time, del_flag from pile_licence_info + + + + + + + + insert into pile_licence_info + + id, + licence_name, + merchant_id, + status, + expire_time, + create_by, + create_time, + update_by, + update_time, + del_flag, + + + #{id}, + #{licenceName}, + #{merchantId}, + #{status}, + #{expireTime}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + #{delFlag}, + + + + + update pile_licence_info + + licence_name = #{licenceName}, + merchant_id = #{merchantId}, + status = #{status}, + expire_time = #{expireTime}, + create_by = #{createBy}, + create_time = #{createTime}, + update_by = #{updateBy}, + update_time = #{updateTime}, + del_flag = #{delFlag}, + + where id = #{id} + + + + delete from pile_licence_info where id = #{id} + + + + delete from pile_licence_info where id in + + #{id} + + + \ No newline at end of file diff --git a/jsowell-pile/src/main/resources/mapper/pile/PileMemberRelationMapper.xml b/jsowell-pile/src/main/resources/mapper/pile/PileMemberRelationMapper.xml new file mode 100644 index 000000000..2fcffd640 --- /dev/null +++ b/jsowell-pile/src/main/resources/mapper/pile/PileMemberRelationMapper.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + select id, pile_sn, member_id, type, create_time from pile_member_relation + + + + id, pile_sn, member_id, type, create_time + + + + + + + + insert into pile_member_relation + + pile_sn, + member_id, + type, + + + #{pileSn}, + #{memberId}, + #{type}, + + + + + update pile_member_relation + + pile_sn = #{pileSn}, + member_id = #{memberId}, + type = #{type}, + + where id = #{id} + + + + delete from pile_member_relation where id = #{id} + + + + delete from pile_member_relation where id in + + #{id} + + + + + + + \ No newline at end of file diff --git a/jsowell-pile/src/main/resources/mapper/pile/PileMerchantInfoMapper.xml b/jsowell-pile/src/main/resources/mapper/pile/PileMerchantInfoMapper.xml new file mode 100644 index 000000000..0063370cf --- /dev/null +++ b/jsowell-pile/src/main/resources/mapper/pile/PileMerchantInfoMapper.xml @@ -0,0 +1,262 @@ + + + + + + + + + + + + + + + + + + + + + + + + id, + merchant_name, + address, + status, + organization_code, + manager_name, + manager_phone, + service_phone, + logo_url, + app_id, + dept_id, + create_by, + create_time, + update_by, + update_time, + del_flag + + + + select + + from pile_merchant_info t + + + + + + + + insert into pile_merchant_info + + + id, + + + merchant_name, + + + address, + + + status, + + + organization_code, + + + manager_name, + + + manager_phone, + + + service_phone, + + + logo_url, + + + app_id, + + + dept_id, + + + create_by, + + + create_time, + + + update_by, + + + update_time, + + + del_flag, + + + + + #{id}, + + + #{merchantName}, + + + #{address}, + + + #{status}, + + + #{organizationCode}, + + + #{managerName}, + + + #{managerPhone}, + + + #{servicePhone}, + + + #{logoUrl}, + + + #{appId}, + + + #{deptId}, + + + #{createBy}, + + + #{createTime}, + + + #{updateBy}, + + + #{updateTime}, + + + #{delFlag}, + + + + + + update pile_merchant_info + + + merchant_name = #{merchantName}, + + + address = #{address}, + + + status = #{status}, + + + organization_code = #{organizationCode}, + + + manager_name = #{managerName}, + + + manager_phone = #{managerPhone}, + + + service_phone = #{servicePhone}, + + + logo_url = #{logoUrl}, + + + app_id = #{appId}, + + + create_by = #{createBy}, + + + create_time = #{createTime}, + + + update_by = #{updateBy}, + + + update_time = #{updateTime}, + + + del_flag = #{delFlag}, + + + where id = #{id} + + + + delete + from pile_merchant_info + where id = #{id} + + + + delete + from pile_merchant_info where id in + + #{id} + + + + + \ No newline at end of file diff --git a/jsowell-pile/src/main/resources/mapper/pile/PileModelInfoMapper.xml b/jsowell-pile/src/main/resources/mapper/pile/PileModelInfoMapper.xml new file mode 100644 index 000000000..03084b571 --- /dev/null +++ b/jsowell-pile/src/main/resources/mapper/pile/PileModelInfoMapper.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + select + + from pile_model_info + + + + id, model_name,rated_power, rated_current, rated_voltage, speed_type, charger_pile_type, connector_num, interface_standard, create_by, + create_time, update_by, update_time, del_flag + + + + + + + + insert into pile_model_info + + model_name, + rated_power, + rated_current, + rated_voltage, + speed_type, + charger_pile_type, + connector_num, + interface_standard, + create_by, + create_time, + update_by, + update_time, + del_flag, + + + #{modelName}, + #{ratedPower}, + #{ratedCurrent}, + #{ratedVoltage}, + #{speedType}, + #{chargerPileType}, + #{connectorNum}, + #{interfaceStandard}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + #{delFlag}, + + + + + update pile_model_info + + model_name = #{modelName}, + rated_power = #{ratedPower}, + rated_current = #{ratedCurrent}, + rated_voltage = #{ratedVoltage}, + speed_type = #{speedType}, + charger_pile_type = #{chargerPileType}, + connector_num = #{connectorNum}, + interface_standard = #{interfaceStandard}, + create_by = #{createBy}, + create_time = #{createTime}, + update_by = #{updateBy}, + update_time = #{updateTime}, + del_flag = #{delFlag}, + + where id = #{id} + + + + delete from pile_model_info where id = #{id} + + + + delete from pile_model_info where id in + + #{id} + + + + + + + \ No newline at end of file diff --git a/jsowell-pile/src/main/resources/mapper/pile/PileMsgRecordMapper.xml b/jsowell-pile/src/main/resources/mapper/pile/PileMsgRecordMapper.xml new file mode 100644 index 000000000..bf174de4b --- /dev/null +++ b/jsowell-pile/src/main/resources/mapper/pile/PileMsgRecordMapper.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + id, pile_sn, frame_type, connector_code, pile_connector_code, original_msg, json_msg, create_time + + + + + + insert into pile_msg_record + + + id, + + + pile_sn, + + + frame_type, + + + connector_code, + + + pile_connector_code, + + + original_msg, + + + json_msg, + + + create_time, + + + + + #{id,jdbcType=INTEGER}, + + + #{pileSn,jdbcType=VARCHAR}, + + + #{frameType,jdbcType=VARCHAR}, + + + #{connectorCode,jdbcType=VARCHAR}, + + + #{pileConnectorCode,jdbcType=VARCHAR}, + + + #{originalMsg,jdbcType=VARCHAR}, + + + #{jsonMsg,jdbcType=VARCHAR}, + + + #{createTime,jdbcType=TIMESTAMP}, + + + + + + + + + + + + \ No newline at end of file diff --git a/jsowell-pile/src/main/resources/mapper/pile/PileSimInfoMapper.xml b/jsowell-pile/src/main/resources/mapper/pile/PileSimInfoMapper.xml new file mode 100644 index 000000000..8eb36457f --- /dev/null +++ b/jsowell-pile/src/main/resources/mapper/pile/PileSimInfoMapper.xml @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + + + + + + + + select id, name, iccid, status, sim_supplier, total_data, surplus_data, expire_time, operator, create_by, create_time, update_by, update_time, del_flag from pile_sim_info + + + + + + + + insert into pile_sim_info + + id, + name, + iccid, + status, + sim_supplier, + total_data, + surplus_data, + expire_time, + operator, + create_by, + create_time, + update_by, + update_time, + del_flag, + + + #{id}, + #{name}, + #{iccid}, + #{status}, + #{simSupplier}, + #{totalData}, + #{surplusData}, + #{expireTime}, + #{operator}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + #{delFlag}, + + + + + update pile_sim_info + + name = #{name}, + iccid = #{iccid}, + status = #{status}, + sim_supplier = #{simSupplier}, + total_data = #{totalData}, + surplus_data = #{surplusData}, + expire_time = #{expireTime}, + operator = #{operator}, + create_by = #{createBy}, + create_time = #{createTime}, + update_by = #{updateBy}, + update_time = #{updateTime}, + del_flag = #{delFlag}, + + where id = #{id} + + + + delete from pile_sim_info where id = #{id} + + + + delete from pile_sim_info where id in + + #{id} + + + + + id, name, iccid, status, sim_supplier, total_data, surplus_data, expire_time, operator, create_by, create_time, + update_by, del_flag + + + + + + + + + + \ No newline at end of file diff --git a/jsowell-pile/src/main/resources/mapper/pile/PileStationInfoMapper.xml b/jsowell-pile/src/main/resources/mapper/pile/PileStationInfoMapper.xml new file mode 100644 index 000000000..4114bf55c --- /dev/null +++ b/jsowell-pile/src/main/resources/mapper/pile/PileStationInfoMapper.xml @@ -0,0 +1,352 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select id,merchant_id, station_name, dept_id, alone_apply, account_number, capacity, public_parking, parking_number, + country_code, area_code, address, station_tel, service_tel, station_type, station_status, station_admin_name, park_nums, + station_lng, station_lat, site_guide, construction, pictures, match_cars, park_info, park_owner, + park_manager, open_all_day, business_hours, park_free, payment, support_order, remark, public_flag, + open_flag,toilet_flag, store_flag, restaurant_flag, lounge_flag, canopy_flag, printer_flag, barrier_flag, + parking_lock_flag, create_by, create_time, update_by, update_time, del_flag + from pile_station_info + + + + + + + + insert into pile_station_info + + id, + merchant_id, + station_name, + dept_id, + alone_apply, + account_number, + capacity, + public_parking, + parking_number, + country_code, + area_code, + address, + station_tel, + service_tel, + station_type, + station_status, + station_admin_name, + park_nums, + station_lng, + station_lat, + site_guide, + construction, + pictures, + match_cars, + park_info, + park_owner, + park_manager, + open_all_day, + business_hours, + park_free, + payment, + support_order, + remark, + publicFlag, + openFlag, + toilet_flag, + store_flag, + restaurant_flag, + lounge_flag, + canopy_flag, + printer_flag, + barrier_flag, + parking_lock_flag, + create_by, + create_time, + update_by, + update_time, + del_flag, + + + #{id}, + #{merchantId}, + #{stationName}, + #{deptId}, + #{aloneApply}, + #{accountNumber}, + #{capacity}, + #{publicParking}, + #{parkingNumber}, + #{countryCode}, + #{areaCode}, + #{address}, + #{stationTel}, + #{serviceTel}, + #{stationType}, + #{stationStatus}, + #{stationAdminName}, + #{parkNums}, + #{stationLng}, + #{stationLat}, + #{siteGuide}, + #{construction}, + #{pictures}, + #{matchCars}, + #{parkInfo}, + #{parkOwner}, + #{parkManager}, + #{openAllDay}, + #{businessHours}, + #{parkFree}, + #{payment}, + #{supportOrder}, + #{remark}, + #{publicFlag}, + #{openFlag}, + #{toiletFlag}, + #{storeFlag}, + #{restaurantFlag}, + #{loungeFlag}, + #{canopyFlag}, + #{printerFlag}, + #{barrierFlag}, + #{parkingLockFlag}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + #{delFlag}, + + + + + update pile_station_info + + merchant_id = #{merchantId}, + station_name = #{stationName}, + dept_id = #{deptId}, + alone_apply = #{aloneApply}, + account_number = #{accountNumber}, + capacity = #{capacity}, + public_parking = #{publicParking}, + parking_number = #{parkingNumber}, + country_code = #{countryCode}, + area_code = #{areaCode}, + address = #{address}, + station_tel = #{stationTel}, + service_tel = #{serviceTel}, + station_type = #{stationType}, + station_status = #{stationStatus}, + station_admin_name = #{stationAdminName}, + park_nums = #{parkNums}, + station_lng = #{stationLng}, + station_lat = #{stationLat}, + site_guide = #{siteGuide}, + construction = #{construction}, + pictures = #{pictures}, + match_cars = #{matchCars}, + park_info = #{parkInfo}, + park_owner = #{parkOwner}, + park_manager = #{parkManager}, + open_all_day = #{openAllDay}, + business_hours = #{businessHours}, + park_free = #{parkFree}, + payment = #{payment}, + support_order = #{supportOrder}, + remark = #{remark}, + public_flag = #{publicFlag}, + open_flag = #{openFlag}, + toilet_flag = #{toiletFlag}, + store_flag = #{storeFlag}, + restaurant_flag = #{restaurantFlag}, + lounge_flag = #{loungeFlag}, + canopy_flag = #{canopyFlag}, + printer_flag = #{printerFlag}, + barrier_flag = #{barrierFlag}, + parking_lock_flag = #{parkingLockFlag}, + create_by = #{createBy}, + create_time = #{createTime}, + update_by = #{updateBy}, + update_time = #{updateTime}, + del_flag = #{delFlag}, + + where id = #{id} + + + + delete from pile_station_info where id in + + #{id} + + + + + + + + \ No newline at end of file diff --git a/jsowell-pile/src/main/resources/mapper/pile/WxpayCallbackRecordMapper.xml b/jsowell-pile/src/main/resources/mapper/pile/WxpayCallbackRecordMapper.xml new file mode 100644 index 000000000..ef5209d85 --- /dev/null +++ b/jsowell-pile/src/main/resources/mapper/pile/WxpayCallbackRecordMapper.xml @@ -0,0 +1,263 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + id, pay_scenario, member_id, order_code, out_trade_no, transaction_id, mch_id, app_id, trade_type, + trade_state, trade_state_desc, bank_type, attach, success_time, payer_open_id, payer_total, + create_time + + + + + delete from wxpay_callback_record + where id = #{id,jdbcType=INTEGER} + + + + insert into wxpay_callback_record (pay_scenario, member_id, order_code, out_trade_no, + transaction_id, mch_id, app_id, + trade_type, trade_state, trade_state_desc, + bank_type, attach, success_time, + payer_open_id, payer_total, create_time + ) + values (#{payScenario,jdbcType=VARCHAR}, #{memberId,jdbcType=VARCHAR}, #{orderCode,jdbcType=VARCHAR}, #{outTradeNo,jdbcType=VARCHAR}, + #{transactionId,jdbcType=VARCHAR}, #{mchId,jdbcType=VARCHAR}, #{appId,jdbcType=VARCHAR}, + #{tradeType,jdbcType=VARCHAR}, #{tradeState,jdbcType=VARCHAR}, #{tradeStateDesc,jdbcType=VARCHAR}, + #{bankType,jdbcType=VARCHAR}, #{attach,jdbcType=VARCHAR}, #{successTime,jdbcType=TIMESTAMP}, + #{payerOpenId,jdbcType=VARCHAR}, #{payerTotal,jdbcType=VARCHAR}, #{createTime,jdbcType=TIMESTAMP} + ) + + + + insert into wxpay_callback_record + + + pay_scenario, + + + member_id, + + + order_code, + + + out_trade_no, + + + transaction_id, + + + mch_id, + + + app_id, + + + trade_type, + + + trade_state, + + + trade_state_desc, + + + bank_type, + + + attach, + + + success_time, + + + payer_open_id, + + + payer_total, + + + create_time, + + + + + #{payScenario,jdbcType=VARCHAR}, + + + #{memberId,jdbcType=VARCHAR}, + + + #{orderCode,jdbcType=VARCHAR}, + + + #{outTradeNo,jdbcType=VARCHAR}, + + + #{transactionId,jdbcType=VARCHAR}, + + + #{mchId,jdbcType=VARCHAR}, + + + #{appId,jdbcType=VARCHAR}, + + + #{tradeType,jdbcType=VARCHAR}, + + + #{tradeState,jdbcType=VARCHAR}, + + + #{tradeStateDesc,jdbcType=VARCHAR}, + + + #{bankType,jdbcType=VARCHAR}, + + + #{attach,jdbcType=VARCHAR}, + + + #{successTime,jdbcType=TIMESTAMP}, + + + #{payerOpenId,jdbcType=VARCHAR}, + + + #{payerTotal,jdbcType=VARCHAR}, + + + #{createTime,jdbcType=TIMESTAMP}, + + + + + + update wxpay_callback_record + + + pay_scenario = #{payScenario,jdbcType=VARCHAR}, + + + member_id = #{memberId,jdbcType=VARCHAR}, + + + order_code = #{orderCode,jdbcType=VARCHAR}, + + + out_trade_no = #{outTradeNo,jdbcType=VARCHAR}, + + + transaction_id = #{transactionId,jdbcType=VARCHAR}, + + + mch_id = #{mchId,jdbcType=VARCHAR}, + + + app_id = #{appId,jdbcType=VARCHAR}, + + + trade_type = #{tradeType,jdbcType=VARCHAR}, + + + trade_state = #{tradeState,jdbcType=VARCHAR}, + + + trade_state_desc = #{tradeStateDesc,jdbcType=VARCHAR}, + + + bank_type = #{bankType,jdbcType=VARCHAR}, + + + attach = #{attach,jdbcType=VARCHAR}, + + + success_time = #{successTime,jdbcType=TIMESTAMP}, + + + payer_open_id = #{payerOpenId,jdbcType=VARCHAR}, + + + payer_total = #{payerTotal,jdbcType=VARCHAR}, + + + create_time = #{createTime,jdbcType=TIMESTAMP}, + + + where id = #{id,jdbcType=INTEGER} + + + + update wxpay_callback_record + set + pay_scenario = #{payScenario,jdbcType=VARCHAR}, + member_id = #{memberId,jdbcType=VARCHAR}, + order_code = #{orderCode,jdbcType=VARCHAR}, + out_trade_no = #{outTradeNo,jdbcType=VARCHAR}, + transaction_id = #{transactionId,jdbcType=VARCHAR}, + mch_id = #{mchId,jdbcType=VARCHAR}, + app_id = #{appId,jdbcType=VARCHAR}, + trade_type = #{tradeType,jdbcType=VARCHAR}, + trade_state = #{tradeState,jdbcType=VARCHAR}, + trade_state_desc = #{tradeStateDesc,jdbcType=VARCHAR}, + bank_type = #{bankType,jdbcType=VARCHAR}, + attach = #{attach,jdbcType=VARCHAR}, + success_time = #{successTime,jdbcType=TIMESTAMP}, + payer_open_id = #{payerOpenId,jdbcType=VARCHAR}, + payer_total = #{payerTotal,jdbcType=VARCHAR}, + create_time = #{createTime,jdbcType=TIMESTAMP} + where id = #{id,jdbcType=INTEGER} + + + + + + + + \ No newline at end of file diff --git a/jsowell-pile/src/main/resources/mapper/pile/WxpayRefundCallbackMapper.xml b/jsowell-pile/src/main/resources/mapper/pile/WxpayRefundCallbackMapper.xml new file mode 100644 index 000000000..c732fbbff --- /dev/null +++ b/jsowell-pile/src/main/resources/mapper/pile/WxpayRefundCallbackMapper.xml @@ -0,0 +1,227 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + id, member_id, order_code, out_trade_no, out_refund_no, transaction_id, mch_id, refund_id, + refund_status, success_time, user_received_account, payer_total, payer_refund, amount_total, + amount_refund, create_time + + + + + delete from wxpay_refund_callback + where id = #{id,jdbcType=INTEGER} + + + + insert into wxpay_refund_callback (member_id, order_code, out_trade_no, + out_refund_no, transaction_id, mch_id, + refund_id, refund_status, success_time, + user_received_account, payer_total, payer_refund, + amount_total, amount_refund, create_time + ) + values (#{memberId,jdbcType=VARCHAR}, #{orderCode,jdbcType=VARCHAR}, #{outTradeNo,jdbcType=VARCHAR}, + #{outRefundNo,jdbcType=VARCHAR}, #{transactionId,jdbcType=VARCHAR}, #{mchId,jdbcType=VARCHAR}, + #{refundId,jdbcType=VARCHAR}, #{refundStatus,jdbcType=VARCHAR}, #{successTime,jdbcType=VARCHAR}, + #{userReceivedAccount,jdbcType=VARCHAR}, #{payerTotal,jdbcType=VARCHAR}, #{payerRefund,jdbcType=VARCHAR}, + #{amountTotal,jdbcType=VARCHAR}, #{amountRefund,jdbcType=VARCHAR}, #{createTime,jdbcType=TIMESTAMP} + ) + + + + insert into wxpay_refund_callback + + + member_id, + + + order_code, + + + out_trade_no, + + + out_refund_no, + + + transaction_id, + + + mch_id, + + + refund_id, + + + refund_status, + + + success_time, + + + user_received_account, + + + payer_total, + + + payer_refund, + + + amount_total, + + + amount_refund, + + + create_time, + + + + + #{memberId,jdbcType=VARCHAR}, + + + #{orderCode,jdbcType=VARCHAR}, + + + #{outTradeNo,jdbcType=VARCHAR}, + + + #{outRefundNo,jdbcType=VARCHAR}, + + + #{transactionId,jdbcType=VARCHAR}, + + + #{mchId,jdbcType=VARCHAR}, + + + #{refundId,jdbcType=VARCHAR}, + + + #{refundStatus,jdbcType=VARCHAR}, + + + #{successTime,jdbcType=VARCHAR}, + + + #{userReceivedAccount,jdbcType=VARCHAR}, + + + #{payerTotal,jdbcType=VARCHAR}, + + + #{payerRefund,jdbcType=VARCHAR}, + + + #{amountTotal,jdbcType=VARCHAR}, + + + #{amountRefund,jdbcType=VARCHAR}, + + + #{createTime,jdbcType=TIMESTAMP}, + + + + + + update wxpay_refund_callback + + + member_id = #{memberId,jdbcType=VARCHAR}, + + + order_code = #{orderCode,jdbcType=VARCHAR}, + + + out_trade_no = #{outTradeNo,jdbcType=VARCHAR}, + + + out_refund_no = #{outRefundNo,jdbcType=VARCHAR}, + + + transaction_id = #{transactionId,jdbcType=VARCHAR}, + + + mch_id = #{mchId,jdbcType=VARCHAR}, + + + refund_id = #{refundId,jdbcType=VARCHAR}, + + + refund_status = #{refundStatus,jdbcType=VARCHAR}, + + + success_time = #{successTime,jdbcType=VARCHAR}, + + + user_received_account = #{userReceivedAccount,jdbcType=VARCHAR}, + + + payer_total = #{payerTotal,jdbcType=VARCHAR}, + + + payer_refund = #{payerRefund,jdbcType=VARCHAR}, + + + amount_total = #{amountTotal,jdbcType=VARCHAR}, + + + amount_refund = #{amountRefund,jdbcType=VARCHAR}, + + + create_time = #{createTime,jdbcType=TIMESTAMP}, + + + where id = #{id,jdbcType=INTEGER} + + + + update wxpay_refund_callback + set member_id = #{memberId,jdbcType=VARCHAR}, + order_code = #{orderCode,jdbcType=VARCHAR}, + out_trade_no = #{outTradeNo,jdbcType=VARCHAR}, + out_refund_no = #{outRefundNo,jdbcType=VARCHAR}, + transaction_id = #{transactionId,jdbcType=VARCHAR}, + mch_id = #{mchId,jdbcType=VARCHAR}, + refund_id = #{refundId,jdbcType=VARCHAR}, + refund_status = #{refundStatus,jdbcType=VARCHAR}, + success_time = #{successTime,jdbcType=VARCHAR}, + user_received_account = #{userReceivedAccount,jdbcType=VARCHAR}, + payer_total = #{payerTotal,jdbcType=VARCHAR}, + payer_refund = #{payerRefund,jdbcType=VARCHAR}, + amount_total = #{amountTotal,jdbcType=VARCHAR}, + amount_refund = #{amountRefund,jdbcType=VARCHAR}, + create_time = #{createTime,jdbcType=TIMESTAMP} + where id = #{id,jdbcType=INTEGER} + + \ No newline at end of file diff --git a/jsowell-quartz/pom.xml b/jsowell-quartz/pom.xml new file mode 100644 index 000000000..540ac9fcc --- /dev/null +++ b/jsowell-quartz/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + com.jsowell + jsowell-charger-web + 1.0.0 + + + jsowell-quartz + + + quartz定时任务 + + + + + + + org.quartz-scheduler + quartz + + + com.mchange + c3p0 + + + + + + + com.jsowell + jsowell-common + + + + + com.jsowell + jsowell-pile + 1.0.0 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.8 + 1.8 + + /src/test/** + + utf-8 + + + + + + \ No newline at end of file diff --git a/jsowell-quartz/src/main/java/com/jsowell/quartz/config/ScheduleConfig.java b/jsowell-quartz/src/main/java/com/jsowell/quartz/config/ScheduleConfig.java new file mode 100644 index 000000000..70e7364d8 --- /dev/null +++ b/jsowell-quartz/src/main/java/com/jsowell/quartz/config/ScheduleConfig.java @@ -0,0 +1,57 @@ +//package com.jsowell.quartz.config; +// +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.scheduling.quartz.SchedulerFactoryBean; +//import javax.sql.DataSource; +//import java.util.Properties; +// +///** +// * 定时任务配置(单机部署建议删除此类和qrtz数据库表,默认走内存会最高效) +// * +// * @author jsowell +// */ +//@Configuration +//public class ScheduleConfig +//{ +// @Bean +// public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) +// { +// SchedulerFactoryBean factory = new SchedulerFactoryBean(); +// factory.setDataSource(dataSource); +// +// // quartz参数 +// Properties prop = new Properties(); +// prop.put("org.quartz.scheduler.instanceName", "RuoyiScheduler"); +// prop.put("org.quartz.scheduler.instanceId", "AUTO"); +// // 线程池配置 +// prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool"); +// prop.put("org.quartz.threadPool.threadCount", "20"); +// prop.put("org.quartz.threadPool.threadPriority", "5"); +// // JobStore配置 +// prop.put("org.quartz.jobStore.class", "org.springframework.scheduling.quartz.LocalDataSourceJobStore"); +// // 集群配置 +// prop.put("org.quartz.jobStore.isClustered", "true"); +// prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000"); +// prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1"); +// prop.put("org.quartz.jobStore.txIsolationLevelSerializable", "true"); +// +// // sqlserver 启用 +// // prop.put("org.quartz.jobStore.selectWithLockSQL", "SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = ?"); +// prop.put("org.quartz.jobStore.misfireThreshold", "12000"); +// prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_"); +// factory.setQuartzProperties(prop); +// +// factory.setSchedulerName("RuoyiScheduler"); +// // 延时启动 +// factory.setStartupDelay(1); +// factory.setApplicationContextSchedulerContextKey("applicationContextKey"); +// // 可选,QuartzScheduler +// // 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了 +// factory.setOverwriteExistingJobs(true); +// // 设置自动启动,默认为true +// factory.setAutoStartup(true); +// +// return factory; +// } +//} diff --git a/jsowell-quartz/src/main/java/com/jsowell/quartz/controller/SysJobController.java b/jsowell-quartz/src/main/java/com/jsowell/quartz/controller/SysJobController.java new file mode 100644 index 000000000..cee7267ed --- /dev/null +++ b/jsowell-quartz/src/main/java/com/jsowell/quartz/controller/SysJobController.java @@ -0,0 +1,148 @@ +package com.jsowell.quartz.controller; + +import com.jsowell.common.annotation.Log; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.exception.job.TaskException; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.poi.ExcelUtil; +import com.jsowell.quartz.domain.SysJob; +import com.jsowell.quartz.service.ISysJobService; +import com.jsowell.quartz.util.CronUtils; +import com.jsowell.quartz.util.ScheduleUtils; +import org.quartz.SchedulerException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 调度任务信息操作处理 + * + * @author jsowell + */ +@RestController +@RequestMapping("/monitor/job") +public class SysJobController extends BaseController { + @Autowired + private ISysJobService jobService; + + /** + * 查询定时任务列表 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:list')") + @GetMapping("/list") + public TableDataInfo list(SysJob sysJob) { + startPage(); + List list = jobService.selectJobList(sysJob); + return getDataTable(list); + } + + /** + * 导出定时任务列表 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:export')") + @Log(title = "定时任务", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, SysJob sysJob) { + List list = jobService.selectJobList(sysJob); + ExcelUtil util = new ExcelUtil(SysJob.class); + util.exportExcel(response, list, "定时任务"); + } + + /** + * 获取定时任务详细信息 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:query')") + @GetMapping(value = "/{jobId}") + public AjaxResult getInfo(@PathVariable("jobId") Long jobId) { + return AjaxResult.success(jobService.selectJobById(jobId)); + } + + /** + * 新增定时任务 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:add')") + @Log(title = "定时任务", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody SysJob job) throws SchedulerException, TaskException { + if (!CronUtils.isValid(job.getCronExpression())) { + return error("新增任务'" + job.getJobName() + "'失败,Cron表达式不正确"); + } else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI)) { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用"); + } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS})) { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用"); + } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{Constants.HTTP, Constants.HTTPS})) { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用"); + } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR)) { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串存在违规"); + } else if (!ScheduleUtils.whiteList(job.getInvokeTarget())) { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不在白名单内"); + } + job.setCreateBy(getUsername()); + return toAjax(jobService.insertJob(job)); + } + + /** + * 修改定时任务 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:edit')") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody SysJob job) throws SchedulerException, TaskException { + if (!CronUtils.isValid(job.getCronExpression())) { + return error("修改任务'" + job.getJobName() + "'失败,Cron表达式不正确"); + } else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI)) { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用"); + } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS})) { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用"); + } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{Constants.HTTP, Constants.HTTPS})) { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用"); + } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR)) { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串存在违规"); + } else if (!ScheduleUtils.whiteList(job.getInvokeTarget())) { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不在白名单内"); + } + job.setUpdateBy(getUsername()); + return toAjax(jobService.updateJob(job)); + } + + /** + * 定时任务状态修改 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysJob job) throws SchedulerException { + SysJob newJob = jobService.selectJobById(job.getJobId()); + newJob.setStatus(job.getStatus()); + return toAjax(jobService.changeStatus(newJob)); + } + + /** + * 定时任务立即执行一次 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping("/run") + public AjaxResult run(@RequestBody SysJob job) throws SchedulerException { + boolean result = jobService.run(job); + return result ? success() : error("任务不存在或已过期!"); + } + + /** + * 删除定时任务 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:remove')") + @Log(title = "定时任务", businessType = BusinessType.DELETE) + @DeleteMapping("/{jobIds}") + public AjaxResult remove(@PathVariable Long[] jobIds) throws SchedulerException, TaskException { + jobService.deleteJobByIds(jobIds); + return success(); + } +} diff --git a/jsowell-quartz/src/main/java/com/jsowell/quartz/controller/SysJobLogController.java b/jsowell-quartz/src/main/java/com/jsowell/quartz/controller/SysJobLogController.java new file mode 100644 index 000000000..493d0a00c --- /dev/null +++ b/jsowell-quartz/src/main/java/com/jsowell/quartz/controller/SysJobLogController.java @@ -0,0 +1,82 @@ +package com.jsowell.quartz.controller; + +import com.jsowell.common.annotation.Log; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.common.util.poi.ExcelUtil; +import com.jsowell.quartz.domain.SysJobLog; +import com.jsowell.quartz.service.ISysJobLogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 调度日志操作处理 + * + * @author jsowell + */ +@RestController +@RequestMapping("/monitor/jobLog") +public class SysJobLogController extends BaseController { + @Autowired + private ISysJobLogService jobLogService; + + /** + * 查询定时任务调度日志列表 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:list')") + @GetMapping("/list") + public TableDataInfo list(SysJobLog sysJobLog) { + startPage(); + List list = jobLogService.selectJobLogList(sysJobLog); + return getDataTable(list); + } + + /** + * 导出定时任务调度日志列表 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:export')") + @Log(title = "任务调度日志", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, SysJobLog sysJobLog) { + List list = jobLogService.selectJobLogList(sysJobLog); + ExcelUtil util = new ExcelUtil(SysJobLog.class); + util.exportExcel(response, list, "调度日志"); + } + + /** + * 根据调度编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:query')") + @GetMapping(value = "/{configId}") + public AjaxResult getInfo(@PathVariable Long jobLogId) { + return AjaxResult.success(jobLogService.selectJobLogById(jobLogId)); + } + + + /** + * 删除定时任务调度日志 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:remove')") + @Log(title = "定时任务调度日志", businessType = BusinessType.DELETE) + @DeleteMapping("/{jobLogIds}") + public AjaxResult remove(@PathVariable Long[] jobLogIds) { + return toAjax(jobLogService.deleteJobLogByIds(jobLogIds)); + } + + /** + * 清空定时任务调度日志 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:remove')") + @Log(title = "调度日志", businessType = BusinessType.CLEAN) + @DeleteMapping("/clean") + public AjaxResult clean() { + jobLogService.cleanJobLog(); + return AjaxResult.success(); + } +} diff --git a/jsowell-quartz/src/main/java/com/jsowell/quartz/domain/SysJob.java b/jsowell-quartz/src/main/java/com/jsowell/quartz/domain/SysJob.java new file mode 100644 index 000000000..18e8b6f14 --- /dev/null +++ b/jsowell-quartz/src/main/java/com/jsowell/quartz/domain/SysJob.java @@ -0,0 +1,169 @@ +package com.jsowell.quartz.domain; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.jsowell.common.annotation.Excel; +import com.jsowell.common.annotation.Excel.ColumnType; +import com.jsowell.common.constant.ScheduleConstants; +import com.jsowell.common.core.domain.BaseEntity; +import com.jsowell.common.util.StringUtils; +import com.jsowell.quartz.util.CronUtils; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.util.Date; + +/** + * 定时任务调度表 sys_job + * + * @author jsowell + */ +public class SysJob extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * 任务ID + */ + @Excel(name = "任务序号", cellType = ColumnType.NUMERIC) + private Long jobId; + + /** + * 任务名称 + */ + @Excel(name = "任务名称") + private String jobName; + + /** + * 任务组名 + */ + @Excel(name = "任务组名") + private String jobGroup; + + /** + * 调用目标字符串 + */ + @Excel(name = "调用目标字符串") + private String invokeTarget; + + /** + * cron执行表达式 + */ + @Excel(name = "执行表达式 ") + private String cronExpression; + + /** + * cron计划策略 + */ + @Excel(name = "计划策略 ", readConverterExp = "0=默认,1=立即触发执行,2=触发一次执行,3=不触发立即执行") + private String misfirePolicy = ScheduleConstants.MISFIRE_DEFAULT; + + /** + * 是否并发执行(0允许 1禁止) + */ + @Excel(name = "并发执行", readConverterExp = "0=允许,1=禁止") + private String concurrent; + + /** + * 任务状态(0正常 1暂停) + */ + @Excel(name = "任务状态", readConverterExp = "0=正常,1=暂停") + private String status; + + public Long getJobId() { + return jobId; + } + + public void setJobId(Long jobId) { + this.jobId = jobId; + } + + @NotBlank(message = "任务名称不能为空") + @Size(min = 0, max = 64, message = "任务名称不能超过64个字符") + public String getJobName() { + return jobName; + } + + public void setJobName(String jobName) { + this.jobName = jobName; + } + + public String getJobGroup() { + return jobGroup; + } + + public void setJobGroup(String jobGroup) { + this.jobGroup = jobGroup; + } + + @NotBlank(message = "调用目标字符串不能为空") + @Size(min = 0, max = 500, message = "调用目标字符串长度不能超过500个字符") + public String getInvokeTarget() { + return invokeTarget; + } + + public void setInvokeTarget(String invokeTarget) { + this.invokeTarget = invokeTarget; + } + + @NotBlank(message = "Cron执行表达式不能为空") + @Size(min = 0, max = 255, message = "Cron执行表达式不能超过255个字符") + public String getCronExpression() { + return cronExpression; + } + + public void setCronExpression(String cronExpression) { + this.cronExpression = cronExpression; + } + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + public Date getNextValidTime() { + if (StringUtils.isNotEmpty(cronExpression)) { + return CronUtils.getNextExecution(cronExpression); + } + return null; + } + + public String getMisfirePolicy() { + return misfirePolicy; + } + + public void setMisfirePolicy(String misfirePolicy) { + this.misfirePolicy = misfirePolicy; + } + + public String getConcurrent() { + return concurrent; + } + + public void setConcurrent(String concurrent) { + this.concurrent = concurrent; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("jobId", getJobId()) + .append("jobName", getJobName()) + .append("jobGroup", getJobGroup()) + .append("cronExpression", getCronExpression()) + .append("nextValidTime", getNextValidTime()) + .append("misfirePolicy", getMisfirePolicy()) + .append("concurrent", getConcurrent()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/jsowell-quartz/src/main/java/com/jsowell/quartz/domain/SysJobLog.java b/jsowell-quartz/src/main/java/com/jsowell/quartz/domain/SysJobLog.java new file mode 100644 index 000000000..824bac9f9 --- /dev/null +++ b/jsowell-quartz/src/main/java/com/jsowell/quartz/domain/SysJobLog.java @@ -0,0 +1,155 @@ +package com.jsowell.quartz.domain; + +import com.jsowell.common.annotation.Excel; +import com.jsowell.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.Date; + +/** + * 定时任务调度日志表 sys_job_log + * + * @author jsowell + */ +public class SysJobLog extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Excel(name = "日志序号") + private Long jobLogId; + + /** + * 任务名称 + */ + @Excel(name = "任务名称") + private String jobName; + + /** + * 任务组名 + */ + @Excel(name = "任务组名") + private String jobGroup; + + /** + * 调用目标字符串 + */ + @Excel(name = "调用目标字符串") + private String invokeTarget; + + /** + * 日志信息 + */ + @Excel(name = "日志信息") + private String jobMessage; + + /** + * 执行状态(0正常 1失败) + */ + @Excel(name = "执行状态", readConverterExp = "0=正常,1=失败") + private String status; + + /** + * 异常信息 + */ + @Excel(name = "异常信息") + private String exceptionInfo; + + /** + * 开始时间 + */ + private Date startTime; + + /** + * 停止时间 + */ + private Date stopTime; + + public Long getJobLogId() { + return jobLogId; + } + + public void setJobLogId(Long jobLogId) { + this.jobLogId = jobLogId; + } + + public String getJobName() { + return jobName; + } + + public void setJobName(String jobName) { + this.jobName = jobName; + } + + public String getJobGroup() { + return jobGroup; + } + + public void setJobGroup(String jobGroup) { + this.jobGroup = jobGroup; + } + + public String getInvokeTarget() { + return invokeTarget; + } + + public void setInvokeTarget(String invokeTarget) { + this.invokeTarget = invokeTarget; + } + + public String getJobMessage() { + return jobMessage; + } + + public void setJobMessage(String jobMessage) { + this.jobMessage = jobMessage; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getExceptionInfo() { + return exceptionInfo; + } + + public void setExceptionInfo(String exceptionInfo) { + this.exceptionInfo = exceptionInfo; + } + + public Date getStartTime() { + return startTime; + } + + public void setStartTime(Date startTime) { + this.startTime = startTime; + } + + public Date getStopTime() { + return stopTime; + } + + public void setStopTime(Date stopTime) { + this.stopTime = stopTime; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("jobLogId", getJobLogId()) + .append("jobName", getJobName()) + .append("jobGroup", getJobGroup()) + .append("jobMessage", getJobMessage()) + .append("status", getStatus()) + .append("exceptionInfo", getExceptionInfo()) + .append("startTime", getStartTime()) + .append("stopTime", getStopTime()) + .toString(); + } +} diff --git a/jsowell-quartz/src/main/java/com/jsowell/quartz/mapper/SysJobLogMapper.java b/jsowell-quartz/src/main/java/com/jsowell/quartz/mapper/SysJobLogMapper.java new file mode 100644 index 000000000..98899d58d --- /dev/null +++ b/jsowell-quartz/src/main/java/com/jsowell/quartz/mapper/SysJobLogMapper.java @@ -0,0 +1,66 @@ +package com.jsowell.quartz.mapper; + +import com.jsowell.quartz.domain.SysJobLog; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 调度任务日志信息 数据层 + * + * @author jsowell + */ +@Repository +public interface SysJobLogMapper { + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * @return 调度任务日志集合 + */ + public List selectJobLogList(SysJobLog jobLog); + + /** + * 查询所有调度任务日志 + * + * @return 调度任务日志列表 + */ + public List selectJobLogAll(); + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * @return 调度任务日志对象信息 + */ + public SysJobLog selectJobLogById(Long jobLogId); + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + * @return 结果 + */ + public int insertJobLog(SysJobLog jobLog); + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的数据ID + * @return 结果 + */ + public int deleteJobLogByIds(Long[] logIds); + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + * @return 结果 + */ + public int deleteJobLogById(Long jobId); + + /** + * 清空任务日志 + */ + public void cleanJobLog(); +} diff --git a/jsowell-quartz/src/main/java/com/jsowell/quartz/mapper/SysJobMapper.java b/jsowell-quartz/src/main/java/com/jsowell/quartz/mapper/SysJobMapper.java new file mode 100644 index 000000000..c726a3618 --- /dev/null +++ b/jsowell-quartz/src/main/java/com/jsowell/quartz/mapper/SysJobMapper.java @@ -0,0 +1,69 @@ +package com.jsowell.quartz.mapper; + +import com.jsowell.quartz.domain.SysJob; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 调度任务信息 数据层 + * + * @author jsowell + */ +@Repository +public interface SysJobMapper { + /** + * 查询调度任务日志集合 + * + * @param job 调度信息 + * @return 操作日志集合 + */ + public List selectJobList(SysJob job); + + /** + * 查询所有调度任务 + * + * @return 调度任务列表 + */ + public List selectJobAll(); + + /** + * 通过调度ID查询调度任务信息 + * + * @param jobId 调度ID + * @return 角色对象信息 + */ + public SysJob selectJobById(Long jobId); + + /** + * 通过调度ID删除调度任务信息 + * + * @param jobId 调度ID + * @return 结果 + */ + public int deleteJobById(Long jobId); + + /** + * 批量删除调度任务信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteJobByIds(Long[] ids); + + /** + * 修改调度任务信息 + * + * @param job 调度任务信息 + * @return 结果 + */ + public int updateJob(SysJob job); + + /** + * 新增调度任务信息 + * + * @param job 调度任务信息 + * @return 结果 + */ + public int insertJob(SysJob job); +} diff --git a/jsowell-quartz/src/main/java/com/jsowell/quartz/service/ISysJobLogService.java b/jsowell-quartz/src/main/java/com/jsowell/quartz/service/ISysJobLogService.java new file mode 100644 index 000000000..e720277cd --- /dev/null +++ b/jsowell-quartz/src/main/java/com/jsowell/quartz/service/ISysJobLogService.java @@ -0,0 +1,56 @@ +package com.jsowell.quartz.service; + +import com.jsowell.quartz.domain.SysJobLog; + +import java.util.List; + +/** + * 定时任务调度日志信息信息 服务层 + * + * @author jsowell + */ +public interface ISysJobLogService { + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * @return 调度任务日志集合 + */ + public List selectJobLogList(SysJobLog jobLog); + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * @return 调度任务日志对象信息 + */ + public SysJobLog selectJobLogById(Long jobLogId); + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + */ + public void addJobLog(SysJobLog jobLog); + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的日志ID + * @return 结果 + */ + public int deleteJobLogByIds(Long[] logIds); + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + * @return 结果 + */ + public int deleteJobLogById(Long jobId); + + /** + * 清空任务日志 + */ + public void cleanJobLog(); +} diff --git a/jsowell-quartz/src/main/java/com/jsowell/quartz/service/ISysJobService.java b/jsowell-quartz/src/main/java/com/jsowell/quartz/service/ISysJobService.java new file mode 100644 index 000000000..04df9b3e4 --- /dev/null +++ b/jsowell-quartz/src/main/java/com/jsowell/quartz/service/ISysJobService.java @@ -0,0 +1,102 @@ +package com.jsowell.quartz.service; + +import com.jsowell.common.exception.job.TaskException; +import com.jsowell.quartz.domain.SysJob; +import org.quartz.SchedulerException; + +import java.util.List; + +/** + * 定时任务调度信息信息 服务层 + * + * @author jsowell + */ +public interface ISysJobService { + /** + * 获取quartz调度器的计划任务 + * + * @param job 调度信息 + * @return 调度任务集合 + */ + public List selectJobList(SysJob job); + + /** + * 通过调度任务ID查询调度信息 + * + * @param jobId 调度任务ID + * @return 调度任务对象信息 + */ + public SysJob selectJobById(Long jobId); + + /** + * 暂停任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int pauseJob(SysJob job) throws SchedulerException; + + /** + * 恢复任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int resumeJob(SysJob job) throws SchedulerException; + + /** + * 删除任务后,所对应的trigger也将被删除 + * + * @param job 调度信息 + * @return 结果 + */ + public int deleteJob(SysJob job) throws SchedulerException; + + /** + * 批量删除调度信息 + * + * @param jobIds 需要删除的任务ID + * @return 结果 + */ + public void deleteJobByIds(Long[] jobIds) throws SchedulerException; + + /** + * 任务调度状态修改 + * + * @param job 调度信息 + * @return 结果 + */ + public int changeStatus(SysJob job) throws SchedulerException; + + /** + * 立即运行任务 + * + * @param job 调度信息 + * @return 结果 + */ + public boolean run(SysJob job) throws SchedulerException; + + /** + * 新增任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int insertJob(SysJob job) throws SchedulerException, TaskException; + + /** + * 更新任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int updateJob(SysJob job) throws SchedulerException, TaskException; + + /** + * 校验cron表达式是否有效 + * + * @param cronExpression 表达式 + * @return 结果 + */ + public boolean checkCronExpressionIsValid(String cronExpression); +} diff --git a/jsowell-quartz/src/main/java/com/jsowell/quartz/service/impl/SysJobLogServiceImpl.java b/jsowell-quartz/src/main/java/com/jsowell/quartz/service/impl/SysJobLogServiceImpl.java new file mode 100644 index 000000000..56b1a1bd7 --- /dev/null +++ b/jsowell-quartz/src/main/java/com/jsowell/quartz/service/impl/SysJobLogServiceImpl.java @@ -0,0 +1,81 @@ +package com.jsowell.quartz.service.impl; + +import com.jsowell.quartz.domain.SysJobLog; +import com.jsowell.quartz.mapper.SysJobLogMapper; +import com.jsowell.quartz.service.ISysJobLogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 定时任务调度日志信息 服务层 + * + * @author jsowell + */ +@Service +public class SysJobLogServiceImpl implements ISysJobLogService { + @Autowired + private SysJobLogMapper jobLogMapper; + + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * @return 调度任务日志集合 + */ + @Override + public List selectJobLogList(SysJobLog jobLog) { + return jobLogMapper.selectJobLogList(jobLog); + } + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * @return 调度任务日志对象信息 + */ + @Override + public SysJobLog selectJobLogById(Long jobLogId) { + return jobLogMapper.selectJobLogById(jobLogId); + } + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + */ + @Override + public void addJobLog(SysJobLog jobLog) { + jobLogMapper.insertJobLog(jobLog); + } + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的数据ID + * @return 结果 + */ + @Override + public int deleteJobLogByIds(Long[] logIds) { + return jobLogMapper.deleteJobLogByIds(logIds); + } + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + */ + @Override + public int deleteJobLogById(Long jobId) { + return jobLogMapper.deleteJobLogById(jobId); + } + + /** + * 清空任务日志 + */ + @Override + public void cleanJobLog() { + jobLogMapper.cleanJobLog(); + } +} diff --git a/jsowell-quartz/src/main/java/com/jsowell/quartz/service/impl/SysJobServiceImpl.java b/jsowell-quartz/src/main/java/com/jsowell/quartz/service/impl/SysJobServiceImpl.java new file mode 100644 index 000000000..c4eadaf6b --- /dev/null +++ b/jsowell-quartz/src/main/java/com/jsowell/quartz/service/impl/SysJobServiceImpl.java @@ -0,0 +1,236 @@ +package com.jsowell.quartz.service.impl; + +import com.jsowell.common.constant.ScheduleConstants; +import com.jsowell.common.exception.job.TaskException; +import com.jsowell.quartz.domain.SysJob; +import com.jsowell.quartz.mapper.SysJobMapper; +import com.jsowell.quartz.service.ISysJobService; +import com.jsowell.quartz.util.CronUtils; +import com.jsowell.quartz.util.ScheduleUtils; +import org.quartz.JobDataMap; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.PostConstruct; +import java.util.List; + +/** + * 定时任务调度信息 服务层 + * + * @author jsowell + */ +@Service +public class SysJobServiceImpl implements ISysJobService { + @Autowired + private Scheduler scheduler; + + @Autowired + private SysJobMapper jobMapper; + + /** + * 项目启动时,初始化定时器 主要是防止手动修改数据库导致未同步到定时任务处理(注:不能手动修改数据库ID和任务组名,否则会导致脏数据) + */ + @PostConstruct + public void init() throws SchedulerException, TaskException { + scheduler.clear(); + List jobList = jobMapper.selectJobAll(); + for (SysJob job : jobList) { + ScheduleUtils.createScheduleJob(scheduler, job); + } + } + + /** + * 获取quartz调度器的计划任务列表 + * + * @param job 调度信息 + * @return + */ + @Override + public List selectJobList(SysJob job) { + return jobMapper.selectJobList(job); + } + + /** + * 通过调度任务ID查询调度信息 + * + * @param jobId 调度任务ID + * @return 调度任务对象信息 + */ + @Override + public SysJob selectJobById(Long jobId) { + return jobMapper.selectJobById(jobId); + } + + /** + * 暂停任务 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int pauseJob(SysJob job) throws SchedulerException { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + job.setStatus(ScheduleConstants.Status.PAUSE.getValue()); + int rows = jobMapper.updateJob(job); + if (rows > 0) { + scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 恢复任务 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int resumeJob(SysJob job) throws SchedulerException { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + job.setStatus(ScheduleConstants.Status.NORMAL.getValue()); + int rows = jobMapper.updateJob(job); + if (rows > 0) { + scheduler.resumeJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 删除任务后,所对应的trigger也将被删除 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int deleteJob(SysJob job) throws SchedulerException { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + int rows = jobMapper.deleteJobById(jobId); + if (rows > 0) { + scheduler.deleteJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 批量删除调度信息 + * + * @param jobIds 需要删除的任务ID + * @return 结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteJobByIds(Long[] jobIds) throws SchedulerException { + for (Long jobId : jobIds) { + SysJob job = jobMapper.selectJobById(jobId); + deleteJob(job); + } + } + + /** + * 任务调度状态修改 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int changeStatus(SysJob job) throws SchedulerException { + int rows = 0; + String status = job.getStatus(); + if (ScheduleConstants.Status.NORMAL.getValue().equals(status)) { + rows = resumeJob(job); + } else if (ScheduleConstants.Status.PAUSE.getValue().equals(status)) { + rows = pauseJob(job); + } + return rows; + } + + /** + * 立即运行任务 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean run(SysJob job) throws SchedulerException { + boolean result = false; + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + SysJob properties = selectJobById(job.getJobId()); + // 参数 + JobDataMap dataMap = new JobDataMap(); + dataMap.put(ScheduleConstants.TASK_PROPERTIES, properties); + JobKey jobKey = ScheduleUtils.getJobKey(jobId, jobGroup); + if (scheduler.checkExists(jobKey)) { + result = true; + scheduler.triggerJob(jobKey, dataMap); + } + return result; + } + + /** + * 新增任务 + * + * @param job 调度信息 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int insertJob(SysJob job) throws SchedulerException, TaskException { + job.setStatus(ScheduleConstants.Status.PAUSE.getValue()); + int rows = jobMapper.insertJob(job); + if (rows > 0) { + ScheduleUtils.createScheduleJob(scheduler, job); + } + return rows; + } + + /** + * 更新任务的时间表达式 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int updateJob(SysJob job) throws SchedulerException, TaskException { + SysJob properties = selectJobById(job.getJobId()); + int rows = jobMapper.updateJob(job); + if (rows > 0) { + updateSchedulerJob(job, properties.getJobGroup()); + } + return rows; + } + + /** + * 更新任务 + * + * @param job 任务对象 + * @param jobGroup 任务组名 + */ + public void updateSchedulerJob(SysJob job, String jobGroup) throws SchedulerException, TaskException { + Long jobId = job.getJobId(); + // 判断是否存在 + JobKey jobKey = ScheduleUtils.getJobKey(jobId, jobGroup); + if (scheduler.checkExists(jobKey)) { + // 防止创建时存在数据问题 先移除,然后在执行创建操作 + scheduler.deleteJob(jobKey); + } + ScheduleUtils.createScheduleJob(scheduler, job); + } + + /** + * 校验cron表达式是否有效 + * + * @param cronExpression 表达式 + * @return 结果 + */ + @Override + public boolean checkCronExpressionIsValid(String cronExpression) { + return CronUtils.isValid(cronExpression); + } +} diff --git a/jsowell-quartz/src/main/java/com/jsowell/quartz/task/JsowellTask.java b/jsowell-quartz/src/main/java/com/jsowell/quartz/task/JsowellTask.java new file mode 100644 index 000000000..c0b6069ea --- /dev/null +++ b/jsowell-quartz/src/main/java/com/jsowell/quartz/task/JsowellTask.java @@ -0,0 +1,39 @@ +package com.jsowell.quartz.task; + +import com.jsowell.common.util.DateUtils; +import com.jsowell.pile.service.IOrderBasicInfoService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Date; + +@Component("jsowellTask") +public class JsowellTask { + + private final Logger log = LoggerFactory.getLogger(JsowellTask.class); + + @Autowired + private IOrderBasicInfoService orderBasicInfoService; + + /** + * 关闭15分钟未支付的订单 + * close15MinutesOfUnpaidOrders + */ + public void close15MinutesOfUnpaidOrders() { + // log.info("关闭15分钟未支付的订单"); + orderBasicInfoService.close15MinutesOfUnpaidOrders(); + } + + /** + * 关闭启动失败的订单 + * 订单支付成功,在15分钟内未启动, + */ + public void closeStartFailedOrder() { + // 查询出最近2天支付成功,并且订单状态为未启动的订单 + String startTime = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, DateUtils.addDays(new Date(), -2)); + String endTime = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, new Date()); + orderBasicInfoService.closeStartFailedOrder(startTime, endTime); + } +} diff --git a/jsowell-quartz/src/main/java/com/jsowell/quartz/task/RyTask.java b/jsowell-quartz/src/main/java/com/jsowell/quartz/task/RyTask.java new file mode 100644 index 000000000..a2993dbae --- /dev/null +++ b/jsowell-quartz/src/main/java/com/jsowell/quartz/task/RyTask.java @@ -0,0 +1,24 @@ +package com.jsowell.quartz.task; + +import org.springframework.stereotype.Component; +import com.jsowell.common.util.StringUtils; + +/** + * 定时任务调度测试 + * + * @author jsowell + */ +@Component("ryTask") +public class RyTask { + public void ryMultipleParams(String s, Boolean b, Long l, Double d, Integer i) { + System.out.println(StringUtils.format("执行多参方法: 字符串类型{},布尔类型{},长整型{},浮点型{},整形{}", s, b, l, d, i)); + } + + public void ryParams(String params) { + System.out.println("执行有参方法:" + params); + } + + public void ryNoParams() { + System.out.println("执行无参方法"); + } +} diff --git a/jsowell-quartz/src/main/java/com/jsowell/quartz/util/AbstractQuartzJob.java b/jsowell-quartz/src/main/java/com/jsowell/quartz/util/AbstractQuartzJob.java new file mode 100644 index 000000000..e8ea1d839 --- /dev/null +++ b/jsowell-quartz/src/main/java/com/jsowell/quartz/util/AbstractQuartzJob.java @@ -0,0 +1,97 @@ +package com.jsowell.quartz.util; + +import com.jsowell.common.constant.Constants; +import com.jsowell.common.constant.ScheduleConstants; +import com.jsowell.common.util.ExceptionUtil; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.bean.BeanUtils; +import com.jsowell.common.util.spring.SpringUtils; +import com.jsowell.quartz.domain.SysJob; +import com.jsowell.quartz.domain.SysJobLog; +import com.jsowell.quartz.service.ISysJobLogService; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; + +/** + * 抽象quartz调用 + * + * @author jsowell + */ +public abstract class AbstractQuartzJob implements Job { + private static final Logger log = LoggerFactory.getLogger(AbstractQuartzJob.class); + + /** + * 线程本地变量 + */ + private static ThreadLocal threadLocal = new ThreadLocal<>(); + + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + SysJob sysJob = new SysJob(); + BeanUtils.copyBeanProp(sysJob, context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES)); + try { + before(context, sysJob); + if (sysJob != null) { + doExecute(context, sysJob); + } + after(context, sysJob, null); + } catch (Exception e) { + log.error("任务执行异常 - :", e); + after(context, sysJob, e); + } + } + + /** + * 执行前 + * + * @param context 工作执行上下文对象 + * @param sysJob 系统计划任务 + */ + protected void before(JobExecutionContext context, SysJob sysJob) { + threadLocal.set(new Date()); + } + + /** + * 执行后 + * + * @param context 工作执行上下文对象 + * @param sysJob 系统计划任务 + */ + protected void after(JobExecutionContext context, SysJob sysJob, Exception e) { + Date startTime = threadLocal.get(); + threadLocal.remove(); + + final SysJobLog sysJobLog = new SysJobLog(); + sysJobLog.setJobName(sysJob.getJobName()); + sysJobLog.setJobGroup(sysJob.getJobGroup()); + sysJobLog.setInvokeTarget(sysJob.getInvokeTarget()); + sysJobLog.setStartTime(startTime); + sysJobLog.setStopTime(new Date()); + long runMs = sysJobLog.getStopTime().getTime() - sysJobLog.getStartTime().getTime(); + sysJobLog.setJobMessage(sysJobLog.getJobName() + " 总共耗时:" + runMs + "毫秒"); + if (e != null) { + sysJobLog.setStatus(Constants.FAIL); + String errorMsg = StringUtils.substring(ExceptionUtil.getExceptionMessage(e), 0, 2000); + sysJobLog.setExceptionInfo(errorMsg); + } else { + sysJobLog.setStatus(Constants.SUCCESS); + } + + // 写入数据库当中 + SpringUtils.getBean(ISysJobLogService.class).addJobLog(sysJobLog); + } + + /** + * 执行方法,由子类重载 + * + * @param context 工作执行上下文对象 + * @param sysJob 系统计划任务 + * @throws Exception 执行过程中的异常 + */ + protected abstract void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception; +} diff --git a/jsowell-quartz/src/main/java/com/jsowell/quartz/util/CronUtils.java b/jsowell-quartz/src/main/java/com/jsowell/quartz/util/CronUtils.java new file mode 100644 index 000000000..7b80a9f14 --- /dev/null +++ b/jsowell-quartz/src/main/java/com/jsowell/quartz/util/CronUtils.java @@ -0,0 +1,53 @@ +package com.jsowell.quartz.util; + +import org.quartz.CronExpression; + +import java.text.ParseException; +import java.util.Date; + +/** + * cron表达式工具类 + * + * @author jsowell + */ +public class CronUtils { + /** + * 返回一个布尔值代表一个给定的Cron表达式的有效性 + * + * @param cronExpression Cron表达式 + * @return boolean 表达式是否有效 + */ + public static boolean isValid(String cronExpression) { + return CronExpression.isValidExpression(cronExpression); + } + + /** + * 返回一个字符串值,表示该消息无效Cron表达式给出有效性 + * + * @param cronExpression Cron表达式 + * @return String 无效时返回表达式错误描述,如果有效返回null + */ + public static String getInvalidMessage(String cronExpression) { + try { + new CronExpression(cronExpression); + return null; + } catch (ParseException pe) { + return pe.getMessage(); + } + } + + /** + * 返回下一个执行时间根据给定的Cron表达式 + * + * @param cronExpression Cron表达式 + * @return Date 下次Cron表达式执行时间 + */ + public static Date getNextExecution(String cronExpression) { + try { + CronExpression cron = new CronExpression(cronExpression); + return cron.getNextValidTimeAfter(new Date(System.currentTimeMillis())); + } catch (ParseException e) { + throw new IllegalArgumentException(e.getMessage()); + } + } +} diff --git a/jsowell-quartz/src/main/java/com/jsowell/quartz/util/JobInvokeUtil.java b/jsowell-quartz/src/main/java/com/jsowell/quartz/util/JobInvokeUtil.java new file mode 100644 index 000000000..4340497bc --- /dev/null +++ b/jsowell-quartz/src/main/java/com/jsowell/quartz/util/JobInvokeUtil.java @@ -0,0 +1,159 @@ +package com.jsowell.quartz.util; + +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.spring.SpringUtils; +import com.jsowell.quartz.domain.SysJob; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.LinkedList; +import java.util.List; + +/** + * 任务执行工具 + * + * @author jsowell + */ +public class JobInvokeUtil { + /** + * 执行方法 + * + * @param sysJob 系统任务 + */ + public static void invokeMethod(SysJob sysJob) throws Exception { + String invokeTarget = sysJob.getInvokeTarget(); + String beanName = getBeanName(invokeTarget); + String methodName = getMethodName(invokeTarget); + List methodParams = getMethodParams(invokeTarget); + + if (!isValidClassName(beanName)) { + Object bean = SpringUtils.getBean(beanName); + invokeMethod(bean, methodName, methodParams); + } else { + Object bean = Class.forName(beanName).newInstance(); + invokeMethod(bean, methodName, methodParams); + } + } + + /** + * 调用任务方法 + * + * @param bean 目标对象 + * @param methodName 方法名称 + * @param methodParams 方法参数 + */ + private static void invokeMethod(Object bean, String methodName, List methodParams) + throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, + InvocationTargetException { + if (StringUtils.isNotNull(methodParams) && methodParams.size() > 0) { + Method method = bean.getClass().getDeclaredMethod(methodName, getMethodParamsType(methodParams)); + method.invoke(bean, getMethodParamsValue(methodParams)); + } else { + Method method = bean.getClass().getDeclaredMethod(methodName); + method.invoke(bean); + } + } + + /** + * 校验是否为为class包名 + * + * @param invokeTarget 名称 + * @return true是 false否 + */ + public static boolean isValidClassName(String invokeTarget) { + return StringUtils.countMatches(invokeTarget, ".") > 1; + } + + /** + * 获取bean名称 + * + * @param invokeTarget 目标字符串 + * @return bean名称 + */ + public static String getBeanName(String invokeTarget) { + String beanName = StringUtils.substringBefore(invokeTarget, "("); + return StringUtils.substringBeforeLast(beanName, "."); + } + + /** + * 获取bean方法 + * + * @param invokeTarget 目标字符串 + * @return method方法 + */ + public static String getMethodName(String invokeTarget) { + String methodName = StringUtils.substringBefore(invokeTarget, "("); + return StringUtils.substringAfterLast(methodName, "."); + } + + /** + * 获取method方法参数相关列表 + * + * @param invokeTarget 目标字符串 + * @return method方法相关参数列表 + */ + public static List getMethodParams(String invokeTarget) { + String methodStr = StringUtils.substringBetween(invokeTarget, "(", ")"); + if (StringUtils.isEmpty(methodStr)) { + return null; + } + String[] methodParams = methodStr.split(",(?=([^\"']*[\"'][^\"']*[\"'])*[^\"']*$)"); + List classs = new LinkedList<>(); + for (int i = 0; i < methodParams.length; i++) { + String str = StringUtils.trimToEmpty(methodParams[i]); + // String字符串类型,以'或"开头 + if (StringUtils.startsWithAny(str, "'", "\"")) { + classs.add(new Object[]{StringUtils.substring(str, 1, str.length() - 1), String.class}); + } + // boolean布尔类型,等于true或者false + else if ("true".equalsIgnoreCase(str) || "false".equalsIgnoreCase(str)) { + classs.add(new Object[]{Boolean.valueOf(str), Boolean.class}); + } + // long长整形,以L结尾 + else if (StringUtils.endsWith(str, "L")) { + classs.add(new Object[]{Long.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Long.class}); + } + // double浮点类型,以D结尾 + else if (StringUtils.endsWith(str, "D")) { + classs.add(new Object[]{Double.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Double.class}); + } + // 其他类型归类为整形 + else { + classs.add(new Object[]{Integer.valueOf(str), Integer.class}); + } + } + return classs; + } + + /** + * 获取参数类型 + * + * @param methodParams 参数相关列表 + * @return 参数类型列表 + */ + public static Class[] getMethodParamsType(List methodParams) { + Class[] classs = new Class[methodParams.size()]; + int index = 0; + for (Object[] os : methodParams) { + classs[index] = (Class) os[1]; + index++; + } + return classs; + } + + /** + * 获取参数值 + * + * @param methodParams 参数相关列表 + * @return 参数值列表 + */ + public static Object[] getMethodParamsValue(List methodParams) { + Object[] classs = new Object[methodParams.size()]; + int index = 0; + for (Object[] os : methodParams) { + classs[index] = (Object) os[0]; + index++; + } + return classs; + } +} diff --git a/jsowell-quartz/src/main/java/com/jsowell/quartz/util/QuartzDisallowConcurrentExecution.java b/jsowell-quartz/src/main/java/com/jsowell/quartz/util/QuartzDisallowConcurrentExecution.java new file mode 100644 index 000000000..03cff007d --- /dev/null +++ b/jsowell-quartz/src/main/java/com/jsowell/quartz/util/QuartzDisallowConcurrentExecution.java @@ -0,0 +1,18 @@ +package com.jsowell.quartz.util; + +import org.quartz.DisallowConcurrentExecution; +import org.quartz.JobExecutionContext; +import com.jsowell.quartz.domain.SysJob; + +/** + * 定时任务处理(禁止并发执行) + * + * @author jsowell + */ +@DisallowConcurrentExecution +public class QuartzDisallowConcurrentExecution extends AbstractQuartzJob { + @Override + protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception { + JobInvokeUtil.invokeMethod(sysJob); + } +} diff --git a/jsowell-quartz/src/main/java/com/jsowell/quartz/util/QuartzJobExecution.java b/jsowell-quartz/src/main/java/com/jsowell/quartz/util/QuartzJobExecution.java new file mode 100644 index 000000000..c6302e16b --- /dev/null +++ b/jsowell-quartz/src/main/java/com/jsowell/quartz/util/QuartzJobExecution.java @@ -0,0 +1,16 @@ +package com.jsowell.quartz.util; + +import org.quartz.JobExecutionContext; +import com.jsowell.quartz.domain.SysJob; + +/** + * 定时任务处理(允许并发执行) + * + * @author jsowell + */ +public class QuartzJobExecution extends AbstractQuartzJob { + @Override + protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception { + JobInvokeUtil.invokeMethod(sysJob); + } +} diff --git a/jsowell-quartz/src/main/java/com/jsowell/quartz/util/ScheduleUtils.java b/jsowell-quartz/src/main/java/com/jsowell/quartz/util/ScheduleUtils.java new file mode 100644 index 000000000..3bd9fff82 --- /dev/null +++ b/jsowell-quartz/src/main/java/com/jsowell/quartz/util/ScheduleUtils.java @@ -0,0 +1,126 @@ +package com.jsowell.quartz.util; + +import org.quartz.CronScheduleBuilder; +import org.quartz.CronTrigger; +import org.quartz.Job; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.TriggerBuilder; +import org.quartz.TriggerKey; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.constant.ScheduleConstants; +import com.jsowell.common.exception.job.TaskException; +import com.jsowell.common.exception.job.TaskException.Code; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.spring.SpringUtils; +import com.jsowell.quartz.domain.SysJob; + +/** + * 定时任务工具类 + * + * @author jsowell + */ +public class ScheduleUtils { + /** + * 得到quartz任务类 + * + * @param sysJob 执行计划 + * @return 具体执行任务类 + */ + private static Class getQuartzJobClass(SysJob sysJob) { + boolean isConcurrent = "0".equals(sysJob.getConcurrent()); + return isConcurrent ? QuartzJobExecution.class : QuartzDisallowConcurrentExecution.class; + } + + /** + * 构建任务触发对象 + */ + public static TriggerKey getTriggerKey(Long jobId, String jobGroup) { + return TriggerKey.triggerKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup); + } + + /** + * 构建任务键对象 + */ + public static JobKey getJobKey(Long jobId, String jobGroup) { + return JobKey.jobKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup); + } + + /** + * 创建定时任务 + */ + public static void createScheduleJob(Scheduler scheduler, SysJob job) throws SchedulerException, TaskException { + Class jobClass = getQuartzJobClass(job); + // 构建job信息 + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(getJobKey(jobId, jobGroup)).build(); + + // 表达式调度构建器 + CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression()); + cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder); + + // 按新的cronExpression表达式构建一个新的trigger + CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(jobId, jobGroup)) + .withSchedule(cronScheduleBuilder).build(); + + // 放入参数,运行时的方法可以获取 + jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job); + + // 判断是否存在 + if (scheduler.checkExists(getJobKey(jobId, jobGroup))) { + // 防止创建时存在数据问题 先移除,然后在执行创建操作 + scheduler.deleteJob(getJobKey(jobId, jobGroup)); + } + + // 判断任务是否过期 + if (StringUtils.isNotNull(CronUtils.getNextExecution(job.getCronExpression()))) { + // 执行调度任务 + scheduler.scheduleJob(jobDetail, trigger); + } + + // 暂停任务 + if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue())) { + scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + } + + /** + * 设置定时任务策略 + */ + public static CronScheduleBuilder handleCronScheduleMisfirePolicy(SysJob job, CronScheduleBuilder cb) + throws TaskException { + switch (job.getMisfirePolicy()) { + case ScheduleConstants.MISFIRE_DEFAULT: + return cb; + case ScheduleConstants.MISFIRE_IGNORE_MISFIRES: + return cb.withMisfireHandlingInstructionIgnoreMisfires(); + case ScheduleConstants.MISFIRE_FIRE_AND_PROCEED: + return cb.withMisfireHandlingInstructionFireAndProceed(); + case ScheduleConstants.MISFIRE_DO_NOTHING: + return cb.withMisfireHandlingInstructionDoNothing(); + default: + throw new TaskException("The task misfire policy '" + job.getMisfirePolicy() + + "' cannot be used in cron schedule tasks", Code.CONFIG_ERROR); + } + } + + /** + * 检查包名是否为白名单配置 + * + * @param invokeTarget 目标字符串 + * @return 结果 + */ + public static boolean whiteList(String invokeTarget) { + String packageName = StringUtils.substringBefore(invokeTarget, "("); + int count = StringUtils.countMatches(packageName, "."); + if (count > 1) { + return StringUtils.containsAnyIgnoreCase(invokeTarget, Constants.JOB_WHITELIST_STR); + } + Object obj = SpringUtils.getBean(StringUtils.split(invokeTarget, ".")[0]); + return StringUtils.containsAnyIgnoreCase(obj.getClass().getPackage().getName(), Constants.JOB_WHITELIST_STR); + } +} diff --git a/jsowell-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml b/jsowell-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml new file mode 100644 index 000000000..1bf62e7a1 --- /dev/null +++ b/jsowell-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + select job_log_id, job_name, job_group, invoke_target, job_message, status, exception_info, create_time + from sys_job_log + + + + + + + + + + delete from sys_job_log where job_log_id = #{jobLogId} + + + + delete from sys_job_log where job_log_id in + + #{jobLogId} + + + + + truncate table sys_job_log + + + + insert into sys_job_log( + job_log_id, + job_name, + job_group, + invoke_target, + job_message, + status, + exception_info, + create_time + )values( + #{jobLogId}, + #{jobName}, + #{jobGroup}, + #{invokeTarget}, + #{jobMessage}, + #{status}, + #{exceptionInfo}, + sysdate() + ) + + + \ No newline at end of file diff --git a/jsowell-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml b/jsowell-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml new file mode 100644 index 000000000..80bde7a65 --- /dev/null +++ b/jsowell-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + select job_id, job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark + from sys_job + + + + + + + + + + delete from sys_job where job_id = #{jobId} + + + + delete from sys_job where job_id in + + #{jobId} + + + + + update sys_job + + job_name = #{jobName}, + job_group = #{jobGroup}, + invoke_target = #{invokeTarget}, + cron_expression = #{cronExpression}, + misfire_policy = #{misfirePolicy}, + concurrent = #{concurrent}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where job_id = #{jobId} + + + + insert into sys_job( + job_id, + job_name, + job_group, + invoke_target, + cron_expression, + misfire_policy, + concurrent, + status, + remark, + create_by, + create_time + )values( + #{jobId}, + #{jobName}, + #{jobGroup}, + #{invokeTarget}, + #{cronExpression}, + #{misfirePolicy}, + #{concurrent}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + \ No newline at end of file diff --git a/jsowell-system/pom.xml b/jsowell-system/pom.xml new file mode 100644 index 000000000..15d524f04 --- /dev/null +++ b/jsowell-system/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + jsowell-charger-web + com.jsowell + 1.0.0 + + + jsowell-system + + + system系统模块 + + + + + + + com.jsowell + jsowell-common + + + + org.projectlombok + lombok + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.8 + 1.8 + + /src/test/** + + utf-8 + + + + + + \ No newline at end of file diff --git a/jsowell-system/src/main/java/com/jsowell/system/domain/SysCache.java b/jsowell-system/src/main/java/com/jsowell/system/domain/SysCache.java new file mode 100644 index 000000000..298000e2f --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/domain/SysCache.java @@ -0,0 +1,77 @@ +package com.jsowell.system.domain; + +import com.jsowell.common.util.StringUtils; + +/** + * 缓存信息 + * + * @author jsowell + */ +public class SysCache { + /** + * 缓存名称 + */ + private String cacheName = ""; + + /** + * 缓存键名 + */ + private String cacheKey = ""; + + /** + * 缓存内容 + */ + private String cacheValue = ""; + + /** + * 备注 + */ + private String remark = ""; + + public SysCache() { + + } + + public SysCache(String cacheName, String remark) { + this.cacheName = cacheName; + this.remark = remark; + } + + public SysCache(String cacheName, String cacheKey, String cacheValue) { + this.cacheName = StringUtils.replace(cacheName, ":", ""); + this.cacheKey = StringUtils.replace(cacheKey, cacheName, ""); + this.cacheValue = cacheValue; + } + + public String getCacheName() { + return cacheName; + } + + public void setCacheName(String cacheName) { + this.cacheName = cacheName; + } + + public String getCacheKey() { + return cacheKey; + } + + public void setCacheKey(String cacheKey) { + this.cacheKey = cacheKey; + } + + public String getCacheValue() { + return cacheValue; + } + + public void setCacheValue(String cacheValue) { + this.cacheValue = cacheValue; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/domain/SysConfig.java b/jsowell-system/src/main/java/com/jsowell/system/domain/SysConfig.java new file mode 100644 index 000000000..adb5dadce --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/domain/SysConfig.java @@ -0,0 +1,111 @@ +package com.jsowell.system.domain; + +import com.jsowell.common.annotation.Excel; +import com.jsowell.common.annotation.Excel.ColumnType; +import com.jsowell.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +/** + * 参数配置表 sys_config + * + * @author jsowell + */ +public class SysConfig extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * 参数主键 + */ + @Excel(name = "参数主键", cellType = ColumnType.NUMERIC) + private Long configId; + + /** + * 参数名称 + */ + @Excel(name = "参数名称") + private String configName; + + /** + * 参数键名 + */ + @Excel(name = "参数键名") + private String configKey; + + /** + * 参数键值 + */ + @Excel(name = "参数键值") + private String configValue; + + /** + * 系统内置(Y是 N否) + */ + @Excel(name = "系统内置", readConverterExp = "Y=是,N=否") + private String configType; + + public Long getConfigId() { + return configId; + } + + public void setConfigId(Long configId) { + this.configId = configId; + } + + @NotBlank(message = "参数名称不能为空") + @Size(min = 0, max = 100, message = "参数名称不能超过100个字符") + public String getConfigName() { + return configName; + } + + public void setConfigName(String configName) { + this.configName = configName; + } + + @NotBlank(message = "参数键名长度不能为空") + @Size(min = 0, max = 100, message = "参数键名长度不能超过100个字符") + public String getConfigKey() { + return configKey; + } + + public void setConfigKey(String configKey) { + this.configKey = configKey; + } + + @NotBlank(message = "参数键值不能为空") + @Size(min = 0, max = 500, message = "参数键值长度不能超过500个字符") + public String getConfigValue() { + return configValue; + } + + public void setConfigValue(String configValue) { + this.configValue = configValue; + } + + public String getConfigType() { + return configType; + } + + public void setConfigType(String configType) { + this.configType = configType; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("configId", getConfigId()) + .append("configName", getConfigName()) + .append("configKey", getConfigKey()) + .append("configValue", getConfigValue()) + .append("configType", getConfigType()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/domain/SysLogininfor.java b/jsowell-system/src/main/java/com/jsowell/system/domain/SysLogininfor.java new file mode 100644 index 000000000..cb880e4ef --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/domain/SysLogininfor.java @@ -0,0 +1,144 @@ +package com.jsowell.system.domain; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.jsowell.common.annotation.Excel; +import com.jsowell.common.annotation.Excel.ColumnType; +import com.jsowell.common.core.domain.BaseEntity; + +import java.util.Date; + +/** + * 系统访问记录表 sys_logininfor + * + * @author jsowell + */ +public class SysLogininfor extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Excel(name = "序号", cellType = ColumnType.NUMERIC) + private Long infoId; + + /** + * 用户账号 + */ + @Excel(name = "用户账号") + private String userName; + + /** + * 登录状态 0成功 1失败 + */ + @Excel(name = "登录状态", readConverterExp = "0=成功,1=失败") + private String status; + + /** + * 登录IP地址 + */ + @Excel(name = "登录地址") + private String ipaddr; + + /** + * 登录地点 + */ + @Excel(name = "登录地点") + private String loginLocation; + + /** + * 浏览器类型 + */ + @Excel(name = "浏览器") + private String browser; + + /** + * 操作系统 + */ + @Excel(name = "操作系统") + private String os; + + /** + * 提示消息 + */ + @Excel(name = "提示消息") + private String msg; + + /** + * 访问时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "访问时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date loginTime; + + public Long getInfoId() { + return infoId; + } + + public void setInfoId(Long infoId) { + this.infoId = infoId; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getIpaddr() { + return ipaddr; + } + + public void setIpaddr(String ipaddr) { + this.ipaddr = ipaddr; + } + + public String getLoginLocation() { + return loginLocation; + } + + public void setLoginLocation(String loginLocation) { + this.loginLocation = loginLocation; + } + + public String getBrowser() { + return browser; + } + + public void setBrowser(String browser) { + this.browser = browser; + } + + public String getOs() { + return os; + } + + public void setOs(String os) { + this.os = os; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + public Date getLoginTime() { + return loginTime; + } + + public void setLoginTime(Date loginTime) { + this.loginTime = loginTime; + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/domain/SysNotice.java b/jsowell-system/src/main/java/com/jsowell/system/domain/SysNotice.java new file mode 100644 index 000000000..bc034672e --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/domain/SysNotice.java @@ -0,0 +1,102 @@ +package com.jsowell.system.domain; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.jsowell.common.core.domain.BaseEntity; +import com.jsowell.common.xss.Xss; + +/** + * 通知公告表 sys_notice + * + * @author jsowell + */ +public class SysNotice extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * 公告ID + */ + private Long noticeId; + + /** + * 公告标题 + */ + private String noticeTitle; + + /** + * 公告类型(1通知 2公告) + */ + private String noticeType; + + /** + * 公告内容 + */ + private String noticeContent; + + /** + * 公告状态(0正常 1关闭) + */ + private String status; + + public Long getNoticeId() { + return noticeId; + } + + public void setNoticeId(Long noticeId) { + this.noticeId = noticeId; + } + + public void setNoticeTitle(String noticeTitle) { + this.noticeTitle = noticeTitle; + } + + @Xss(message = "公告标题不能包含脚本字符") + @NotBlank(message = "公告标题不能为空") + @Size(min = 0, max = 50, message = "公告标题不能超过50个字符") + public String getNoticeTitle() { + return noticeTitle; + } + + public void setNoticeType(String noticeType) { + this.noticeType = noticeType; + } + + public String getNoticeType() { + return noticeType; + } + + public void setNoticeContent(String noticeContent) { + this.noticeContent = noticeContent; + } + + public String getNoticeContent() { + return noticeContent; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getStatus() { + return status; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("noticeId", getNoticeId()) + .append("noticeTitle", getNoticeTitle()) + .append("noticeType", getNoticeType()) + .append("noticeContent", getNoticeContent()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/domain/SysOperLog.java b/jsowell-system/src/main/java/com/jsowell/system/domain/SysOperLog.java new file mode 100644 index 000000000..ec7bd2cc1 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/domain/SysOperLog.java @@ -0,0 +1,255 @@ +package com.jsowell.system.domain; + +import java.util.Date; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.jsowell.common.annotation.Excel; +import com.jsowell.common.annotation.Excel.ColumnType; +import com.jsowell.common.core.domain.BaseEntity; + +/** + * 操作日志记录表 oper_log + * + * @author jsowell + */ +public class SysOperLog extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * 日志主键 + */ + @Excel(name = "操作序号", cellType = ColumnType.NUMERIC) + private Long operId; + + /** + * 操作模块 + */ + @Excel(name = "操作模块") + private String title; + + /** + * 业务类型(0其它 1新增 2修改 3删除) + */ + @Excel(name = "业务类型", readConverterExp = "0=其它,1=新增,2=修改,3=删除,4=授权,5=导出,6=导入,7=强退,8=生成代码,9=清空数据") + private Integer businessType; + + /** + * 业务类型数组 + */ + private Integer[] businessTypes; + + /** + * 请求方法 + */ + @Excel(name = "请求方法") + private String method; + + /** + * 请求方式 + */ + @Excel(name = "请求方式") + private String requestMethod; + + /** + * 操作类别(0其它 1后台用户 2手机端用户) + */ + @Excel(name = "操作类别", readConverterExp = "0=其它,1=后台用户,2=手机端用户") + private Integer operatorType; + + /** + * 操作人员 + */ + @Excel(name = "操作人员") + private String operName; + + /** + * 部门名称 + */ + @Excel(name = "部门名称") + private String deptName; + + /** + * 请求url + */ + @Excel(name = "请求地址") + private String operUrl; + + /** + * 操作地址 + */ + @Excel(name = "操作地址") + private String operIp; + + /** + * 操作地点 + */ + @Excel(name = "操作地点") + private String operLocation; + + /** + * 请求参数 + */ + @Excel(name = "请求参数") + private String operParam; + + /** + * 返回参数 + */ + @Excel(name = "返回参数") + private String jsonResult; + + /** + * 操作状态(0正常 1异常) + */ + @Excel(name = "状态", readConverterExp = "0=正常,1=异常") + private Integer status; + + /** + * 错误消息 + */ + @Excel(name = "错误消息") + private String errorMsg; + + /** + * 操作时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "操作时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date operTime; + + public Long getOperId() { + return operId; + } + + public void setOperId(Long operId) { + this.operId = operId; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Integer getBusinessType() { + return businessType; + } + + public void setBusinessType(Integer businessType) { + this.businessType = businessType; + } + + public Integer[] getBusinessTypes() { + return businessTypes; + } + + public void setBusinessTypes(Integer[] businessTypes) { + this.businessTypes = businessTypes; + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + public String getRequestMethod() { + return requestMethod; + } + + public void setRequestMethod(String requestMethod) { + this.requestMethod = requestMethod; + } + + public Integer getOperatorType() { + return operatorType; + } + + public void setOperatorType(Integer operatorType) { + this.operatorType = operatorType; + } + + public String getOperName() { + return operName; + } + + public void setOperName(String operName) { + this.operName = operName; + } + + public String getDeptName() { + return deptName; + } + + public void setDeptName(String deptName) { + this.deptName = deptName; + } + + public String getOperUrl() { + return operUrl; + } + + public void setOperUrl(String operUrl) { + this.operUrl = operUrl; + } + + public String getOperIp() { + return operIp; + } + + public void setOperIp(String operIp) { + this.operIp = operIp; + } + + public String getOperLocation() { + return operLocation; + } + + public void setOperLocation(String operLocation) { + this.operLocation = operLocation; + } + + public String getOperParam() { + return operParam; + } + + public void setOperParam(String operParam) { + this.operParam = operParam; + } + + public String getJsonResult() { + return jsonResult; + } + + public void setJsonResult(String jsonResult) { + this.jsonResult = jsonResult; + } + + public Integer getStatus() { + return status; + } + + public void setStatus(Integer status) { + this.status = status; + } + + public String getErrorMsg() { + return errorMsg; + } + + public void setErrorMsg(String errorMsg) { + this.errorMsg = errorMsg; + } + + public Date getOperTime() { + return operTime; + } + + public void setOperTime(Date operTime) { + this.operTime = operTime; + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/domain/SysPost.java b/jsowell-system/src/main/java/com/jsowell/system/domain/SysPost.java new file mode 100644 index 000000000..3ef9fc6d4 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/domain/SysPost.java @@ -0,0 +1,123 @@ +package com.jsowell.system.domain; + +import com.jsowell.common.annotation.Excel; +import com.jsowell.common.annotation.Excel.ColumnType; +import com.jsowell.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +/** + * 岗位表 sys_post + * + * @author jsowell + */ +public class SysPost extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * 岗位序号 + */ + @Excel(name = "岗位序号", cellType = ColumnType.NUMERIC) + private Long postId; + + /** + * 岗位编码 + */ + @Excel(name = "岗位编码") + private String postCode; + + /** + * 岗位名称 + */ + @Excel(name = "岗位名称") + private String postName; + + /** + * 岗位排序 + */ + @Excel(name = "岗位排序") + private String postSort; + + /** + * 状态(0正常 1停用) + */ + @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** + * 用户是否存在此岗位标识 默认不存在 + */ + private boolean flag = false; + + public Long getPostId() { + return postId; + } + + public void setPostId(Long postId) { + this.postId = postId; + } + + @NotBlank(message = "岗位编码不能为空") + @Size(min = 0, max = 64, message = "岗位编码长度不能超过64个字符") + public String getPostCode() { + return postCode; + } + + public void setPostCode(String postCode) { + this.postCode = postCode; + } + + @NotBlank(message = "岗位名称不能为空") + @Size(min = 0, max = 50, message = "岗位名称长度不能超过50个字符") + public String getPostName() { + return postName; + } + + public void setPostName(String postName) { + this.postName = postName; + } + + @NotBlank(message = "显示顺序不能为空") + public String getPostSort() { + return postSort; + } + + public void setPostSort(String postSort) { + this.postSort = postSort; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public boolean isFlag() { + return flag; + } + + public void setFlag(boolean flag) { + this.flag = flag; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("postId", getPostId()) + .append("postCode", getPostCode()) + .append("postName", getPostName()) + .append("postSort", getPostSort()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/domain/SysRoleDept.java b/jsowell-system/src/main/java/com/jsowell/system/domain/SysRoleDept.java new file mode 100644 index 000000000..c47603301 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/domain/SysRoleDept.java @@ -0,0 +1,45 @@ +package com.jsowell.system.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 角色和部门关联 sys_role_dept + * + * @author jsowell + */ +public class SysRoleDept { + /** + * 角色ID + */ + private Long roleId; + + /** + * 部门ID + */ + private Long deptId; + + public Long getRoleId() { + return roleId; + } + + public void setRoleId(Long roleId) { + this.roleId = roleId; + } + + public Long getDeptId() { + return deptId; + } + + public void setDeptId(Long deptId) { + this.deptId = deptId; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("roleId", getRoleId()) + .append("deptId", getDeptId()) + .toString(); + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/domain/SysRoleMenu.java b/jsowell-system/src/main/java/com/jsowell/system/domain/SysRoleMenu.java new file mode 100644 index 000000000..3fcd1678d --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/domain/SysRoleMenu.java @@ -0,0 +1,45 @@ +package com.jsowell.system.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 角色和菜单关联 sys_role_menu + * + * @author jsowell + */ +public class SysRoleMenu { + /** + * 角色ID + */ + private Long roleId; + + /** + * 菜单ID + */ + private Long menuId; + + public Long getRoleId() { + return roleId; + } + + public void setRoleId(Long roleId) { + this.roleId = roleId; + } + + public Long getMenuId() { + return menuId; + } + + public void setMenuId(Long menuId) { + this.menuId = menuId; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("roleId", getRoleId()) + .append("menuId", getMenuId()) + .toString(); + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/domain/SysUserOnline.java b/jsowell-system/src/main/java/com/jsowell/system/domain/SysUserOnline.java new file mode 100644 index 000000000..1d8c9512f --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/domain/SysUserOnline.java @@ -0,0 +1,112 @@ +package com.jsowell.system.domain; + +/** + * 当前在线会话 + * + * @author jsowell + */ +public class SysUserOnline { + /** + * 会话编号 + */ + private String tokenId; + + /** + * 部门名称 + */ + private String deptName; + + /** + * 用户名称 + */ + private String userName; + + /** + * 登录IP地址 + */ + private String ipaddr; + + /** + * 登录地址 + */ + private String loginLocation; + + /** + * 浏览器类型 + */ + private String browser; + + /** + * 操作系统 + */ + private String os; + + /** + * 登录时间 + */ + private Long loginTime; + + public String getTokenId() { + return tokenId; + } + + public void setTokenId(String tokenId) { + this.tokenId = tokenId; + } + + public String getDeptName() { + return deptName; + } + + public void setDeptName(String deptName) { + this.deptName = deptName; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getIpaddr() { + return ipaddr; + } + + public void setIpaddr(String ipaddr) { + this.ipaddr = ipaddr; + } + + public String getLoginLocation() { + return loginLocation; + } + + public void setLoginLocation(String loginLocation) { + this.loginLocation = loginLocation; + } + + public String getBrowser() { + return browser; + } + + public void setBrowser(String browser) { + this.browser = browser; + } + + public String getOs() { + return os; + } + + public void setOs(String os) { + this.os = os; + } + + public Long getLoginTime() { + return loginTime; + } + + public void setLoginTime(Long loginTime) { + this.loginTime = loginTime; + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/domain/SysUserPost.java b/jsowell-system/src/main/java/com/jsowell/system/domain/SysUserPost.java new file mode 100644 index 000000000..2e0322eb4 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/domain/SysUserPost.java @@ -0,0 +1,45 @@ +package com.jsowell.system.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 用户和岗位关联 sys_user_post + * + * @author jsowell + */ +public class SysUserPost { + /** + * 用户ID + */ + private Long userId; + + /** + * 岗位ID + */ + private Long postId; + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public Long getPostId() { + return postId; + } + + public void setPostId(Long postId) { + this.postId = postId; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("userId", getUserId()) + .append("postId", getPostId()) + .toString(); + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/domain/SysUserRole.java b/jsowell-system/src/main/java/com/jsowell/system/domain/SysUserRole.java new file mode 100644 index 000000000..a8de814d8 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/domain/SysUserRole.java @@ -0,0 +1,45 @@ +package com.jsowell.system.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 用户和角色关联 sys_user_role + * + * @author jsowell + */ +public class SysUserRole { + /** + * 用户ID + */ + private Long userId; + + /** + * 角色ID + */ + private Long roleId; + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public Long getRoleId() { + return roleId; + } + + public void setRoleId(Long roleId) { + this.roleId = roleId; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.JSON_STYLE) + .append("userId", getUserId()) + .append("roleId", getRoleId()) + .toString(); + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/mapper/SysConfigMapper.java b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysConfigMapper.java new file mode 100644 index 000000000..aedfa29b6 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysConfigMapper.java @@ -0,0 +1,68 @@ +package com.jsowell.system.mapper; + +import com.jsowell.system.domain.SysConfig; + +import java.util.List; + +/** + * 参数配置 数据层 + * + * @author jsowell + */ +public interface SysConfigMapper { + /** + * 查询参数配置信息 + * + * @param config 参数配置信息 + * @return 参数配置信息 + */ + public SysConfig selectConfig(SysConfig config); + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + public List selectConfigList(SysConfig config); + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数键名 + * @return 参数配置信息 + */ + public SysConfig checkConfigKeyUnique(String configKey); + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int insertConfig(SysConfig config); + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int updateConfig(SysConfig config); + + /** + * 删除参数配置 + * + * @param configId 参数ID + * @return 结果 + */ + public int deleteConfigById(Long configId); + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + * @return 结果 + */ + public int deleteConfigByIds(Long[] configIds); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/mapper/SysDeptMapper.java b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysDeptMapper.java new file mode 100644 index 000000000..c5f2604e1 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysDeptMapper.java @@ -0,0 +1,118 @@ +package com.jsowell.system.mapper; + +import com.jsowell.common.core.domain.entity.SysDept; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 部门管理 数据层 + * + * @author jsowell + */ +public interface SysDeptMapper { + /** + * 查询部门管理数据 + * + * @param dept 部门信息 + * @return 部门信息集合 + */ + public List selectDeptList(SysDept dept); + + /** + * 根据角色ID查询部门树信息 + * + * @param roleId 角色ID + * @param deptCheckStrictly 部门树选择项是否关联显示 + * @return 选中部门列表 + */ + public List selectDeptListByRoleId(@Param("roleId") Long roleId, @Param("deptCheckStrictly") boolean deptCheckStrictly); + + /** + * 根据部门ID查询信息 + * + * @param deptId 部门ID + * @return 部门信息 + */ + public SysDept selectDeptById(Long deptId); + + /** + * 根据ID查询所有子部门 + * + * @param deptId 部门ID + * @return 部门列表 + */ + public List selectChildrenDeptById(Long deptId); + + /** + * 根据ID查询所有子部门(正常状态) + * + * @param deptId 部门ID + * @return 子部门数 + */ + public int selectNormalChildrenDeptById(Long deptId); + + /** + * 是否存在子节点 + * + * @param deptId 部门ID + * @return 结果 + */ + public int hasChildByDeptId(Long deptId); + + /** + * 查询部门是否存在用户 + * + * @param deptId 部门ID + * @return 结果 + */ + public int checkDeptExistUser(Long deptId); + + /** + * 校验部门名称是否唯一 + * + * @param deptName 部门名称 + * @param parentId 父部门ID + * @return 结果 + */ + public SysDept checkDeptNameUnique(@Param("deptName") String deptName, @Param("parentId") Long parentId); + + /** + * 新增部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int insertDept(SysDept dept); + + /** + * 修改部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int updateDept(SysDept dept); + + /** + * 修改所在部门正常状态 + * + * @param deptIds 部门ID组 + */ + public void updateDeptStatusNormal(Long[] deptIds); + + /** + * 修改子元素关系 + * + * @param depts 子元素 + * @return 结果 + */ + public int updateDeptChildren(@Param("depts") List depts); + + /** + * 删除部门管理信息 + * + * @param deptId 部门ID + * @return 结果 + */ + public int deleteDeptById(Long deptId); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/mapper/SysDictDataMapper.java b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysDictDataMapper.java new file mode 100644 index 000000000..8477cae79 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysDictDataMapper.java @@ -0,0 +1,95 @@ +package com.jsowell.system.mapper; + +import com.jsowell.common.core.domain.entity.SysDictData; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 字典表 数据层 + * + * @author jsowell + */ +public interface SysDictDataMapper { + /** + * 根据条件分页查询字典数据 + * + * @param dictData 字典数据信息 + * @return 字典数据集合信息 + */ + public List selectDictDataList(SysDictData dictData); + + /** + * 根据字典类型查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据集合信息 + */ + public List selectDictDataByType(String dictType); + + /** + * 根据字典类型和字典键值查询字典数据信息 + * + * @param dictType 字典类型 + * @param dictValue 字典键值 + * @return 字典标签 + */ + public String selectDictLabel(@Param("dictType") String dictType, @Param("dictValue") String dictValue); + + /** + * 根据字典数据ID查询信息 + * + * @param dictCode 字典数据ID + * @return 字典数据 + */ + public SysDictData selectDictDataById(Long dictCode); + + /** + * 查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据 + */ + public int countDictDataByType(String dictType); + + /** + * 通过字典ID删除字典数据信息 + * + * @param dictCode 字典数据ID + * @return 结果 + */ + public int deleteDictDataById(Long dictCode); + + /** + * 批量删除字典数据信息 + * + * @param dictCodes 需要删除的字典数据ID + * @return 结果 + */ + public int deleteDictDataByIds(Long[] dictCodes); + + /** + * 新增字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int insertDictData(SysDictData dictData); + + /** + * 修改字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int updateDictData(SysDictData dictData); + + /** + * 同步修改字典类型 + * + * @param oldDictType 旧字典类型 + * @param newDictType 新旧字典类型 + * @return 结果 + */ + public int updateDictDataType(@Param("oldDictType") String oldDictType, @Param("newDictType") String newDictType); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/mapper/SysDictTypeMapper.java b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysDictTypeMapper.java new file mode 100644 index 000000000..7ac992590 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysDictTypeMapper.java @@ -0,0 +1,83 @@ +package com.jsowell.system.mapper; + +import com.jsowell.common.core.domain.entity.SysDictType; + +import java.util.List; + +/** + * 字典表 数据层 + * + * @author jsowell + */ +public interface SysDictTypeMapper { + /** + * 根据条件分页查询字典类型 + * + * @param dictType 字典类型信息 + * @return 字典类型集合信息 + */ + public List selectDictTypeList(SysDictType dictType); + + /** + * 根据所有字典类型 + * + * @return 字典类型集合信息 + */ + public List selectDictTypeAll(); + + /** + * 根据字典类型ID查询信息 + * + * @param dictId 字典类型ID + * @return 字典类型 + */ + public SysDictType selectDictTypeById(Long dictId); + + /** + * 根据字典类型查询信息 + * + * @param dictType 字典类型 + * @return 字典类型 + */ + public SysDictType selectDictTypeByType(String dictType); + + /** + * 通过字典ID删除字典信息 + * + * @param dictId 字典ID + * @return 结果 + */ + public int deleteDictTypeById(Long dictId); + + /** + * 批量删除字典类型信息 + * + * @param dictIds 需要删除的字典ID + * @return 结果 + */ + public int deleteDictTypeByIds(Long[] dictIds); + + /** + * 新增字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int insertDictType(SysDictType dictType); + + /** + * 修改字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int updateDictType(SysDictType dictType); + + /** + * 校验字典类型称是否唯一 + * + * @param dictType 字典类型 + * @return 结果 + */ + public SysDictType checkDictTypeUnique(String dictType); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/mapper/SysLogininforMapper.java b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysLogininforMapper.java new file mode 100644 index 000000000..2be3106ac --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysLogininforMapper.java @@ -0,0 +1,42 @@ +package com.jsowell.system.mapper; + +import com.jsowell.system.domain.SysLogininfor; + +import java.util.List; + +/** + * 系统访问日志情况信息 数据层 + * + * @author jsowell + */ +public interface SysLogininforMapper { + /** + * 新增系统登录日志 + * + * @param logininfor 访问日志对象 + */ + public void insertLogininfor(SysLogininfor logininfor); + + /** + * 查询系统登录日志集合 + * + * @param logininfor 访问日志对象 + * @return 登录记录集合 + */ + public List selectLogininforList(SysLogininfor logininfor); + + /** + * 批量删除系统登录日志 + * + * @param infoIds 需要删除的登录日志ID + * @return 结果 + */ + public int deleteLogininforByIds(Long[] infoIds); + + /** + * 清空系统登录日志 + * + * @return 结果 + */ + public int cleanLogininfor(); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/mapper/SysMenuMapper.java b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysMenuMapper.java new file mode 100644 index 000000000..b102d1eca --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysMenuMapper.java @@ -0,0 +1,117 @@ +package com.jsowell.system.mapper; + +import com.jsowell.common.core.domain.entity.SysMenu; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 菜单表 数据层 + * + * @author jsowell + */ +public interface SysMenuMapper { + /** + * 查询系统菜单列表 + * + * @param menu 菜单信息 + * @return 菜单列表 + */ + public List selectMenuList(SysMenu menu); + + /** + * 根据用户所有权限 + * + * @return 权限列表 + */ + public List selectMenuPerms(); + + /** + * 根据用户查询系统菜单列表 + * + * @param menu 菜单信息 + * @return 菜单列表 + */ + public List selectMenuListByUserId(SysMenu menu); + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + public List selectMenuPermsByUserId(Long userId); + + /** + * 根据用户ID查询菜单 + * + * @return 菜单列表 + */ + public List selectMenuTreeAll(); + + /** + * 根据用户ID查询菜单 + * + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuTreeByUserId(Long userId); + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @param menuCheckStrictly 菜单树选择项是否关联显示 + * @return 选中菜单列表 + */ + public List selectMenuListByRoleId(@Param("roleId") Long roleId, @Param("menuCheckStrictly") boolean menuCheckStrictly); + + /** + * 根据菜单ID查询信息 + * + * @param menuId 菜单ID + * @return 菜单信息 + */ + public SysMenu selectMenuById(Long menuId); + + /** + * 是否存在菜单子节点 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int hasChildByMenuId(Long menuId); + + /** + * 新增菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int insertMenu(SysMenu menu); + + /** + * 修改菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int updateMenu(SysMenu menu); + + /** + * 删除菜单管理信息 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int deleteMenuById(Long menuId); + + /** + * 校验菜单名称是否唯一 + * + * @param menuName 菜单名称 + * @param parentId 父菜单ID + * @return 结果 + */ + public SysMenu checkMenuNameUnique(@Param("menuName") String menuName, @Param("parentId") Long parentId); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/mapper/SysNoticeMapper.java b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysNoticeMapper.java new file mode 100644 index 000000000..73c808545 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysNoticeMapper.java @@ -0,0 +1,60 @@ +package com.jsowell.system.mapper; + +import com.jsowell.system.domain.SysNotice; + +import java.util.List; + +/** + * 通知公告表 数据层 + * + * @author jsowell + */ +public interface SysNoticeMapper { + /** + * 查询公告信息 + * + * @param noticeId 公告ID + * @return 公告信息 + */ + public SysNotice selectNoticeById(Long noticeId); + + /** + * 查询公告列表 + * + * @param notice 公告信息 + * @return 公告集合 + */ + public List selectNoticeList(SysNotice notice); + + /** + * 新增公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int insertNotice(SysNotice notice); + + /** + * 修改公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int updateNotice(SysNotice notice); + + /** + * 批量删除公告 + * + * @param noticeId 公告ID + * @return 结果 + */ + public int deleteNoticeById(Long noticeId); + + /** + * 批量删除公告信息 + * + * @param noticeIds 需要删除的公告ID + * @return 结果 + */ + public int deleteNoticeByIds(Long[] noticeIds); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/mapper/SysOperLogMapper.java b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysOperLogMapper.java new file mode 100644 index 000000000..edef0b756 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysOperLogMapper.java @@ -0,0 +1,48 @@ +package com.jsowell.system.mapper; + +import com.jsowell.system.domain.SysOperLog; + +import java.util.List; + +/** + * 操作日志 数据层 + * + * @author jsowell + */ +public interface SysOperLogMapper { + /** + * 新增操作日志 + * + * @param operLog 操作日志对象 + */ + public void insertOperlog(SysOperLog operLog); + + /** + * 查询系统操作日志集合 + * + * @param operLog 操作日志对象 + * @return 操作日志集合 + */ + public List selectOperLogList(SysOperLog operLog); + + /** + * 批量删除系统操作日志 + * + * @param operIds 需要删除的操作日志ID + * @return 结果 + */ + public int deleteOperLogByIds(Long[] operIds); + + /** + * 查询操作日志详细 + * + * @param operId 操作ID + * @return 操作日志对象 + */ + public SysOperLog selectOperLogById(Long operId); + + /** + * 清空操作日志 + */ + public void cleanOperLog(); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/mapper/SysPostMapper.java b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysPostMapper.java new file mode 100644 index 000000000..e7c722feb --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysPostMapper.java @@ -0,0 +1,99 @@ +package com.jsowell.system.mapper; + +import com.jsowell.system.domain.SysPost; + +import java.util.List; + +/** + * 岗位信息 数据层 + * + * @author jsowell + */ +public interface SysPostMapper { + /** + * 查询岗位数据集合 + * + * @param post 岗位信息 + * @return 岗位数据集合 + */ + public List selectPostList(SysPost post); + + /** + * 查询所有岗位 + * + * @return 岗位列表 + */ + public List selectPostAll(); + + /** + * 通过岗位ID查询岗位信息 + * + * @param postId 岗位ID + * @return 角色对象信息 + */ + public SysPost selectPostById(Long postId); + + /** + * 根据用户ID获取岗位选择框列表 + * + * @param userId 用户ID + * @return 选中岗位ID列表 + */ + public List selectPostListByUserId(Long userId); + + /** + * 查询用户所属岗位组 + * + * @param userName 用户名 + * @return 结果 + */ + public List selectPostsByUserName(String userName); + + /** + * 删除岗位信息 + * + * @param postId 岗位ID + * @return 结果 + */ + public int deletePostById(Long postId); + + /** + * 批量删除岗位信息 + * + * @param postIds 需要删除的岗位ID + * @return 结果 + */ + public int deletePostByIds(Long[] postIds); + + /** + * 修改岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int updatePost(SysPost post); + + /** + * 新增岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int insertPost(SysPost post); + + /** + * 校验岗位名称 + * + * @param postName 岗位名称 + * @return 结果 + */ + public SysPost checkPostNameUnique(String postName); + + /** + * 校验岗位编码 + * + * @param postCode 岗位编码 + * @return 结果 + */ + public SysPost checkPostCodeUnique(String postCode); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/mapper/SysRoleDeptMapper.java b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysRoleDeptMapper.java new file mode 100644 index 000000000..bd80611f5 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysRoleDeptMapper.java @@ -0,0 +1,44 @@ +package com.jsowell.system.mapper; + +import com.jsowell.system.domain.SysRoleDept; + +import java.util.List; + +/** + * 角色与部门关联表 数据层 + * + * @author jsowell + */ +public interface SysRoleDeptMapper { + /** + * 通过角色ID删除角色和部门关联 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleDeptByRoleId(Long roleId); + + /** + * 批量删除角色部门关联信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteRoleDept(Long[] ids); + + /** + * 查询部门使用数量 + * + * @param deptId 部门ID + * @return 结果 + */ + public int selectCountRoleDeptByDeptId(Long deptId); + + /** + * 批量新增角色部门信息 + * + * @param roleDeptList 角色部门列表 + * @return 结果 + */ + public int batchRoleDept(List roleDeptList); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/mapper/SysRoleMapper.java b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysRoleMapper.java new file mode 100644 index 000000000..525234c7a --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysRoleMapper.java @@ -0,0 +1,107 @@ +package com.jsowell.system.mapper; + +import com.jsowell.common.core.domain.entity.SysRole; + +import java.util.List; + +/** + * 角色表 数据层 + * + * @author jsowell + */ +public interface SysRoleMapper { + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + public List selectRoleList(SysRole role); + + /** + * 根据用户ID查询角色 + * + * @param userId 用户ID + * @return 角色列表 + */ + public List selectRolePermissionByUserId(Long userId); + + /** + * 查询所有角色 + * + * @return 角色列表 + */ + public List selectRoleAll(); + + /** + * 根据用户ID获取角色选择框列表 + * + * @param userId 用户ID + * @return 选中角色ID列表 + */ + public List selectRoleListByUserId(Long userId); + + /** + * 通过角色ID查询角色 + * + * @param roleId 角色ID + * @return 角色对象信息 + */ + public SysRole selectRoleById(Long roleId); + + /** + * 根据用户ID查询角色 + * + * @param userName 用户名 + * @return 角色列表 + */ + public List selectRolesByUserName(String userName); + + /** + * 校验角色名称是否唯一 + * + * @param roleName 角色名称 + * @return 角色信息 + */ + public SysRole checkRoleNameUnique(String roleName); + + /** + * 校验角色权限是否唯一 + * + * @param roleKey 角色权限 + * @return 角色信息 + */ + public SysRole checkRoleKeyUnique(String roleKey); + + /** + * 修改角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int updateRole(SysRole role); + + /** + * 新增角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int insertRole(SysRole role); + + /** + * 通过角色ID删除角色 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleById(Long roleId); + + /** + * 批量删除角色信息 + * + * @param roleIds 需要删除的角色ID + * @return 结果 + */ + public int deleteRoleByIds(Long[] roleIds); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/mapper/SysRoleMenuMapper.java b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysRoleMenuMapper.java new file mode 100644 index 000000000..69ac6ec99 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysRoleMenuMapper.java @@ -0,0 +1,44 @@ +package com.jsowell.system.mapper; + +import com.jsowell.system.domain.SysRoleMenu; + +import java.util.List; + +/** + * 角色与菜单关联表 数据层 + * + * @author jsowell + */ +public interface SysRoleMenuMapper { + /** + * 查询菜单使用数量 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int checkMenuExistRole(Long menuId); + + /** + * 通过角色ID删除角色和菜单关联 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleMenuByRoleId(Long roleId); + + /** + * 批量删除角色菜单关联信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteRoleMenu(Long[] ids); + + /** + * 批量新增角色菜单信息 + * + * @param roleMenuList 角色菜单列表 + * @return 结果 + */ + public int batchRoleMenu(List roleMenuList); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/mapper/SysUserMapper.java b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysUserMapper.java new file mode 100644 index 000000000..3c24176f4 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysUserMapper.java @@ -0,0 +1,127 @@ +package com.jsowell.system.mapper; + +import com.jsowell.common.core.domain.entity.SysUser; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 用户表 数据层 + * + * @author jsowell + */ +public interface SysUserMapper { + /** + * 根据条件分页查询用户列表 + * + * @param sysUser 用户信息 + * @return 用户信息集合信息 + */ + List selectUserList(SysUser sysUser); + + /** + * 根据条件分页查询已配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + List selectAllocatedList(SysUser user); + + /** + * 根据条件分页查询未分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + List selectUnallocatedList(SysUser user); + + /** + * 通过用户名查询用户 + * + * @param userName 用户名 + * @return 用户对象信息 + */ + SysUser selectUserByUserName(String userName); + + /** + * 通过用户ID查询用户 + * + * @param userId 用户ID + * @return 用户对象信息 + */ + SysUser selectUserById(Long userId); + + /** + * 新增用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + int insertUser(SysUser user); + + /** + * 修改用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + int updateUser(SysUser user); + + /** + * 修改用户头像 + * + * @param userName 用户名 + * @param avatar 头像地址 + * @return 结果 + */ + int updateUserAvatar(@Param("userName") String userName, @Param("avatar") String avatar); + + /** + * 重置用户密码 + * + * @param userName 用户名 + * @param password 密码 + * @return 结果 + */ + int resetUserPwd(@Param("userName") String userName, @Param("password") String password); + + /** + * 通过用户ID删除用户 + * + * @param userId 用户ID + * @return 结果 + */ + int deleteUserById(Long userId); + + /** + * 批量删除用户信息 + * + * @param userIds 需要删除的用户ID + * @return 结果 + */ + int deleteUserByIds(Long[] userIds); + + /** + * 校验用户名称是否唯一 + * + * @param userName 用户名称 + * @return 结果 + */ + int checkUserNameUnique(String userName); + + /** + * 校验手机号码是否唯一 + * + * @param phone 手机号码 + * @return 结果 + */ + SysUser checkPhoneUnique(String phone); + + /** + * 校验email是否唯一 + * + * @param email 用户邮箱 + * @return 结果 + */ + SysUser checkEmailUnique(String email); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/mapper/SysUserPostMapper.java b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysUserPostMapper.java new file mode 100644 index 000000000..a7d87c47c --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysUserPostMapper.java @@ -0,0 +1,44 @@ +package com.jsowell.system.mapper; + +import com.jsowell.system.domain.SysUserPost; + +import java.util.List; + +/** + * 用户与岗位关联表 数据层 + * + * @author jsowell + */ +public interface SysUserPostMapper { + /** + * 通过用户ID删除用户和岗位关联 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserPostByUserId(Long userId); + + /** + * 通过岗位ID查询岗位使用数量 + * + * @param postId 岗位ID + * @return 结果 + */ + public int countUserPostById(Long postId); + + /** + * 批量删除用户和岗位关联 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteUserPost(Long[] ids); + + /** + * 批量新增用户岗位信息 + * + * @param userPostList 用户角色列表 + * @return 结果 + */ + public int batchUserPost(List userPostList); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/mapper/SysUserRoleMapper.java b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysUserRoleMapper.java new file mode 100644 index 000000000..cd2d7214e --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysUserRoleMapper.java @@ -0,0 +1,62 @@ +package com.jsowell.system.mapper; + +import com.jsowell.system.domain.SysUserRole; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 用户与角色关联表 数据层 + * + * @author jsowell + */ +public interface SysUserRoleMapper { + /** + * 通过用户ID删除用户和角色关联 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserRoleByUserId(Long userId); + + /** + * 批量删除用户和角色关联 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteUserRole(Long[] ids); + + /** + * 通过角色ID查询角色使用数量 + * + * @param roleId 角色ID + * @return 结果 + */ + public int countUserRoleByRoleId(Long roleId); + + /** + * 批量新增用户角色信息 + * + * @param userRoleList 用户角色列表 + * @return 结果 + */ + public int batchUserRole(List userRoleList); + + /** + * 删除用户和角色关联信息 + * + * @param userRole 用户和角色关联信息 + * @return 结果 + */ + public int deleteUserRoleInfo(SysUserRole userRole); + + /** + * 批量取消授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要删除的用户数据ID + * @return 结果 + */ + public int deleteUserRoleInfos(@Param("roleId") Long roleId, @Param("userIds") Long[] userIds); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/SysConfigService.java b/jsowell-system/src/main/java/com/jsowell/system/service/SysConfigService.java new file mode 100644 index 000000000..0227798a5 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/SysConfigService.java @@ -0,0 +1,89 @@ +package com.jsowell.system.service; + +import com.jsowell.system.domain.SysConfig; + +import java.util.List; + +/** + * 参数配置 服务层 + * + * @author jsowell + */ +public interface SysConfigService { + /** + * 查询参数配置信息 + * + * @param configId 参数配置ID + * @return 参数配置信息 + */ + public SysConfig selectConfigById(Long configId); + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数键名 + * @return 参数键值 + */ + public String selectConfigByKey(String configKey); + + /** + * 获取验证码开关 + * + * @return true开启,false关闭 + */ + public boolean selectCaptchaEnabled(); + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + public List selectConfigList(SysConfig config); + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int insertConfig(SysConfig config); + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int updateConfig(SysConfig config); + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + */ + public void deleteConfigByIds(Long[] configIds); + + /** + * 加载参数缓存数据 + */ + public void loadingConfigCache(); + + /** + * 清空参数缓存数据 + */ + public void clearConfigCache(); + + /** + * 重置参数缓存数据 + */ + public void resetConfigCache(); + + /** + * 校验参数键名是否唯一 + * + * @param config 参数信息 + * @return 结果 + */ + public String checkConfigKeyUnique(SysConfig config); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/SysDeptService.java b/jsowell-system/src/main/java/com/jsowell/system/service/SysDeptService.java new file mode 100644 index 000000000..fab3db6c8 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/SysDeptService.java @@ -0,0 +1,116 @@ +package com.jsowell.system.service; + +import com.jsowell.common.core.domain.TreeSelect; +import com.jsowell.common.core.domain.entity.SysDept; + +import java.util.List; + +/** + * 部门管理 服务层 + * + * @author jsowell + */ +public interface SysDeptService { + /** + * 查询部门管理数据 + * + * @param dept 部门信息 + * @return 部门信息集合 + */ + public List selectDeptList(SysDept dept); + + /** + * 构建前端所需要树结构 + * + * @param depts 部门列表 + * @return 树结构列表 + */ + public List buildDeptTree(List depts); + + /** + * 构建前端所需要下拉树结构 + * + * @param depts 部门列表 + * @return 下拉树结构列表 + */ + public List buildDeptTreeSelect(List depts); + + /** + * 根据角色ID查询部门树信息 + * + * @param roleId 角色ID + * @return 选中部门列表 + */ + public List selectDeptListByRoleId(Long roleId); + + /** + * 根据部门ID查询信息 + * + * @param deptId 部门ID + * @return 部门信息 + */ + public SysDept selectDeptById(Long deptId); + + /** + * 根据ID查询所有子部门(正常状态) + * + * @param deptId 部门ID + * @return 子部门数 + */ + public int selectNormalChildrenDeptById(Long deptId); + + /** + * 是否存在部门子节点 + * + * @param deptId 部门ID + * @return 结果 + */ + public boolean hasChildByDeptId(Long deptId); + + /** + * 查询部门是否存在用户 + * + * @param deptId 部门ID + * @return 结果 true 存在 false 不存在 + */ + public boolean checkDeptExistUser(Long deptId); + + /** + * 校验部门名称是否唯一 + * + * @param dept 部门信息 + * @return 结果 + */ + public String checkDeptNameUnique(SysDept dept); + + /** + * 校验部门是否有数据权限 + * + * @param deptId 部门id + */ + public void checkDeptDataScope(Long deptId); + + /** + * 新增保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int insertDept(SysDept dept); + + /** + * 修改保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int updateDept(SysDept dept); + + /** + * 删除部门管理信息 + * + * @param deptId 部门ID + * @return 结果 + */ + public int deleteDeptById(Long deptId); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/SysDictDataService.java b/jsowell-system/src/main/java/com/jsowell/system/service/SysDictDataService.java new file mode 100644 index 000000000..6329d5132 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/SysDictDataService.java @@ -0,0 +1,60 @@ +package com.jsowell.system.service; + +import com.jsowell.common.core.domain.entity.SysDictData; + +import java.util.List; + +/** + * 字典 业务层 + * + * @author jsowell + */ +public interface SysDictDataService { + /** + * 根据条件分页查询字典数据 + * + * @param dictData 字典数据信息 + * @return 字典数据集合信息 + */ + public List selectDictDataList(SysDictData dictData); + + /** + * 根据字典类型和字典键值查询字典数据信息 + * + * @param dictType 字典类型 + * @param dictValue 字典键值 + * @return 字典标签 + */ + public String selectDictLabel(String dictType, String dictValue); + + /** + * 根据字典数据ID查询信息 + * + * @param dictCode 字典数据ID + * @return 字典数据 + */ + public SysDictData selectDictDataById(Long dictCode); + + /** + * 批量删除字典数据信息 + * + * @param dictCodes 需要删除的字典数据ID + */ + public void deleteDictDataByIds(Long[] dictCodes); + + /** + * 新增保存字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int insertDictData(SysDictData dictData); + + /** + * 修改保存字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int updateDictData(SysDictData dictData); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/SysDictTypeService.java b/jsowell-system/src/main/java/com/jsowell/system/service/SysDictTypeService.java new file mode 100644 index 000000000..1a8c9b63b --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/SysDictTypeService.java @@ -0,0 +1,98 @@ +package com.jsowell.system.service; + +import com.jsowell.common.core.domain.entity.SysDictData; +import com.jsowell.common.core.domain.entity.SysDictType; + +import java.util.List; + +/** + * 字典 业务层 + * + * @author jsowell + */ +public interface SysDictTypeService { + /** + * 根据条件分页查询字典类型 + * + * @param dictType 字典类型信息 + * @return 字典类型集合信息 + */ + public List selectDictTypeList(SysDictType dictType); + + /** + * 根据所有字典类型 + * + * @return 字典类型集合信息 + */ + public List selectDictTypeAll(); + + /** + * 根据字典类型查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据集合信息 + */ + public List selectDictDataByType(String dictType); + + /** + * 根据字典类型ID查询信息 + * + * @param dictId 字典类型ID + * @return 字典类型 + */ + public SysDictType selectDictTypeById(Long dictId); + + /** + * 根据字典类型查询信息 + * + * @param dictType 字典类型 + * @return 字典类型 + */ + public SysDictType selectDictTypeByType(String dictType); + + /** + * 批量删除字典信息 + * + * @param dictIds 需要删除的字典ID + */ + public void deleteDictTypeByIds(Long[] dictIds); + + /** + * 加载字典缓存数据 + */ + public void loadingDictCache(); + + /** + * 清空字典缓存数据 + */ + public void clearDictCache(); + + /** + * 重置字典缓存数据 + */ + public void resetDictCache(); + + /** + * 新增保存字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int insertDictType(SysDictType dictType); + + /** + * 修改保存字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int updateDictType(SysDictType dictType); + + /** + * 校验字典类型称是否唯一 + * + * @param dictType 字典类型 + * @return 结果 + */ + public String checkDictTypeUnique(SysDictType dictType); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/SysLogininforService.java b/jsowell-system/src/main/java/com/jsowell/system/service/SysLogininforService.java new file mode 100644 index 000000000..a9da7232e --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/SysLogininforService.java @@ -0,0 +1,40 @@ +package com.jsowell.system.service; + +import com.jsowell.system.domain.SysLogininfor; + +import java.util.List; + +/** + * 系统访问日志情况信息 服务层 + * + * @author jsowell + */ +public interface SysLogininforService { + /** + * 新增系统登录日志 + * + * @param logininfor 访问日志对象 + */ + public void insertLogininfor(SysLogininfor logininfor); + + /** + * 查询系统登录日志集合 + * + * @param logininfor 访问日志对象 + * @return 登录记录集合 + */ + public List selectLogininforList(SysLogininfor logininfor); + + /** + * 批量删除系统登录日志 + * + * @param infoIds 需要删除的登录日志ID + * @return 结果 + */ + public int deleteLogininforByIds(Long[] infoIds); + + /** + * 清空系统登录日志 + */ + public void cleanLogininfor(); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/SysMenuService.java b/jsowell-system/src/main/java/com/jsowell/system/service/SysMenuService.java new file mode 100644 index 000000000..e208b6e88 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/SysMenuService.java @@ -0,0 +1,136 @@ +package com.jsowell.system.service; + +import com.jsowell.common.core.domain.TreeSelect; +import com.jsowell.common.core.domain.entity.SysMenu; +import com.jsowell.system.vo.RouterVO; + +import java.util.List; +import java.util.Set; + +/** + * 菜单 业务层 + * + * @author jsowell + */ +public interface SysMenuService { + /** + * 根据用户查询系统菜单列表 + * + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuList(Long userId); + + /** + * 根据用户查询系统菜单列表 + * + * @param menu 菜单信息 + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuList(SysMenu menu, Long userId); + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + public Set selectMenuPermsByUserId(Long userId); + + /** + * 根据用户ID查询菜单树信息 + * + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuTreeByUserId(Long userId); + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @return 选中菜单列表 + */ + public List selectMenuListByRoleId(Long roleId); + + /** + * 构建前端路由所需要的菜单 + * + * @param menus 菜单列表 + * @return 路由列表 + */ + public List buildMenus(List menus); + + /** + * 构建前端所需要树结构 + * + * @param menus 菜单列表 + * @return 树结构列表 + */ + public List buildMenuTree(List menus); + + /** + * 构建前端所需要下拉树结构 + * + * @param menus 菜单列表 + * @return 下拉树结构列表 + */ + public List buildMenuTreeSelect(List menus); + + /** + * 根据菜单ID查询信息 + * + * @param menuId 菜单ID + * @return 菜单信息 + */ + public SysMenu selectMenuById(Long menuId); + + /** + * 是否存在菜单子节点 + * + * @param menuId 菜单ID + * @return 结果 true 存在 false 不存在 + */ + public boolean hasChildByMenuId(Long menuId); + + /** + * 查询菜单是否存在角色 + * + * @param menuId 菜单ID + * @return 结果 true 存在 false 不存在 + */ + public boolean checkMenuExistRole(Long menuId); + + /** + * 新增保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int insertMenu(SysMenu menu); + + /** + * 修改保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int updateMenu(SysMenu menu); + + /** + * 删除菜单管理信息 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int deleteMenuById(Long menuId); + + /** + * 校验菜单名称是否唯一 + * + * @param menu 菜单信息 + * @return 结果 + */ + public String checkMenuNameUnique(SysMenu menu); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/SysNoticeService.java b/jsowell-system/src/main/java/com/jsowell/system/service/SysNoticeService.java new file mode 100644 index 000000000..40ea05766 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/SysNoticeService.java @@ -0,0 +1,60 @@ +package com.jsowell.system.service; + +import com.jsowell.system.domain.SysNotice; + +import java.util.List; + +/** + * 公告 服务层 + * + * @author jsowell + */ +public interface SysNoticeService { + /** + * 查询公告信息 + * + * @param noticeId 公告ID + * @return 公告信息 + */ + public SysNotice selectNoticeById(Long noticeId); + + /** + * 查询公告列表 + * + * @param notice 公告信息 + * @return 公告集合 + */ + public List selectNoticeList(SysNotice notice); + + /** + * 新增公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int insertNotice(SysNotice notice); + + /** + * 修改公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int updateNotice(SysNotice notice); + + /** + * 删除公告信息 + * + * @param noticeId 公告ID + * @return 结果 + */ + public int deleteNoticeById(Long noticeId); + + /** + * 批量删除公告信息 + * + * @param noticeIds 需要删除的公告ID + * @return 结果 + */ + public int deleteNoticeByIds(Long[] noticeIds); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/SysOperLogService.java b/jsowell-system/src/main/java/com/jsowell/system/service/SysOperLogService.java new file mode 100644 index 000000000..94e033618 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/SysOperLogService.java @@ -0,0 +1,48 @@ +package com.jsowell.system.service; + +import com.jsowell.system.domain.SysOperLog; + +import java.util.List; + +/** + * 操作日志 服务层 + * + * @author jsowell + */ +public interface SysOperLogService { + /** + * 新增操作日志 + * + * @param operLog 操作日志对象 + */ + public void insertOperlog(SysOperLog operLog); + + /** + * 查询系统操作日志集合 + * + * @param operLog 操作日志对象 + * @return 操作日志集合 + */ + public List selectOperLogList(SysOperLog operLog); + + /** + * 批量删除系统操作日志 + * + * @param operIds 需要删除的操作日志ID + * @return 结果 + */ + public int deleteOperLogByIds(Long[] operIds); + + /** + * 查询操作日志详细 + * + * @param operId 操作ID + * @return 操作日志对象 + */ + public SysOperLog selectOperLogById(Long operId); + + /** + * 清空操作日志 + */ + public void cleanOperLog(); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/SysPostService.java b/jsowell-system/src/main/java/com/jsowell/system/service/SysPostService.java new file mode 100644 index 000000000..67d5d6dd6 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/SysPostService.java @@ -0,0 +1,99 @@ +package com.jsowell.system.service; + +import com.jsowell.system.domain.SysPost; + +import java.util.List; + +/** + * 岗位信息 服务层 + * + * @author jsowell + */ +public interface SysPostService { + /** + * 查询岗位信息集合 + * + * @param post 岗位信息 + * @return 岗位列表 + */ + public List selectPostList(SysPost post); + + /** + * 查询所有岗位 + * + * @return 岗位列表 + */ + public List selectPostAll(); + + /** + * 通过岗位ID查询岗位信息 + * + * @param postId 岗位ID + * @return 角色对象信息 + */ + public SysPost selectPostById(Long postId); + + /** + * 根据用户ID获取岗位选择框列表 + * + * @param userId 用户ID + * @return 选中岗位ID列表 + */ + public List selectPostListByUserId(Long userId); + + /** + * 校验岗位名称 + * + * @param post 岗位信息 + * @return 结果 + */ + public String checkPostNameUnique(SysPost post); + + /** + * 校验岗位编码 + * + * @param post 岗位信息 + * @return 结果 + */ + public String checkPostCodeUnique(SysPost post); + + /** + * 通过岗位ID查询岗位使用数量 + * + * @param postId 岗位ID + * @return 结果 + */ + public int countUserPostById(Long postId); + + /** + * 删除岗位信息 + * + * @param postId 岗位ID + * @return 结果 + */ + public int deletePostById(Long postId); + + /** + * 批量删除岗位信息 + * + * @param postIds 需要删除的岗位ID + * @return 结果 + */ + public int deletePostByIds(Long[] postIds); + + /** + * 新增保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int insertPost(SysPost post); + + /** + * 修改保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int updatePost(SysPost post); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/SysRoleService.java b/jsowell-system/src/main/java/com/jsowell/system/service/SysRoleService.java new file mode 100644 index 000000000..4a3f40a36 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/SysRoleService.java @@ -0,0 +1,173 @@ +package com.jsowell.system.service; + +import com.jsowell.common.core.domain.entity.SysRole; +import com.jsowell.system.domain.SysUserRole; + +import java.util.List; +import java.util.Set; + +/** + * 角色业务层 + * + * @author jsowell + */ +public interface SysRoleService { + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + public List selectRoleList(SysRole role); + + /** + * 根据用户ID查询角色列表 + * + * @param userId 用户ID + * @return 角色列表 + */ + public List selectRolesByUserId(Long userId); + + /** + * 根据用户ID查询角色权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + public Set selectRolePermissionByUserId(Long userId); + + /** + * 查询所有角色 + * + * @return 角色列表 + */ + public List selectRoleAll(); + + /** + * 根据用户ID获取角色选择框列表 + * + * @param userId 用户ID + * @return 选中角色ID列表 + */ + public List selectRoleListByUserId(Long userId); + + /** + * 通过角色ID查询角色 + * + * @param roleId 角色ID + * @return 角色对象信息 + */ + public SysRole selectRoleById(Long roleId); + + /** + * 校验角色名称是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + public String checkRoleNameUnique(SysRole role); + + /** + * 校验角色权限是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + public String checkRoleKeyUnique(SysRole role); + + /** + * 校验角色是否允许操作 + * + * @param role 角色信息 + */ + public void checkRoleAllowed(SysRole role); + + /** + * 校验角色是否有数据权限 + * + * @param roleId 角色id + */ + public void checkRoleDataScope(Long roleId); + + /** + * 通过角色ID查询角色使用数量 + * + * @param roleId 角色ID + * @return 结果 + */ + public int countUserRoleByRoleId(Long roleId); + + /** + * 新增保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int insertRole(SysRole role); + + /** + * 修改保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int updateRole(SysRole role); + + /** + * 修改角色状态 + * + * @param role 角色信息 + * @return 结果 + */ + public int updateRoleStatus(SysRole role); + + /** + * 修改数据权限信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int authDataScope(SysRole role); + + /** + * 通过角色ID删除角色 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleById(Long roleId); + + /** + * 批量删除角色信息 + * + * @param roleIds 需要删除的角色ID + * @return 结果 + */ + public int deleteRoleByIds(Long[] roleIds); + + /** + * 取消授权用户角色 + * + * @param userRole 用户和角色关联信息 + * @return 结果 + */ + public int deleteAuthUser(SysUserRole userRole); + + /** + * 批量取消授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要取消授权的用户数据ID + * @return 结果 + */ + public int deleteAuthUsers(Long roleId, Long[] userIds); + + /** + * 批量选择授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要删除的用户数据ID + * @return 结果 + */ + public int insertAuthUsers(Long roleId, Long[] userIds); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/SysUserOnlineService.java b/jsowell-system/src/main/java/com/jsowell/system/service/SysUserOnlineService.java new file mode 100644 index 000000000..4c4af1f4a --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/SysUserOnlineService.java @@ -0,0 +1,47 @@ +package com.jsowell.system.service; + +import com.jsowell.common.core.domain.model.LoginUser; +import com.jsowell.system.domain.SysUserOnline; + +/** + * 在线用户 服务层 + * + * @author jsowell + */ +public interface SysUserOnlineService { + /** + * 通过登录地址查询信息 + * + * @param ipaddr 登录地址 + * @param user 用户信息 + * @return 在线用户信息 + */ + public SysUserOnline selectOnlineByIpaddr(String ipaddr, LoginUser user); + + /** + * 通过用户名称查询信息 + * + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + public SysUserOnline selectOnlineByUserName(String userName, LoginUser user); + + /** + * 通过登录地址/用户名称查询信息 + * + * @param ipaddr 登录地址 + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + public SysUserOnline selectOnlineByInfo(String ipaddr, String userName, LoginUser user); + + /** + * 设置在线用户信息 + * + * @param user 用户信息 + * @return 在线用户 + */ + public SysUserOnline loginUserToUserOnline(LoginUser user); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/SysUserService.java b/jsowell-system/src/main/java/com/jsowell/system/service/SysUserService.java new file mode 100644 index 000000000..ff1c475ed --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/SysUserService.java @@ -0,0 +1,206 @@ +package com.jsowell.system.service; + +import com.jsowell.common.core.domain.entity.SysUser; + +import java.util.List; + +/** + * 用户 业务层 + * + * @author jsowell + */ +public interface SysUserService { + /** + * 根据条件分页查询用户列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectUserList(SysUser user); + + /** + * 根据条件分页查询已分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectAllocatedList(SysUser user); + + /** + * 根据条件分页查询未分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectUnallocatedList(SysUser user); + + /** + * 通过用户名查询用户 + * + * @param userName 用户名 + * @return 用户对象信息 + */ + public SysUser selectUserByUserName(String userName); + + /** + * 通过用户ID查询用户 + * + * @param userId 用户ID + * @return 用户对象信息 + */ + public SysUser selectUserById(Long userId); + + /** + * 根据用户ID查询用户所属角色组 + * + * @param userName 用户名 + * @return 结果 + */ + public String selectUserRoleGroup(String userName); + + /** + * 根据用户ID查询用户所属岗位组 + * + * @param userName 用户名 + * @return 结果 + */ + public String selectUserPostGroup(String userName); + + /** + * 校验用户名称是否唯一 + * + * @param userName 用户名称 + * @return 结果 + */ + public String checkUserNameUnique(String userName); + + /** + * 校验手机号码是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + public String checkPhoneUnique(SysUser user); + + /** + * 校验email是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + public String checkEmailUnique(SysUser user); + + /** + * 校验用户是否允许操作 + * + * @param user 用户信息 + */ + public void checkUserAllowed(SysUser user); + + /** + * 校验用户是否有数据权限 + * + * @param userId 用户id + */ + public void checkUserDataScope(Long userId); + + /** + * 新增用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int insertUser(SysUser user); + + /** + * 注册用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean registerUser(SysUser user); + + /** + * 修改用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUser(SysUser user); + + /** + * 用户授权角色 + * + * @param userId 用户ID + * @param roleIds 角色组 + */ + public void insertUserAuth(Long userId, Long[] roleIds); + + /** + * 修改用户状态 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUserStatus(SysUser user); + + /** + * 修改用户基本信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUserProfile(SysUser user); + + /** + * 修改用户头像 + * + * @param userName 用户名 + * @param avatar 头像地址 + * @return 结果 + */ + public boolean updateUserAvatar(String userName, String avatar); + + /** + * 重置用户密码 + * + * @param user 用户信息 + * @return 结果 + */ + public int resetPwd(SysUser user); + + /** + * 重置用户密码 + * + * @param userName 用户名 + * @param password 密码 + * @return 结果 + */ + public int resetUserPwd(String userName, String password); + + /** + * 通过用户ID删除用户 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserById(Long userId); + + /** + * 批量删除用户信息 + * + * @param userIds 需要删除的用户ID + * @return 结果 + */ + public int deleteUserByIds(Long[] userIds); + + /** + * 导入用户数据 + * + * @param userList 用户数据列表 + * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据 + * @param operName 操作用户 + * @return 结果 + */ + public String importUser(List userList, Boolean isUpdateSupport, String operName); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysConfigServiceImpl.java b/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysConfigServiceImpl.java new file mode 100644 index 000000000..ba95e2b87 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysConfigServiceImpl.java @@ -0,0 +1,204 @@ +package com.jsowell.system.service.impl; + +import com.jsowell.common.annotation.DataSource; +import com.jsowell.common.constant.CacheConstants; +import com.jsowell.common.constant.UserConstants; +import com.jsowell.common.core.redis.RedisCache; +import com.jsowell.common.core.text.Convert; +import com.jsowell.common.enums.DataSourceType; +import com.jsowell.common.exception.ServiceException; +import com.jsowell.common.util.StringUtils; +import com.jsowell.system.domain.SysConfig; +import com.jsowell.system.mapper.SysConfigMapper; +import com.jsowell.system.service.SysConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.util.Collection; +import java.util.List; + +/** + * 参数配置 服务层实现 + * + * @author jsowell + */ +@Service +public class SysConfigServiceImpl implements SysConfigService { + @Autowired + private SysConfigMapper configMapper; + + @Autowired + private RedisCache redisCache; + + /** + * 项目启动时,初始化参数到缓存 + */ + @PostConstruct + public void init() { + loadingConfigCache(); + } + + /** + * 查询参数配置信息 + * + * @param configId 参数配置ID + * @return 参数配置信息 + */ + @Override + @DataSource(DataSourceType.MASTER) + public SysConfig selectConfigById(Long configId) { + SysConfig config = new SysConfig(); + config.setConfigId(configId); + return configMapper.selectConfig(config); + } + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数key + * @return 参数键值 + */ + @Override + public String selectConfigByKey(String configKey) { + String configValue = Convert.toStr(redisCache.getCacheObject(getCacheKey(configKey))); + if (StringUtils.isNotEmpty(configValue)) { + return configValue; + } + SysConfig config = new SysConfig(); + config.setConfigKey(configKey); + SysConfig retConfig = configMapper.selectConfig(config); + if (StringUtils.isNotNull(retConfig)) { + redisCache.setCacheObject(getCacheKey(configKey), retConfig.getConfigValue()); + return retConfig.getConfigValue(); + } + return StringUtils.EMPTY; + } + + /** + * 获取验证码开关 + * + * @return true开启,false关闭 + */ + @Override + public boolean selectCaptchaEnabled() { + String captchaEnabled = selectConfigByKey("sys.account.captchaEnabled"); + if (StringUtils.isEmpty(captchaEnabled)) { + return true; + } + return Convert.toBool(captchaEnabled); + } + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + @Override + public List selectConfigList(SysConfig config) { + return configMapper.selectConfigList(config); + } + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public int insertConfig(SysConfig config) { + int row = configMapper.insertConfig(config); + if (row > 0) { + redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue()); + } + return row; + } + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public int updateConfig(SysConfig config) { + int row = configMapper.updateConfig(config); + if (row > 0) { + redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue()); + } + return row; + } + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + */ + @Override + public void deleteConfigByIds(Long[] configIds) { + for (Long configId : configIds) { + SysConfig config = selectConfigById(configId); + if (StringUtils.equals(UserConstants.YES, config.getConfigType())) { + throw new ServiceException(String.format("内置参数【%1$s】不能删除 ", config.getConfigKey())); + } + configMapper.deleteConfigById(configId); + redisCache.deleteObject(getCacheKey(config.getConfigKey())); + } + } + + /** + * 加载参数缓存数据 + */ + @Override + public void loadingConfigCache() { + List configsList = configMapper.selectConfigList(new SysConfig()); + for (SysConfig config : configsList) { + redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue()); + } + } + + /** + * 清空参数缓存数据 + */ + @Override + public void clearConfigCache() { + Collection keys = redisCache.keys(CacheConstants.SYS_CONFIG_KEY + "*"); + redisCache.deleteObject(keys); + } + + /** + * 重置参数缓存数据 + */ + @Override + public void resetConfigCache() { + clearConfigCache(); + loadingConfigCache(); + } + + /** + * 校验参数键名是否唯一 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public String checkConfigKeyUnique(SysConfig config) { + Long configId = StringUtils.isNull(config.getConfigId()) ? -1L : config.getConfigId(); + SysConfig info = configMapper.checkConfigKeyUnique(config.getConfigKey()); + if (StringUtils.isNotNull(info) && info.getConfigId().longValue() != configId.longValue()) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 设置cache key + * + * @param configKey 参数键 + * @return 缓存键key + */ + private String getCacheKey(String configKey) { + return CacheConstants.SYS_CONFIG_KEY + configKey; + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysDeptServiceImpl.java b/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysDeptServiceImpl.java new file mode 100644 index 000000000..176529a67 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysDeptServiceImpl.java @@ -0,0 +1,295 @@ +package com.jsowell.system.service.impl; + +import com.jsowell.common.annotation.DataScope; +import com.jsowell.common.constant.UserConstants; +import com.jsowell.common.core.domain.TreeSelect; +import com.jsowell.common.core.domain.entity.SysDept; +import com.jsowell.common.core.domain.entity.SysRole; +import com.jsowell.common.core.domain.entity.SysUser; +import com.jsowell.common.core.text.Convert; +import com.jsowell.common.exception.ServiceException; +import com.jsowell.common.util.SecurityUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.spring.SpringUtils; +import com.jsowell.system.mapper.SysDeptMapper; +import com.jsowell.system.mapper.SysRoleMapper; +import com.jsowell.system.service.SysDeptService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 部门管理 服务实现 + * + * @author jsowell + */ +@Service +public class SysDeptServiceImpl implements SysDeptService { + @Autowired + private SysDeptMapper deptMapper; + + @Autowired + private SysRoleMapper roleMapper; + + /** + * 查询部门管理数据 + * + * @param dept 部门信息 + * @return 部门信息集合 + */ + @Override + @DataScope(deptAlias = "d") + public List selectDeptList(SysDept dept) { + return deptMapper.selectDeptList(dept); + } + + /** + * 构建前端所需要树结构 + * + * @param depts 部门列表 + * @return 树结构列表 + */ + @Override + public List buildDeptTree(List depts) { + List returnList = new ArrayList(); + List tempList = new ArrayList(); + for (SysDept dept : depts) { + tempList.add(dept.getDeptId()); + } + for (SysDept dept : depts) { + // 如果是顶级节点, 遍历该父节点的所有子节点 + if (!tempList.contains(dept.getParentId())) { + recursionFn(depts, dept); + returnList.add(dept); + } + } + if (returnList.isEmpty()) { + returnList = depts; + } + return returnList; + } + + /** + * 构建前端所需要下拉树结构 + * + * @param depts 部门列表 + * @return 下拉树结构列表 + */ + @Override + public List buildDeptTreeSelect(List depts) { + List deptTrees = buildDeptTree(depts); + return deptTrees.stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + /** + * 根据角色ID查询部门树信息 + * + * @param roleId 角色ID + * @return 选中部门列表 + */ + @Override + public List selectDeptListByRoleId(Long roleId) { + SysRole role = roleMapper.selectRoleById(roleId); + return deptMapper.selectDeptListByRoleId(roleId, role.isDeptCheckStrictly()); + } + + /** + * 根据部门ID查询信息 + * + * @param deptId 部门ID + * @return 部门信息 + */ + @Override + public SysDept selectDeptById(Long deptId) { + return deptMapper.selectDeptById(deptId); + } + + /** + * 根据ID查询所有子部门(正常状态) + * + * @param deptId 部门ID + * @return 子部门数 + */ + @Override + public int selectNormalChildrenDeptById(Long deptId) { + return deptMapper.selectNormalChildrenDeptById(deptId); + } + + /** + * 是否存在子节点 + * + * @param deptId 部门ID + * @return 结果 + */ + @Override + public boolean hasChildByDeptId(Long deptId) { + int result = deptMapper.hasChildByDeptId(deptId); + return result > 0; + } + + /** + * 查询部门是否存在用户 + * + * @param deptId 部门ID + * @return 结果 true 存在 false 不存在 + */ + @Override + public boolean checkDeptExistUser(Long deptId) { + int result = deptMapper.checkDeptExistUser(deptId); + return result > 0; + } + + /** + * 校验部门名称是否唯一 + * + * @param dept 部门信息 + * @return 结果 + */ + @Override + public String checkDeptNameUnique(SysDept dept) { + Long deptId = StringUtils.isNull(dept.getDeptId()) ? -1L : dept.getDeptId(); + SysDept info = deptMapper.checkDeptNameUnique(dept.getDeptName(), dept.getParentId()); + if (StringUtils.isNotNull(info) && info.getDeptId().longValue() != deptId.longValue()) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验部门是否有数据权限 + * + * @param deptId 部门id + */ + @Override + public void checkDeptDataScope(Long deptId) { + if (!SysUser.isAdmin(SecurityUtils.getUserId())) { + SysDept dept = new SysDept(); + dept.setDeptId(deptId); + List depts = SpringUtils.getAopProxy(this).selectDeptList(dept); + if (StringUtils.isEmpty(depts)) { + throw new ServiceException("没有权限访问部门数据!"); + } + } + } + + /** + * 新增保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + @Override + public int insertDept(SysDept dept) { + SysDept info = deptMapper.selectDeptById(dept.getParentId()); + // 如果父节点不为正常状态,则不允许新增子节点 + if (!UserConstants.DEPT_NORMAL.equals(info.getStatus())) { + throw new ServiceException("部门停用,不允许新增"); + } + dept.setAncestors(info.getAncestors() + "," + dept.getParentId()); + return deptMapper.insertDept(dept); + } + + /** + * 修改保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + @Override + public int updateDept(SysDept dept) { + SysDept newParentDept = deptMapper.selectDeptById(dept.getParentId()); + SysDept oldDept = deptMapper.selectDeptById(dept.getDeptId()); + if (StringUtils.isNotNull(newParentDept) && StringUtils.isNotNull(oldDept)) { + String newAncestors = newParentDept.getAncestors() + "," + newParentDept.getDeptId(); + String oldAncestors = oldDept.getAncestors(); + dept.setAncestors(newAncestors); + updateDeptChildren(dept.getDeptId(), newAncestors, oldAncestors); + } + int result = deptMapper.updateDept(dept); + if (UserConstants.DEPT_NORMAL.equals(dept.getStatus()) && StringUtils.isNotEmpty(dept.getAncestors()) + && !StringUtils.equals("0", dept.getAncestors())) { + // 如果该部门是启用状态,则启用该部门的所有上级部门 + updateParentDeptStatusNormal(dept); + } + return result; + } + + /** + * 修改该部门的父级部门状态 + * + * @param dept 当前部门 + */ + private void updateParentDeptStatusNormal(SysDept dept) { + String ancestors = dept.getAncestors(); + Long[] deptIds = Convert.toLongArray(ancestors); + deptMapper.updateDeptStatusNormal(deptIds); + } + + /** + * 修改子元素关系 + * + * @param deptId 被修改的部门ID + * @param newAncestors 新的父ID集合 + * @param oldAncestors 旧的父ID集合 + */ + public void updateDeptChildren(Long deptId, String newAncestors, String oldAncestors) { + List children = deptMapper.selectChildrenDeptById(deptId); + for (SysDept child : children) { + child.setAncestors(child.getAncestors().replaceFirst(oldAncestors, newAncestors)); + } + if (children.size() > 0) { + deptMapper.updateDeptChildren(children); + } + } + + /** + * 删除部门管理信息 + * + * @param deptId 部门ID + * @return 结果 + */ + @Override + public int deleteDeptById(Long deptId) { + return deptMapper.deleteDeptById(deptId); + } + + /** + * 递归列表 + */ + private void recursionFn(List list, SysDept t) { + // 得到子节点列表 + List childList = getChildList(list, t); + t.setChildren(childList); + for (SysDept tChild : childList) { + if (hasChild(list, tChild)) { + recursionFn(list, tChild); + } + } + } + + /** + * 得到子节点列表 + */ + private List getChildList(List list, SysDept t) { + List tlist = new ArrayList(); + Iterator it = list.iterator(); + while (it.hasNext()) { + SysDept n = (SysDept) it.next(); + if (StringUtils.isNotNull(n.getParentId()) && n.getParentId().longValue() == t.getDeptId().longValue()) { + tlist.add(n); + } + } + return tlist; + } + + /** + * 判断是否有子节点 + */ + private boolean hasChild(List list, SysDept t) { + return getChildList(list, t).size() > 0; + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysDictDataServiceImpl.java b/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysDictDataServiceImpl.java new file mode 100644 index 000000000..3c3eed963 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysDictDataServiceImpl.java @@ -0,0 +1,102 @@ +package com.jsowell.system.service.impl; + +import com.jsowell.common.core.domain.entity.SysDictData; +import com.jsowell.common.util.DictUtils; +import com.jsowell.system.mapper.SysDictDataMapper; +import com.jsowell.system.service.SysDictDataService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 字典 业务层处理 + * + * @author jsowell + */ +@Service +public class SysDictDataServiceImpl implements SysDictDataService { + @Autowired + private SysDictDataMapper dictDataMapper; + + /** + * 根据条件分页查询字典数据 + * + * @param dictData 字典数据信息 + * @return 字典数据集合信息 + */ + @Override + public List selectDictDataList(SysDictData dictData) { + return dictDataMapper.selectDictDataList(dictData); + } + + /** + * 根据字典类型和字典键值查询字典数据信息 + * + * @param dictType 字典类型 + * @param dictValue 字典键值 + * @return 字典标签 + */ + @Override + public String selectDictLabel(String dictType, String dictValue) { + return dictDataMapper.selectDictLabel(dictType, dictValue); + } + + /** + * 根据字典数据ID查询信息 + * + * @param dictCode 字典数据ID + * @return 字典数据 + */ + @Override + public SysDictData selectDictDataById(Long dictCode) { + return dictDataMapper.selectDictDataById(dictCode); + } + + /** + * 批量删除字典数据信息 + * + * @param dictCodes 需要删除的字典数据ID + */ + @Override + public void deleteDictDataByIds(Long[] dictCodes) { + for (Long dictCode : dictCodes) { + SysDictData data = selectDictDataById(dictCode); + dictDataMapper.deleteDictDataById(dictCode); + List dictDatas = dictDataMapper.selectDictDataByType(data.getDictType()); + DictUtils.setDictCache(data.getDictType(), dictDatas); + } + } + + /** + * 新增保存字典数据信息 + * + * @param data 字典数据信息 + * @return 结果 + */ + @Override + public int insertDictData(SysDictData data) { + int row = dictDataMapper.insertDictData(data); + if (row > 0) { + List dictDatas = dictDataMapper.selectDictDataByType(data.getDictType()); + DictUtils.setDictCache(data.getDictType(), dictDatas); + } + return row; + } + + /** + * 修改保存字典数据信息 + * + * @param data 字典数据信息 + * @return 结果 + */ + @Override + public int updateDictData(SysDictData data) { + int row = dictDataMapper.updateDictData(data); + if (row > 0) { + List dictDatas = dictDataMapper.selectDictDataByType(data.getDictType()); + DictUtils.setDictCache(data.getDictType(), dictDatas); + } + return row; + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysDictTypeServiceImpl.java b/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysDictTypeServiceImpl.java new file mode 100644 index 000000000..f26c79526 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysDictTypeServiceImpl.java @@ -0,0 +1,202 @@ +package com.jsowell.system.service.impl; + +import com.jsowell.common.constant.UserConstants; +import com.jsowell.common.core.domain.entity.SysDictData; +import com.jsowell.common.core.domain.entity.SysDictType; +import com.jsowell.common.exception.ServiceException; +import com.jsowell.common.util.DictUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.system.mapper.SysDictDataMapper; +import com.jsowell.system.mapper.SysDictTypeMapper; +import com.jsowell.system.service.SysDictTypeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.PostConstruct; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 字典 业务层处理 + * + * @author jsowell + */ +@Service +public class SysDictTypeServiceImpl implements SysDictTypeService { + @Autowired + private SysDictTypeMapper dictTypeMapper; + + @Autowired + private SysDictDataMapper dictDataMapper; + + /** + * 项目启动时,初始化字典到缓存 + */ + @PostConstruct + public void init() { + loadingDictCache(); + } + + /** + * 根据条件分页查询字典类型 + * + * @param dictType 字典类型信息 + * @return 字典类型集合信息 + */ + @Override + public List selectDictTypeList(SysDictType dictType) { + return dictTypeMapper.selectDictTypeList(dictType); + } + + /** + * 根据所有字典类型 + * + * @return 字典类型集合信息 + */ + @Override + public List selectDictTypeAll() { + return dictTypeMapper.selectDictTypeAll(); + } + + /** + * 根据字典类型查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据集合信息 + */ + @Override + public List selectDictDataByType(String dictType) { + List dictDatas = DictUtils.getDictCache(dictType); + if (StringUtils.isNotEmpty(dictDatas)) { + return dictDatas; + } + dictDatas = dictDataMapper.selectDictDataByType(dictType); + if (StringUtils.isNotEmpty(dictDatas)) { + DictUtils.setDictCache(dictType, dictDatas); + return dictDatas; + } + return null; + } + + /** + * 根据字典类型ID查询信息 + * + * @param dictId 字典类型ID + * @return 字典类型 + */ + @Override + public SysDictType selectDictTypeById(Long dictId) { + return dictTypeMapper.selectDictTypeById(dictId); + } + + /** + * 根据字典类型查询信息 + * + * @param dictType 字典类型 + * @return 字典类型 + */ + @Override + public SysDictType selectDictTypeByType(String dictType) { + return dictTypeMapper.selectDictTypeByType(dictType); + } + + /** + * 批量删除字典类型信息 + * + * @param dictIds 需要删除的字典ID + */ + @Override + public void deleteDictTypeByIds(Long[] dictIds) { + for (Long dictId : dictIds) { + SysDictType dictType = selectDictTypeById(dictId); + if (dictDataMapper.countDictDataByType(dictType.getDictType()) > 0) { + throw new ServiceException(String.format("%1$s已分配,不能删除", dictType.getDictName())); + } + dictTypeMapper.deleteDictTypeById(dictId); + DictUtils.removeDictCache(dictType.getDictType()); + } + } + + /** + * 加载字典缓存数据 + */ + @Override + public void loadingDictCache() { + SysDictData dictData = new SysDictData(); + dictData.setStatus("0"); + Map> dictDataMap = dictDataMapper.selectDictDataList(dictData).stream().collect(Collectors.groupingBy(SysDictData::getDictType)); + for (Map.Entry> entry : dictDataMap.entrySet()) { + DictUtils.setDictCache(entry.getKey(), entry.getValue().stream().sorted(Comparator.comparing(SysDictData::getDictSort)).collect(Collectors.toList())); + } + } + + /** + * 清空字典缓存数据 + */ + @Override + public void clearDictCache() { + DictUtils.clearDictCache(); + } + + /** + * 重置字典缓存数据 + */ + @Override + public void resetDictCache() { + clearDictCache(); + loadingDictCache(); + } + + /** + * 新增保存字典类型信息 + * + * @param dict 字典类型信息 + * @return 结果 + */ + @Override + public int insertDictType(SysDictType dict) { + int row = dictTypeMapper.insertDictType(dict); + if (row > 0) { + DictUtils.setDictCache(dict.getDictType(), null); + } + return row; + } + + /** + * 修改保存字典类型信息 + * + * @param dict 字典类型信息 + * @return 结果 + */ + @Override + @Transactional + public int updateDictType(SysDictType dict) { + SysDictType oldDict = dictTypeMapper.selectDictTypeById(dict.getDictId()); + dictDataMapper.updateDictDataType(oldDict.getDictType(), dict.getDictType()); + int row = dictTypeMapper.updateDictType(dict); + if (row > 0) { + List dictDatas = dictDataMapper.selectDictDataByType(dict.getDictType()); + DictUtils.setDictCache(dict.getDictType(), dictDatas); + } + return row; + } + + /** + * 校验字典类型称是否唯一 + * + * @param dict 字典类型 + * @return 结果 + */ + @Override + public String checkDictTypeUnique(SysDictType dict) { + Long dictId = StringUtils.isNull(dict.getDictId()) ? -1L : dict.getDictId(); + SysDictType dictType = dictTypeMapper.checkDictTypeUnique(dict.getDictType()); + if (StringUtils.isNotNull(dictType) && dictType.getDictId().longValue() != dictId.longValue()) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysLogininforServiceImpl.java b/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysLogininforServiceImpl.java new file mode 100644 index 000000000..deb9bb02e --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysLogininforServiceImpl.java @@ -0,0 +1,61 @@ +package com.jsowell.system.service.impl; + +import com.jsowell.system.domain.SysLogininfor; +import com.jsowell.system.mapper.SysLogininforMapper; +import com.jsowell.system.service.SysLogininforService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 系统访问日志情况信息 服务层处理 + * + * @author jsowell + */ +@Service +public class SysLogininforServiceImpl implements SysLogininforService { + + @Autowired + private SysLogininforMapper logininforMapper; + + /** + * 新增系统登录日志 + * + * @param logininfor 访问日志对象 + */ + @Override + public void insertLogininfor(SysLogininfor logininfor) { + logininforMapper.insertLogininfor(logininfor); + } + + /** + * 查询系统登录日志集合 + * + * @param logininfor 访问日志对象 + * @return 登录记录集合 + */ + @Override + public List selectLogininforList(SysLogininfor logininfor) { + return logininforMapper.selectLogininforList(logininfor); + } + + /** + * 批量删除系统登录日志 + * + * @param infoIds 需要删除的登录日志ID + * @return 结果 + */ + @Override + public int deleteLogininforByIds(Long[] infoIds) { + return logininforMapper.deleteLogininforByIds(infoIds); + } + + /** + * 清空系统登录日志 + */ + @Override + public void cleanLogininfor() { + logininforMapper.cleanLogininfor(); + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysMenuServiceImpl.java b/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysMenuServiceImpl.java new file mode 100644 index 000000000..ba4b5a896 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysMenuServiceImpl.java @@ -0,0 +1,457 @@ +package com.jsowell.system.service.impl; + +import com.jsowell.common.constant.Constants; +import com.jsowell.common.constant.UserConstants; +import com.jsowell.common.core.domain.TreeSelect; +import com.jsowell.common.core.domain.entity.SysMenu; +import com.jsowell.common.core.domain.entity.SysRole; +import com.jsowell.common.core.domain.entity.SysUser; +import com.jsowell.common.util.SecurityUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.system.mapper.SysMenuMapper; +import com.jsowell.system.mapper.SysRoleMapper; +import com.jsowell.system.mapper.SysRoleMenuMapper; +import com.jsowell.system.service.SysMenuService; +import com.jsowell.system.vo.MetaVO; +import com.jsowell.system.vo.RouterVO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 菜单 业务层处理 + * + * @author jsowell + */ +@Slf4j +@Service +public class SysMenuServiceImpl implements SysMenuService { + public static final String PREMISSION_STRING = "perms[\"{0}\"]"; + + @Autowired + private SysMenuMapper menuMapper; + + @Autowired + private SysRoleMapper roleMapper; + + @Autowired + private SysRoleMenuMapper roleMenuMapper; + + /** + * 根据用户查询系统菜单列表 + * + * @param userId 用户ID + * @return 菜单列表 + */ + @Override + public List selectMenuList(Long userId) { + return selectMenuList(new SysMenu(), userId); + } + + /** + * 查询系统菜单列表 + * + * @param menu 菜单信息 + * @return 菜单列表 + */ + @Override + public List selectMenuList(SysMenu menu, Long userId) { + List menuList = null; + // 管理员显示所有菜单信息 + if (SysUser.isAdmin(userId)) { + menuList = menuMapper.selectMenuList(menu); + } else { + menu.getParams().put("userId", userId); + menuList = menuMapper.selectMenuListByUserId(menu); + } + return menuList; + } + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + @Override + public Set selectMenuPermsByUserId(Long userId) { + List perms = menuMapper.selectMenuPermsByUserId(userId); + Set permsSet = new HashSet<>(); + for (String perm : perms) { + if (StringUtils.isNotEmpty(perm)) { + permsSet.addAll(Arrays.asList(perm.trim().split(","))); + } + } + return permsSet; + } + + /** + * 根据用户ID查询菜单 + * + * @param userId 用户名称 + * @return 菜单列表 + */ + @Override + public List selectMenuTreeByUserId(Long userId) { + List menus = null; + if (SecurityUtils.isAdmin(userId)) { + menus = menuMapper.selectMenuTreeAll(); + } else { + menus = menuMapper.selectMenuTreeByUserId(userId); + } + return getChildPerms(menus, 0); + } + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @return 选中菜单列表 + */ + @Override + public List selectMenuListByRoleId(Long roleId) { + SysRole role = roleMapper.selectRoleById(roleId); + return menuMapper.selectMenuListByRoleId(roleId, role.isMenuCheckStrictly()); + } + + /** + * 构建前端路由所需要的菜单 + * + * @param menus 菜单列表 + * @return 路由列表 + */ + @Override + public List buildMenus(List menus) { + List routers = new LinkedList(); + for (SysMenu menu : menus) { + RouterVO router = new RouterVO(); + router.setHidden("1".equals(menu.getVisible())); + router.setName(getRouteName(menu)); + router.setPath(getRouterPath(menu)); + router.setComponent(getComponent(menu)); + router.setQuery(menu.getQuery()); + router.setMeta(new MetaVO(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath())); + List cMenus = menu.getChildren(); + if (!cMenus.isEmpty() && cMenus.size() > 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType())) { + router.setAlwaysShow(true); + router.setRedirect("noRedirect"); + router.setChildren(buildMenus(cMenus)); + } else if (isMenuFrame(menu)) { + router.setMeta(null); + List childrenList = new ArrayList(); + RouterVO children = new RouterVO(); + children.setPath(menu.getPath()); + children.setComponent(menu.getComponent()); + children.setName(StringUtils.capitalize(menu.getPath())); + children.setMeta(new MetaVO(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath())); + children.setQuery(menu.getQuery()); + childrenList.add(children); + router.setChildren(childrenList); + } else if (menu.getParentId().intValue() == 0 && isInnerLink(menu)) { + router.setMeta(new MetaVO(menu.getMenuName(), menu.getIcon())); + router.setPath("/"); + List childrenList = new ArrayList(); + RouterVO children = new RouterVO(); + String routerPath = innerLinkReplaceEach(menu.getPath()); + children.setPath(routerPath); + children.setComponent(UserConstants.INNER_LINK); + children.setName(StringUtils.capitalize(routerPath)); + children.setMeta(new MetaVO(menu.getMenuName(), menu.getIcon(), menu.getPath())); + childrenList.add(children); + router.setChildren(childrenList); + } + routers.add(router); + } + return routers; + } + + /** + * 构建前端所需要树结构 + * + * @param menus 菜单列表 + * @return 树结构列表 + */ + @Override + public List buildMenuTree(List menus) { + List returnList = new ArrayList(); + List tempList = new ArrayList(); + for (SysMenu dept : menus) { + tempList.add(dept.getMenuId()); + } + for (Iterator iterator = menus.iterator(); iterator.hasNext(); ) { + SysMenu menu = (SysMenu) iterator.next(); + // 如果是顶级节点, 遍历该父节点的所有子节点 + if (!tempList.contains(menu.getParentId())) { + recursionFn(menus, menu); + returnList.add(menu); + } + } + if (returnList.isEmpty()) { + returnList = menus; + } + return returnList; + } + + /** + * 构建前端所需要下拉树结构 + * + * @param menus 菜单列表 + * @return 下拉树结构列表 + */ + @Override + public List buildMenuTreeSelect(List menus) { + List menuTrees = buildMenuTree(menus); + return menuTrees.stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + /** + * 根据菜单ID查询信息 + * + * @param menuId 菜单ID + * @return 菜单信息 + */ + @Override + public SysMenu selectMenuById(Long menuId) { + return menuMapper.selectMenuById(menuId); + } + + /** + * 是否存在菜单子节点 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public boolean hasChildByMenuId(Long menuId) { + int result = menuMapper.hasChildByMenuId(menuId); + return result > 0; + } + + /** + * 查询菜单使用数量 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public boolean checkMenuExistRole(Long menuId) { + int result = roleMenuMapper.checkMenuExistRole(menuId); + return result > 0; + } + + /** + * 新增保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public int insertMenu(SysMenu menu) { + return menuMapper.insertMenu(menu); + } + + /** + * 修改保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public int updateMenu(SysMenu menu) { + return menuMapper.updateMenu(menu); + } + + /** + * 删除菜单管理信息 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public int deleteMenuById(Long menuId) { + return menuMapper.deleteMenuById(menuId); + } + + /** + * 校验菜单名称是否唯一 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public String checkMenuNameUnique(SysMenu menu) { + Long menuId = StringUtils.isNull(menu.getMenuId()) ? -1L : menu.getMenuId(); + SysMenu info = menuMapper.checkMenuNameUnique(menu.getMenuName(), menu.getParentId()); + if (StringUtils.isNotNull(info) && info.getMenuId().longValue() != menuId.longValue()) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 获取路由名称 + * + * @param menu 菜单信息 + * @return 路由名称 + */ + public String getRouteName(SysMenu menu) { + String routerName = StringUtils.capitalize(menu.getPath()); + // 非外链并且是一级目录(类型为目录) + if (isMenuFrame(menu)) { + routerName = StringUtils.EMPTY; + } + return routerName; + } + + /** + * 获取路由地址 + * + * @param menu 菜单信息 + * @return 路由地址 + */ + public String getRouterPath(SysMenu menu) { + String routerPath = menu.getPath(); + // 内链打开外网方式 + if (menu.getParentId().intValue() != 0 && isInnerLink(menu)) { + routerPath = innerLinkReplaceEach(routerPath); + } + // 非外链并且是一级目录(类型为目录) + if (0 == menu.getParentId().intValue() && UserConstants.TYPE_DIR.equals(menu.getMenuType()) + && UserConstants.NO_FRAME.equals(menu.getIsFrame())) { + routerPath = "/" + menu.getPath(); + } + // 非外链并且是一级目录(类型为菜单) + else if (isMenuFrame(menu)) { + routerPath = "/"; + } + return routerPath; + } + + /** + * 获取组件信息 + * + * @param menu 菜单信息 + * @return 组件信息 + */ + public String getComponent(SysMenu menu) { + String component = UserConstants.LAYOUT; + if (StringUtils.isNotEmpty(menu.getComponent()) && !isMenuFrame(menu)) { + component = menu.getComponent(); + } else if (StringUtils.isEmpty(menu.getComponent()) && menu.getParentId().intValue() != 0 && isInnerLink(menu)) { + component = UserConstants.INNER_LINK; + } else if (StringUtils.isEmpty(menu.getComponent()) && isParentView(menu)) { + component = UserConstants.PARENT_VIEW; + } + return component; + } + + /** + * 是否为菜单内部跳转 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isMenuFrame(SysMenu menu) { + return menu.getParentId().intValue() == 0 && UserConstants.TYPE_MENU.equals(menu.getMenuType()) + && menu.getIsFrame().equals(UserConstants.NO_FRAME); + } + + /** + * 是否为内链组件 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isInnerLink(SysMenu menu) { + return menu.getIsFrame().equals(UserConstants.NO_FRAME) && StringUtils.isHttp(menu.getPath()); + } + + /** + * 是否为parent_view组件 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isParentView(SysMenu menu) { + return menu.getParentId().intValue() != 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType()); + } + + /** + * 根据父节点的ID获取所有子节点 + * + * @param list 分类表 + * @param parentId 传入的父节点ID + * @return String + */ + public List getChildPerms(List list, int parentId) { + List returnList = new ArrayList(); + for (Iterator iterator = list.iterator(); iterator.hasNext(); ) { + SysMenu t = (SysMenu) iterator.next(); + // 一、根据传入的某个父节点ID,遍历该父节点的所有子节点 + if (t.getParentId() == parentId) { + recursionFn(list, t); + returnList.add(t); + } + } + return returnList; + } + + /** + * 递归列表 + * + * @param list + * @param t + */ + private void recursionFn(List list, SysMenu t) { + // 得到子节点列表 + List childList = getChildList(list, t); + t.setChildren(childList); + for (SysMenu tChild : childList) { + if (hasChild(list, tChild)) { + recursionFn(list, tChild); + } + } + } + + /** + * 得到子节点列表 + */ + private List getChildList(List list, SysMenu t) { + // log.info("得到子节点列表 list:{}, SysMenu:{}", JSONObject.toJSONString(list), JSONObject.toJSONString(t)); + List tlist = new ArrayList(); + Iterator it = list.iterator(); + while (it.hasNext()) { + SysMenu n = (SysMenu) it.next(); + if (n.getParentId().longValue() == t.getMenuId().longValue()) { + tlist.add(n); + } + } + return tlist; + } + + /** + * 判断是否有子节点 + */ + private boolean hasChild(List list, SysMenu t) { + return getChildList(list, t).size() > 0; + } + + /** + * 内链域名特殊字符替换 + * + * @return + */ + public String innerLinkReplaceEach(String path) { + return StringUtils.replaceEach(path, new String[]{Constants.HTTP, Constants.HTTPS}, + new String[]{"", ""}); + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysNoticeServiceImpl.java b/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysNoticeServiceImpl.java new file mode 100644 index 000000000..c74fad41d --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysNoticeServiceImpl.java @@ -0,0 +1,86 @@ +package com.jsowell.system.service.impl; + +import com.jsowell.system.domain.SysNotice; +import com.jsowell.system.mapper.SysNoticeMapper; +import com.jsowell.system.service.SysNoticeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 公告 服务层实现 + * + * @author jsowell + */ +@Service +public class SysNoticeServiceImpl implements SysNoticeService { + @Autowired + private SysNoticeMapper noticeMapper; + + /** + * 查询公告信息 + * + * @param noticeId 公告ID + * @return 公告信息 + */ + @Override + public SysNotice selectNoticeById(Long noticeId) { + return noticeMapper.selectNoticeById(noticeId); + } + + /** + * 查询公告列表 + * + * @param notice 公告信息 + * @return 公告集合 + */ + @Override + public List selectNoticeList(SysNotice notice) { + return noticeMapper.selectNoticeList(notice); + } + + /** + * 新增公告 + * + * @param notice 公告信息 + * @return 结果 + */ + @Override + public int insertNotice(SysNotice notice) { + return noticeMapper.insertNotice(notice); + } + + /** + * 修改公告 + * + * @param notice 公告信息 + * @return 结果 + */ + @Override + public int updateNotice(SysNotice notice) { + return noticeMapper.updateNotice(notice); + } + + /** + * 删除公告对象 + * + * @param noticeId 公告ID + * @return 结果 + */ + @Override + public int deleteNoticeById(Long noticeId) { + return noticeMapper.deleteNoticeById(noticeId); + } + + /** + * 批量删除公告信息 + * + * @param noticeIds 需要删除的公告ID + * @return 结果 + */ + @Override + public int deleteNoticeByIds(Long[] noticeIds) { + return noticeMapper.deleteNoticeByIds(noticeIds); + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysOperLogServiceImpl.java b/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysOperLogServiceImpl.java new file mode 100644 index 000000000..b273276b5 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysOperLogServiceImpl.java @@ -0,0 +1,71 @@ +package com.jsowell.system.service.impl; + +import com.jsowell.system.domain.SysOperLog; +import com.jsowell.system.mapper.SysOperLogMapper; +import com.jsowell.system.service.SysOperLogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 操作日志 服务层处理 + * + * @author jsowell + */ +@Service +public class SysOperLogServiceImpl implements SysOperLogService { + @Autowired + private SysOperLogMapper operLogMapper; + + /** + * 新增操作日志 + * + * @param operLog 操作日志对象 + */ + @Override + public void insertOperlog(SysOperLog operLog) { + operLogMapper.insertOperlog(operLog); + } + + /** + * 查询系统操作日志集合 + * + * @param operLog 操作日志对象 + * @return 操作日志集合 + */ + @Override + public List selectOperLogList(SysOperLog operLog) { + return operLogMapper.selectOperLogList(operLog); + } + + /** + * 批量删除系统操作日志 + * + * @param operIds 需要删除的操作日志ID + * @return 结果 + */ + @Override + public int deleteOperLogByIds(Long[] operIds) { + return operLogMapper.deleteOperLogByIds(operIds); + } + + /** + * 查询操作日志详细 + * + * @param operId 操作ID + * @return 操作日志对象 + */ + @Override + public SysOperLog selectOperLogById(Long operId) { + return operLogMapper.selectOperLogById(operId); + } + + /** + * 清空操作日志 + */ + @Override + public void cleanOperLog() { + operLogMapper.cleanOperLog(); + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysPostServiceImpl.java b/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysPostServiceImpl.java new file mode 100644 index 000000000..1116617a3 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysPostServiceImpl.java @@ -0,0 +1,163 @@ +package com.jsowell.system.service.impl; + +import com.jsowell.common.constant.UserConstants; +import com.jsowell.common.exception.ServiceException; +import com.jsowell.common.util.StringUtils; +import com.jsowell.system.domain.SysPost; +import com.jsowell.system.mapper.SysPostMapper; +import com.jsowell.system.mapper.SysUserPostMapper; +import com.jsowell.system.service.SysPostService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 岗位信息 服务层处理 + * + * @author jsowell + */ +@Service +public class SysPostServiceImpl implements SysPostService { + @Autowired + private SysPostMapper postMapper; + + @Autowired + private SysUserPostMapper userPostMapper; + + /** + * 查询岗位信息集合 + * + * @param post 岗位信息 + * @return 岗位信息集合 + */ + @Override + public List selectPostList(SysPost post) { + return postMapper.selectPostList(post); + } + + /** + * 查询所有岗位 + * + * @return 岗位列表 + */ + @Override + public List selectPostAll() { + return postMapper.selectPostAll(); + } + + /** + * 通过岗位ID查询岗位信息 + * + * @param postId 岗位ID + * @return 角色对象信息 + */ + @Override + public SysPost selectPostById(Long postId) { + return postMapper.selectPostById(postId); + } + + /** + * 根据用户ID获取岗位选择框列表 + * + * @param userId 用户ID + * @return 选中岗位ID列表 + */ + @Override + public List selectPostListByUserId(Long userId) { + return postMapper.selectPostListByUserId(userId); + } + + /** + * 校验岗位名称是否唯一 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public String checkPostNameUnique(SysPost post) { + Long postId = StringUtils.isNull(post.getPostId()) ? -1L : post.getPostId(); + SysPost info = postMapper.checkPostNameUnique(post.getPostName()); + if (StringUtils.isNotNull(info) && info.getPostId().longValue() != postId.longValue()) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验岗位编码是否唯一 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public String checkPostCodeUnique(SysPost post) { + Long postId = StringUtils.isNull(post.getPostId()) ? -1L : post.getPostId(); + SysPost info = postMapper.checkPostCodeUnique(post.getPostCode()); + if (StringUtils.isNotNull(info) && info.getPostId().longValue() != postId.longValue()) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 通过岗位ID查询岗位使用数量 + * + * @param postId 岗位ID + * @return 结果 + */ + @Override + public int countUserPostById(Long postId) { + return userPostMapper.countUserPostById(postId); + } + + /** + * 删除岗位信息 + * + * @param postId 岗位ID + * @return 结果 + */ + @Override + public int deletePostById(Long postId) { + return postMapper.deletePostById(postId); + } + + /** + * 批量删除岗位信息 + * + * @param postIds 需要删除的岗位ID + * @return 结果 + */ + @Override + public int deletePostByIds(Long[] postIds) { + for (Long postId : postIds) { + SysPost post = selectPostById(postId); + if (countUserPostById(postId) > 0) { + throw new ServiceException(String.format("%1$s已分配,不能删除", post.getPostName())); + } + } + return postMapper.deletePostByIds(postIds); + } + + /** + * 新增保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public int insertPost(SysPost post) { + return postMapper.insertPost(post); + } + + /** + * 修改保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public int updatePost(SysPost post) { + return postMapper.updatePost(post); + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysRoleServiceImpl.java b/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysRoleServiceImpl.java new file mode 100644 index 000000000..1e0e34a1a --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysRoleServiceImpl.java @@ -0,0 +1,381 @@ +package com.jsowell.system.service.impl; + +import com.jsowell.common.annotation.DataScope; +import com.jsowell.common.constant.UserConstants; +import com.jsowell.common.core.domain.entity.SysRole; +import com.jsowell.common.core.domain.entity.SysUser; +import com.jsowell.common.exception.ServiceException; +import com.jsowell.common.util.SecurityUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.spring.SpringUtils; +import com.jsowell.system.domain.SysRoleDept; +import com.jsowell.system.domain.SysRoleMenu; +import com.jsowell.system.domain.SysUserRole; +import com.jsowell.system.mapper.SysRoleDeptMapper; +import com.jsowell.system.mapper.SysRoleMapper; +import com.jsowell.system.mapper.SysRoleMenuMapper; +import com.jsowell.system.mapper.SysUserRoleMapper; +import com.jsowell.system.service.SysRoleService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; + +/** + * 角色 业务层处理 + * + * @author jsowell + */ +@Service +public class SysRoleServiceImpl implements SysRoleService { + @Autowired + private SysRoleMapper roleMapper; + + @Autowired + private SysRoleMenuMapper roleMenuMapper; + + @Autowired + private SysUserRoleMapper userRoleMapper; + + @Autowired + private SysRoleDeptMapper roleDeptMapper; + + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + @Override + @DataScope(deptAlias = "d") + public List selectRoleList(SysRole role) { + return roleMapper.selectRoleList(role); + } + + /** + * 根据用户ID查询角色 + * + * @param userId 用户ID + * @return 角色列表 + */ + @Override + public List selectRolesByUserId(Long userId) { + List userRoles = roleMapper.selectRolePermissionByUserId(userId); + List roles = selectRoleAll(); + for (SysRole role : roles) { + for (SysRole userRole : userRoles) { + if (role.getRoleId().longValue() == userRole.getRoleId().longValue()) { + role.setFlag(true); + break; + } + } + } + return roles; + } + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + @Override + public Set selectRolePermissionByUserId(Long userId) { + List perms = roleMapper.selectRolePermissionByUserId(userId); + Set permsSet = new HashSet<>(); + for (SysRole perm : perms) { + if (StringUtils.isNotNull(perm)) { + permsSet.addAll(Arrays.asList(perm.getRoleKey().trim().split(","))); + } + } + return permsSet; + } + + /** + * 查询所有角色 + * + * @return 角色列表 + */ + @Override + public List selectRoleAll() { + return SpringUtils.getAopProxy(this).selectRoleList(new SysRole()); + } + + /** + * 根据用户ID获取角色选择框列表 + * + * @param userId 用户ID + * @return 选中角色ID列表 + */ + @Override + public List selectRoleListByUserId(Long userId) { + return roleMapper.selectRoleListByUserId(userId); + } + + /** + * 通过角色ID查询角色 + * + * @param roleId 角色ID + * @return 角色对象信息 + */ + @Override + public SysRole selectRoleById(Long roleId) { + return roleMapper.selectRoleById(roleId); + } + + /** + * 校验角色名称是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public String checkRoleNameUnique(SysRole role) { + Long roleId = StringUtils.isNull(role.getRoleId()) ? -1L : role.getRoleId(); + SysRole info = roleMapper.checkRoleNameUnique(role.getRoleName()); + if (StringUtils.isNotNull(info) && info.getRoleId().longValue() != roleId.longValue()) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验角色权限是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public String checkRoleKeyUnique(SysRole role) { + Long roleId = StringUtils.isNull(role.getRoleId()) ? -1L : role.getRoleId(); + SysRole info = roleMapper.checkRoleKeyUnique(role.getRoleKey()); + if (StringUtils.isNotNull(info) && info.getRoleId().longValue() != roleId.longValue()) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验角色是否允许操作 + * + * @param role 角色信息 + */ + @Override + public void checkRoleAllowed(SysRole role) { + if (StringUtils.isNotNull(role.getRoleId()) && role.isAdmin()) { + throw new ServiceException("不允许操作超级管理员角色"); + } + } + + /** + * 校验角色是否有数据权限 + * + * @param roleId 角色id + */ + @Override + public void checkRoleDataScope(Long roleId) { + if (!SysUser.isAdmin(SecurityUtils.getUserId())) { + SysRole role = new SysRole(); + role.setRoleId(roleId); + List roles = SpringUtils.getAopProxy(this).selectRoleList(role); + if (StringUtils.isEmpty(roles)) { + throw new ServiceException("没有权限访问角色数据!"); + } + } + } + + /** + * 通过角色ID查询角色使用数量 + * + * @param roleId 角色ID + * @return 结果 + */ + @Override + public int countUserRoleByRoleId(Long roleId) { + return userRoleMapper.countUserRoleByRoleId(roleId); + } + + /** + * 新增保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional + public int insertRole(SysRole role) { + // 新增角色信息 + roleMapper.insertRole(role); + return insertRoleMenu(role); + } + + /** + * 修改保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional + public int updateRole(SysRole role) { + // 修改角色信息 + roleMapper.updateRole(role); + // 删除角色与菜单关联 + roleMenuMapper.deleteRoleMenuByRoleId(role.getRoleId()); + return insertRoleMenu(role); + } + + /** + * 修改角色状态 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public int updateRoleStatus(SysRole role) { + return roleMapper.updateRole(role); + } + + /** + * 修改数据权限信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional + public int authDataScope(SysRole role) { + // 修改角色信息 + roleMapper.updateRole(role); + // 删除角色与部门关联 + roleDeptMapper.deleteRoleDeptByRoleId(role.getRoleId()); + // 新增角色和部门信息(数据权限) + return insertRoleDept(role); + } + + /** + * 新增角色菜单信息 + * + * @param role 角色对象 + */ + public int insertRoleMenu(SysRole role) { + int rows = 1; + // 新增用户与角色管理 + List list = new ArrayList(); + for (Long menuId : role.getMenuIds()) { + SysRoleMenu rm = new SysRoleMenu(); + rm.setRoleId(role.getRoleId()); + rm.setMenuId(menuId); + list.add(rm); + } + if (list.size() > 0) { + rows = roleMenuMapper.batchRoleMenu(list); + } + return rows; + } + + /** + * 新增角色部门信息(数据权限) + * + * @param role 角色对象 + */ + public int insertRoleDept(SysRole role) { + int rows = 1; + // 新增角色与部门(数据权限)管理 + List list = new ArrayList(); + for (Long deptId : role.getDeptIds()) { + SysRoleDept rd = new SysRoleDept(); + rd.setRoleId(role.getRoleId()); + rd.setDeptId(deptId); + list.add(rd); + } + if (list.size() > 0) { + rows = roleDeptMapper.batchRoleDept(list); + } + return rows; + } + + /** + * 通过角色ID删除角色 + * + * @param roleId 角色ID + * @return 结果 + */ + @Override + @Transactional + public int deleteRoleById(Long roleId) { + // 删除角色与菜单关联 + roleMenuMapper.deleteRoleMenuByRoleId(roleId); + // 删除角色与部门关联 + roleDeptMapper.deleteRoleDeptByRoleId(roleId); + return roleMapper.deleteRoleById(roleId); + } + + /** + * 批量删除角色信息 + * + * @param roleIds 需要删除的角色ID + * @return 结果 + */ + @Override + @Transactional + public int deleteRoleByIds(Long[] roleIds) { + for (Long roleId : roleIds) { + checkRoleAllowed(new SysRole(roleId)); + checkRoleDataScope(roleId); + SysRole role = selectRoleById(roleId); + if (countUserRoleByRoleId(roleId) > 0) { + throw new ServiceException(String.format("%1$s已分配,不能删除", role.getRoleName())); + } + } + // 删除角色与菜单关联 + roleMenuMapper.deleteRoleMenu(roleIds); + // 删除角色与部门关联 + roleDeptMapper.deleteRoleDept(roleIds); + return roleMapper.deleteRoleByIds(roleIds); + } + + /** + * 取消授权用户角色 + * + * @param userRole 用户和角色关联信息 + * @return 结果 + */ + @Override + public int deleteAuthUser(SysUserRole userRole) { + return userRoleMapper.deleteUserRoleInfo(userRole); + } + + /** + * 批量取消授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要取消授权的用户数据ID + * @return 结果 + */ + @Override + public int deleteAuthUsers(Long roleId, Long[] userIds) { + return userRoleMapper.deleteUserRoleInfos(roleId, userIds); + } + + /** + * 批量选择授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要授权的用户数据ID + * @return 结果 + */ + @Override + public int insertAuthUsers(Long roleId, Long[] userIds) { + // 新增用户与角色管理 + List list = new ArrayList(); + for (Long userId : userIds) { + SysUserRole ur = new SysUserRole(); + ur.setUserId(userId); + ur.setRoleId(roleId); + list.add(ur); + } + return userRoleMapper.batchUserRole(list); + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysUserOnlineServiceImpl.java b/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysUserOnlineServiceImpl.java new file mode 100644 index 000000000..d55050497 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysUserOnlineServiceImpl.java @@ -0,0 +1,86 @@ +package com.jsowell.system.service.impl; + +import org.springframework.stereotype.Service; +import com.jsowell.common.core.domain.model.LoginUser; +import com.jsowell.common.util.StringUtils; +import com.jsowell.system.domain.SysUserOnline; +import com.jsowell.system.service.SysUserOnlineService; + +/** + * 在线用户 服务层处理 + * + * @author jsowell + */ +@Service +public class SysUserOnlineServiceImpl implements SysUserOnlineService { + /** + * 通过登录地址查询信息 + * + * @param ipaddr 登录地址 + * @param user 用户信息 + * @return 在线用户信息 + */ + @Override + public SysUserOnline selectOnlineByIpaddr(String ipaddr, LoginUser user) { + if (StringUtils.equals(ipaddr, user.getIpaddr())) { + return loginUserToUserOnline(user); + } + return null; + } + + /** + * 通过用户名称查询信息 + * + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + @Override + public SysUserOnline selectOnlineByUserName(String userName, LoginUser user) { + if (StringUtils.equals(userName, user.getUsername())) { + return loginUserToUserOnline(user); + } + return null; + } + + /** + * 通过登录地址/用户名称查询信息 + * + * @param ipaddr 登录地址 + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + @Override + public SysUserOnline selectOnlineByInfo(String ipaddr, String userName, LoginUser user) { + if (StringUtils.equals(ipaddr, user.getIpaddr()) && StringUtils.equals(userName, user.getUsername())) { + return loginUserToUserOnline(user); + } + return null; + } + + /** + * 设置在线用户信息 + * + * @param user 用户信息 + * @return 在线用户 + */ + @Override + public SysUserOnline loginUserToUserOnline(LoginUser user) { + if (StringUtils.isNull(user) || StringUtils.isNull(user.getUser())) { + return null; + } + SysUserOnline sysUserOnline = new SysUserOnline(); + sysUserOnline.setTokenId(user.getToken()); + sysUserOnline.setUserName(user.getUsername()); + sysUserOnline.setIpaddr(user.getIpaddr()); + sysUserOnline.setLoginLocation(user.getLoginLocation()); + sysUserOnline.setBrowser(user.getBrowser()); + sysUserOnline.setOs(user.getOs()); + sysUserOnline.setLoginTime(user.getLoginTime()); + if (StringUtils.isNotNull(user.getUser().getDept())) { + sysUserOnline.setDeptName(user.getUser().getDept().getDeptName()); + } + return sysUserOnline; + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysUserServiceImpl.java b/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysUserServiceImpl.java new file mode 100644 index 000000000..0eb094008 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/impl/SysUserServiceImpl.java @@ -0,0 +1,483 @@ +package com.jsowell.system.service.impl; + +import com.jsowell.common.annotation.DataScope; +import com.jsowell.common.constant.UserConstants; +import com.jsowell.common.core.domain.entity.SysRole; +import com.jsowell.common.core.domain.entity.SysUser; +import com.jsowell.common.exception.ServiceException; +import com.jsowell.common.util.SecurityUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.common.util.bean.BeanValidators; +import com.jsowell.common.util.spring.SpringUtils; +import com.jsowell.system.domain.SysPost; +import com.jsowell.system.domain.SysUserPost; +import com.jsowell.system.domain.SysUserRole; +import com.jsowell.system.mapper.*; +import com.jsowell.system.service.SysConfigService; +import com.jsowell.system.service.SysUserService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import javax.validation.Validator; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 用户 业务层处理 + * + * @author jsowell + */ +@Service +public class SysUserServiceImpl implements SysUserService { + private static final Logger log = LoggerFactory.getLogger(SysUserServiceImpl.class); + + @Autowired + private SysUserMapper userMapper; + + @Autowired + private SysRoleMapper roleMapper; + + @Autowired + private SysPostMapper postMapper; + + @Autowired + private SysUserRoleMapper userRoleMapper; + + @Autowired + private SysUserPostMapper userPostMapper; + + @Autowired + private SysConfigService configService; + + @Autowired + protected Validator validator; + + /** + * 根据条件分页查询用户列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + @Override + @DataScope(deptAlias = "d", userAlias = "u") + public List selectUserList(SysUser user) { + return userMapper.selectUserList(user); + } + + /** + * 根据条件分页查询已分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + @Override + @DataScope(deptAlias = "d", userAlias = "u") + public List selectAllocatedList(SysUser user) { + return userMapper.selectAllocatedList(user); + } + + /** + * 根据条件分页查询未分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + @Override + @DataScope(deptAlias = "d", userAlias = "u") + public List selectUnallocatedList(SysUser user) { + return userMapper.selectUnallocatedList(user); + } + + /** + * 通过用户名查询用户 + * + * @param userName 用户名 + * @return 用户对象信息 + */ + @Override + public SysUser selectUserByUserName(String userName) { + return userMapper.selectUserByUserName(userName); + } + + /** + * 通过用户ID查询用户 + * + * @param userId 用户ID + * @return 用户对象信息 + */ + @Override + public SysUser selectUserById(Long userId) { + return userMapper.selectUserById(userId); + } + + /** + * 查询用户所属角色组 + * + * @param userName 用户名 + * @return 结果 + */ + @Override + public String selectUserRoleGroup(String userName) { + List list = roleMapper.selectRolesByUserName(userName); + if (CollectionUtils.isEmpty(list)) { + return StringUtils.EMPTY; + } + return list.stream().map(SysRole::getRoleName).collect(Collectors.joining(",")); + } + + /** + * 查询用户所属岗位组 + * + * @param userName 用户名 + * @return 结果 + */ + @Override + public String selectUserPostGroup(String userName) { + List list = postMapper.selectPostsByUserName(userName); + if (CollectionUtils.isEmpty(list)) { + return StringUtils.EMPTY; + } + return list.stream().map(SysPost::getPostName).collect(Collectors.joining(",")); + } + + /** + * 校验用户名称是否唯一 + * + * @param userName 用户名称 + * @return 结果 + */ + @Override + public String checkUserNameUnique(String userName) { + int count = userMapper.checkUserNameUnique(userName); + if (count > 0) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验手机号码是否唯一 + * + * @param user 用户信息 + * @return + */ + @Override + public String checkPhoneUnique(SysUser user) { + Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId(); + SysUser info = userMapper.checkPhoneUnique(user.getPhone()); + if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验email是否唯一 + * + * @param user 用户信息 + * @return + */ + @Override + public String checkEmailUnique(SysUser user) { + Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId(); + SysUser info = userMapper.checkEmailUnique(user.getEmail()); + if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验用户是否允许操作 + * + * @param user 用户信息 + */ + @Override + public void checkUserAllowed(SysUser user) { + if (StringUtils.isNotNull(user.getUserId()) && user.isAdmin()) { + // throw new ServiceException("不允许操作超级管理员用户"); + } + } + + /** + * 校验用户是否有数据权限 + * + * @param userId 用户id + */ + @Override + public void checkUserDataScope(Long userId) { + if (!SysUser.isAdmin(SecurityUtils.getUserId())) { + SysUser user = new SysUser(); + user.setUserId(userId); + List users = SpringUtils.getAopProxy(this).selectUserList(user); + if (StringUtils.isEmpty(users)) { + throw new ServiceException("没有权限访问用户数据!"); + } + } + } + + /** + * 新增保存用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + @Transactional + public int insertUser(SysUser user) { + // 新增用户信息 + int rows = userMapper.insertUser(user); + // 新增用户岗位关联 + insertUserPost(user); + // 新增用户与角色管理 + insertUserRole(user); + return rows; + } + + /** + * 注册用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public boolean registerUser(SysUser user) { + return userMapper.insertUser(user) > 0; + } + + /** + * 修改保存用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + @Transactional + public int updateUser(SysUser user) { + Long userId = user.getUserId(); + // 删除用户与角色关联 + userRoleMapper.deleteUserRoleByUserId(userId); + // 新增用户与角色管理 + insertUserRole(user); + // 删除用户与岗位关联 + userPostMapper.deleteUserPostByUserId(userId); + // 新增用户与岗位管理 + insertUserPost(user); + return userMapper.updateUser(user); + } + + /** + * 用户授权角色 + * + * @param userId 用户ID + * @param roleIds 角色组 + */ + @Override + @Transactional + public void insertUserAuth(Long userId, Long[] roleIds) { + userRoleMapper.deleteUserRoleByUserId(userId); + insertUserRole(userId, roleIds); + } + + /** + * 修改用户状态 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public int updateUserStatus(SysUser user) { + return userMapper.updateUser(user); + } + + /** + * 修改用户基本信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public int updateUserProfile(SysUser user) { + return userMapper.updateUser(user); + } + + /** + * 修改用户头像 + * + * @param userName 用户名 + * @param avatar 头像地址 + * @return 结果 + */ + @Override + public boolean updateUserAvatar(String userName, String avatar) { + return userMapper.updateUserAvatar(userName, avatar) > 0; + } + + /** + * 重置用户密码 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public int resetPwd(SysUser user) { + return userMapper.updateUser(user); + } + + /** + * 重置用户密码 + * + * @param userName 用户名 + * @param password 密码 + * @return 结果 + */ + @Override + public int resetUserPwd(String userName, String password) { + return userMapper.resetUserPwd(userName, password); + } + + /** + * 新增用户角色信息 + * + * @param user 用户对象 + */ + public void insertUserRole(SysUser user) { + this.insertUserRole(user.getUserId(), user.getRoleIds()); + } + + /** + * 新增用户岗位信息 + * + * @param user 用户对象 + */ + public void insertUserPost(SysUser user) { + Long[] posts = user.getPostIds(); + if (StringUtils.isNotEmpty(posts)) { + // 新增用户与岗位管理 + List list = new ArrayList(posts.length); + for (Long postId : posts) { + SysUserPost up = new SysUserPost(); + up.setUserId(user.getUserId()); + up.setPostId(postId); + list.add(up); + } + userPostMapper.batchUserPost(list); + } + } + + /** + * 新增用户角色信息 + * + * @param userId 用户ID + * @param roleIds 角色组 + */ + public void insertUserRole(Long userId, Long[] roleIds) { + if (StringUtils.isNotEmpty(roleIds)) { + // 新增用户与角色管理 + List list = new ArrayList(roleIds.length); + for (Long roleId : roleIds) { + SysUserRole ur = new SysUserRole(); + ur.setUserId(userId); + ur.setRoleId(roleId); + list.add(ur); + } + userRoleMapper.batchUserRole(list); + } + } + + /** + * 通过用户ID删除用户 + * + * @param userId 用户ID + * @return 结果 + */ + @Override + @Transactional + public int deleteUserById(Long userId) { + // 删除用户与角色关联 + userRoleMapper.deleteUserRoleByUserId(userId); + // 删除用户与岗位表 + userPostMapper.deleteUserPostByUserId(userId); + return userMapper.deleteUserById(userId); + } + + /** + * 批量删除用户信息 + * + * @param userIds 需要删除的用户ID + * @return 结果 + */ + @Override + @Transactional + public int deleteUserByIds(Long[] userIds) { + for (Long userId : userIds) { + checkUserAllowed(new SysUser(userId)); + checkUserDataScope(userId); + } + // 删除用户与角色关联 + userRoleMapper.deleteUserRole(userIds); + // 删除用户与岗位关联 + userPostMapper.deleteUserPost(userIds); + return userMapper.deleteUserByIds(userIds); + } + + /** + * 导入用户数据 + * + * @param userList 用户数据列表 + * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据 + * @param operName 操作用户 + * @return 结果 + */ + @Override + public String importUser(List userList, Boolean isUpdateSupport, String operName) { + if (StringUtils.isNull(userList) || userList.size() == 0) { + throw new ServiceException("导入用户数据不能为空!"); + } + int successNum = 0; + int failureNum = 0; + StringBuilder successMsg = new StringBuilder(); + StringBuilder failureMsg = new StringBuilder(); + String password = configService.selectConfigByKey("sys.user.initPassword"); + for (SysUser user : userList) { + try { + // 验证是否存在这个用户 + SysUser u = userMapper.selectUserByUserName(user.getUserName()); + if (StringUtils.isNull(u)) { + BeanValidators.validateWithException(validator, user); + user.setPassword(SecurityUtils.encryptPassword(password)); + user.setCreateBy(operName); + this.insertUser(user); + successNum++; + successMsg.append("
" + successNum + "、账号 " + user.getUserName() + " 导入成功"); + } else if (isUpdateSupport) { + BeanValidators.validateWithException(validator, user); + user.setUpdateBy(operName); + this.updateUser(user); + successNum++; + successMsg.append("
" + successNum + "、账号 " + user.getUserName() + " 更新成功"); + } else { + failureNum++; + failureMsg.append("
" + failureNum + "、账号 " + user.getUserName() + " 已存在"); + } + } catch (Exception e) { + failureNum++; + String msg = "
" + failureNum + "、账号 " + user.getUserName() + " 导入失败:"; + failureMsg.append(msg + e.getMessage()); + log.error(msg, e); + } + } + if (failureNum > 0) { + failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:"); + throw new ServiceException(failureMsg.toString()); + } else { + successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:"); + } + return successMsg.toString(); + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/vo/MetaVO.java b/jsowell-system/src/main/java/com/jsowell/system/vo/MetaVO.java new file mode 100644 index 000000000..6e0b6f9d4 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/vo/MetaVO.java @@ -0,0 +1,91 @@ +package com.jsowell.system.vo; + +import com.jsowell.common.util.StringUtils; + +/** + * 路由显示信息 + * + * @author jsowell + */ +public class MetaVO { + /** + * 设置该路由在侧边栏和面包屑中展示的名字 + */ + private String title; + + /** + * 设置该路由的图标,对应路径src/assets/icons/svg + */ + private String icon; + + /** + * 设置为true,则不会被 缓存 + */ + private boolean noCache; + + /** + * 内链地址(http(s)://开头) + */ + private String link; + + public MetaVO() { + } + + public MetaVO(String title, String icon) { + this.title = title; + this.icon = icon; + } + + public MetaVO(String title, String icon, boolean noCache) { + this.title = title; + this.icon = icon; + this.noCache = noCache; + } + + public MetaVO(String title, String icon, String link) { + this.title = title; + this.icon = icon; + this.link = link; + } + + public MetaVO(String title, String icon, boolean noCache, String link) { + this.title = title; + this.icon = icon; + this.noCache = noCache; + if (StringUtils.isHttp(link)) { + this.link = link; + } + } + + public boolean isNoCache() { + return noCache; + } + + public void setNoCache(boolean noCache) { + this.noCache = noCache; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + + public String getLink() { + return link; + } + + public void setLink(String link) { + this.link = link; + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/vo/RouterVO.java b/jsowell-system/src/main/java/com/jsowell/system/vo/RouterVO.java new file mode 100644 index 000000000..998310bf9 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/vo/RouterVO.java @@ -0,0 +1,130 @@ +package com.jsowell.system.vo; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.List; + +/** + * 路由配置信息 + * + * @author jsowell + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class RouterVO { + /** + * 路由名字 + */ + private String name; + + /** + * 路由地址 + */ + private String path; + + /** + * 是否隐藏路由,当设置 true 的时候该路由不会再侧边栏出现 + */ + private boolean hidden; + + /** + * 重定向地址,当设置 noRedirect 的时候该路由在面包屑导航中不可被点击 + */ + private String redirect; + + /** + * 组件地址 + */ + private String component; + + /** + * 路由参数:如 {"id": 1, "name": "ry"} + */ + private String query; + + /** + * 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面 + */ + private Boolean alwaysShow; + + /** + * 其他元素 + */ + private MetaVO meta; + + /** + * 子路由 + */ + private List children; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public boolean getHidden() { + return hidden; + } + + public void setHidden(boolean hidden) { + this.hidden = hidden; + } + + public String getRedirect() { + return redirect; + } + + public void setRedirect(String redirect) { + this.redirect = redirect; + } + + public String getComponent() { + return component; + } + + public void setComponent(String component) { + this.component = component; + } + + public String getQuery() { + return query; + } + + public void setQuery(String query) { + this.query = query; + } + + public Boolean getAlwaysShow() { + return alwaysShow; + } + + public void setAlwaysShow(Boolean alwaysShow) { + this.alwaysShow = alwaysShow; + } + + public MetaVO getMeta() { + return meta; + } + + public void setMeta(MetaVO meta) { + this.meta = meta; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } +} diff --git a/jsowell-system/src/main/resources/mapper/system/SysConfigMapper.xml b/jsowell-system/src/main/resources/mapper/system/SysConfigMapper.xml new file mode 100644 index 000000000..b8497000c --- /dev/null +++ b/jsowell-system/src/main/resources/mapper/system/SysConfigMapper.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + select config_id, config_name, config_key, config_value, config_type, create_by, create_time, update_by, update_time, remark + from sys_config + + + + + + + and config_id = #{configId} + + + and config_key = #{configKey} + + + + + + + + + + + + insert into sys_config ( + config_name, + config_key, + config_value, + config_type, + create_by, + remark, + create_time + )values( + #{configName}, + #{configKey}, + #{configValue}, + #{configType}, + #{createBy}, + #{remark}, + sysdate() + ) + + + + update sys_config + + config_name = #{configName}, + config_key = #{configKey}, + config_value = #{configValue}, + config_type = #{configType}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where config_id = #{configId} + + + + delete from sys_config where config_id = #{configId} + + + + delete from sys_config where config_id in + + #{configId} + + + + \ No newline at end of file diff --git a/jsowell-system/src/main/resources/mapper/system/SysDeptMapper.xml b/jsowell-system/src/main/resources/mapper/system/SysDeptMapper.xml new file mode 100644 index 000000000..34cf61028 --- /dev/null +++ b/jsowell-system/src/main/resources/mapper/system/SysDeptMapper.xml @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.del_flag, d.create_by, d.create_time + from sys_dept d + + + + + + + + + + + + + + + + + + + + insert into sys_dept( + + dept_id, + parent_id, + dept_name, + ancestors, + order_num, + leader, + phone, + email, + status, + create_by, + + )values( + + #{deptId}, + #{parentId}, + #{deptName}, + #{ancestors}, + #{orderNum}, + #{leader}, + #{phone}, + #{email}, + #{status}, + #{createBy}, + + ) + + + + update sys_dept + + parent_id = #{parentId}, + dept_name = #{deptName}, + ancestors = #{ancestors}, + order_num = #{orderNum}, + leader = #{leader}, + phone = #{phone}, + email = #{email}, + status = #{status}, + update_by = #{updateBy}, + + where dept_id = #{deptId} + + + + update sys_dept set ancestors = + + when #{item.deptId} then #{item.ancestors} + + where dept_id in + + #{item.deptId} + + + + + update sys_dept set status = '0' where dept_id in + + #{deptId} + + + + + update sys_dept set del_flag = '2' where dept_id = #{deptId} + + + \ No newline at end of file diff --git a/jsowell-system/src/main/resources/mapper/system/SysDictDataMapper.xml b/jsowell-system/src/main/resources/mapper/system/SysDictDataMapper.xml new file mode 100644 index 000000000..b96ca9f24 --- /dev/null +++ b/jsowell-system/src/main/resources/mapper/system/SysDictDataMapper.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + select dict_code, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark + from sys_dict_data + + + + + + + + + + + + + + delete from sys_dict_data where dict_code = #{dictCode} + + + + delete from sys_dict_data where dict_code in + + #{dictCode} + + + + + update sys_dict_data + + dict_sort = #{dictSort}, + dict_label = #{dictLabel}, + dict_value = #{dictValue}, + dict_type = #{dictType}, + css_class = #{cssClass}, + list_class = #{listClass}, + is_default = #{isDefault}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where dict_code = #{dictCode} + + + + update sys_dict_data set dict_type = #{newDictType} where dict_type = #{oldDictType} + + + + insert into sys_dict_data( + dict_sort, + dict_label, + dict_value, + dict_type, + css_class, + list_class, + is_default, + status, + remark, + create_by, + create_time + )values( + #{dictSort}, + #{dictLabel}, + #{dictValue}, + #{dictType}, + #{cssClass}, + #{listClass}, + #{isDefault}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + \ No newline at end of file diff --git a/jsowell-system/src/main/resources/mapper/system/SysDictTypeMapper.xml b/jsowell-system/src/main/resources/mapper/system/SysDictTypeMapper.xml new file mode 100644 index 000000000..4992a4d0d --- /dev/null +++ b/jsowell-system/src/main/resources/mapper/system/SysDictTypeMapper.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + select dict_id, dict_name, dict_type, status, create_by, create_time, remark + from sys_dict_type + + + + + + + + + + + + + + delete from sys_dict_type where dict_id = #{dictId} + + + + delete from sys_dict_type where dict_id in + + #{dictId} + + + + + update sys_dict_type + + dict_name = #{dictName}, + dict_type = #{dictType}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where dict_id = #{dictId} + + + + insert into sys_dict_type( + dict_name, + dict_type, + status, + remark, + create_by, + create_time + )values( + #{dictName}, + #{dictType}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + \ No newline at end of file diff --git a/jsowell-system/src/main/resources/mapper/system/SysLogininforMapper.xml b/jsowell-system/src/main/resources/mapper/system/SysLogininforMapper.xml new file mode 100644 index 000000000..26014b6c0 --- /dev/null +++ b/jsowell-system/src/main/resources/mapper/system/SysLogininforMapper.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + insert into sys_logininfor (user_name, status, ipaddr, login_location, browser, os, msg, login_time) + values (#{userName}, #{status}, #{ipaddr}, #{loginLocation}, #{browser}, #{os}, #{msg}, sysdate()) + + + + + + delete from sys_logininfor where info_id in + + #{infoId} + + + + + truncate table sys_logininfor + + + \ No newline at end of file diff --git a/jsowell-system/src/main/resources/mapper/system/SysMenuMapper.xml b/jsowell-system/src/main/resources/mapper/system/SysMenuMapper.xml new file mode 100644 index 000000000..5f349b452 --- /dev/null +++ b/jsowell-system/src/main/resources/mapper/system/SysMenuMapper.xml @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select menu_id, menu_name, parent_id, order_num, path, component, `query`, is_frame, is_cache, menu_type, visible, status, ifnull(perms,'') as perms, icon, create_time + from sys_menu + + + + + + + + + + + + + + + + + + + + + + + + update sys_menu + + menu_name = #{menuName}, + parent_id = #{parentId}, + order_num = #{orderNum}, + path = #{path}, + component = #{component}, + `query` = #{query}, + is_frame = #{isFrame}, + is_cache = #{isCache}, + menu_type = #{menuType}, + visible = #{visible}, + status = #{status}, + perms = #{perms}, + icon = #{icon}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where menu_id = #{menuId} + + + + insert into sys_menu( + menu_id, + parent_id, + menu_name, + order_num, + path, + component, + `query`, + is_frame, + is_cache, + menu_type, + visible, + status, + perms, + icon, + remark, + create_by, + create_time + )values( + #{menuId}, + #{parentId}, + #{menuName}, + #{orderNum}, + #{path}, + #{component}, + #{query}, + #{isFrame}, + #{isCache}, + #{menuType}, + #{visible}, + #{status}, + #{perms}, + #{icon}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + delete from sys_menu where menu_id = #{menuId} + + + \ No newline at end of file diff --git a/jsowell-system/src/main/resources/mapper/system/SysNoticeMapper.xml b/jsowell-system/src/main/resources/mapper/system/SysNoticeMapper.xml new file mode 100644 index 000000000..ee1d2251b --- /dev/null +++ b/jsowell-system/src/main/resources/mapper/system/SysNoticeMapper.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + select notice_id, notice_title, notice_type, cast(notice_content as char) as notice_content, status, create_by, create_time, update_by, update_time, remark + from sys_notice + + + + + + + + insert into sys_notice ( + notice_title, + notice_type, + notice_content, + status, + remark, + create_by, + create_time + )values( + #{noticeTitle}, + #{noticeType}, + #{noticeContent}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + update sys_notice + + notice_title = #{noticeTitle}, + notice_type = #{noticeType}, + notice_content = #{noticeContent}, + status = #{status}, + update_by = #{updateBy}, + update_time = sysdate() + + where notice_id = #{noticeId} + + + + delete from sys_notice where notice_id = #{noticeId} + + + + delete from sys_notice where notice_id in + + #{noticeId} + + + + \ No newline at end of file diff --git a/jsowell-system/src/main/resources/mapper/system/SysOperLogMapper.xml b/jsowell-system/src/main/resources/mapper/system/SysOperLogMapper.xml new file mode 100644 index 000000000..e2b4c6635 --- /dev/null +++ b/jsowell-system/src/main/resources/mapper/system/SysOperLogMapper.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + select oper_id, title, business_type, method, request_method, operator_type, oper_name, dept_name, oper_url, oper_ip, oper_location, oper_param, json_result, status, error_msg, oper_time + from sys_oper_log + + + + insert into sys_oper_log(title, business_type, method, request_method, operator_type, oper_name, dept_name, oper_url, oper_ip, oper_location, oper_param, json_result, status, error_msg, oper_time) + values (#{title}, #{businessType}, #{method}, #{requestMethod}, #{operatorType}, #{operName}, #{deptName}, #{operUrl}, #{operIp}, #{operLocation}, #{operParam}, #{jsonResult}, #{status}, #{errorMsg}, sysdate()) + + + + + + delete from sys_oper_log where oper_id in + + #{operId} + + + + + + + truncate table sys_oper_log + + + \ No newline at end of file diff --git a/jsowell-system/src/main/resources/mapper/system/SysPostMapper.xml b/jsowell-system/src/main/resources/mapper/system/SysPostMapper.xml new file mode 100644 index 000000000..c847fb9e8 --- /dev/null +++ b/jsowell-system/src/main/resources/mapper/system/SysPostMapper.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + select post_id, post_code, post_name, post_sort, status, create_by, create_time, remark + from sys_post + + + + + + + + + + + + + + + + + + update sys_post + + post_code = #{postCode}, + post_name = #{postName}, + post_sort = #{postSort}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where post_id = #{postId} + + + + insert into sys_post( + post_id, + post_code, + post_name, + post_sort, + status, + remark, + create_by, + create_time + )values( + #{postId}, + #{postCode}, + #{postName}, + #{postSort}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + delete from sys_post where post_id = #{postId} + + + + delete from sys_post where post_id in + + #{postId} + + + + \ No newline at end of file diff --git a/jsowell-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml b/jsowell-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml new file mode 100644 index 000000000..9a1baa729 --- /dev/null +++ b/jsowell-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + delete from sys_role_dept where role_id=#{roleId} + + + + + + delete from sys_role_dept where role_id in + + #{roleId} + + + + + insert into sys_role_dept(role_id, dept_id) values + + (#{item.roleId},#{item.deptId}) + + + + \ No newline at end of file diff --git a/jsowell-system/src/main/resources/mapper/system/SysRoleMapper.xml b/jsowell-system/src/main/resources/mapper/system/SysRoleMapper.xml new file mode 100644 index 000000000..ffeec6825 --- /dev/null +++ b/jsowell-system/src/main/resources/mapper/system/SysRoleMapper.xml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + select distinct r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.menu_check_strictly, r.dept_check_strictly, + r.status, r.del_flag, r.create_time, r.remark + from sys_role r + left join sys_user_role ur on ur.role_id = r.role_id + left join sys_user u on u.user_id = ur.user_id + left join sys_dept d on u.dept_id = d.dept_id + + + + + + + + + + + + + + + + + + + + insert into sys_role( + role_id, + role_name, + role_key, + role_sort, + data_scope, + menu_check_strictly, + dept_check_strictly, + status, + remark, + create_by, + create_time + )values( + #{roleId}, + #{roleName}, + #{roleKey}, + #{roleSort}, + #{dataScope}, + #{menuCheckStrictly}, + #{deptCheckStrictly}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + update sys_role + + role_name = #{roleName}, + role_key = #{roleKey}, + role_sort = #{roleSort}, + data_scope = #{dataScope}, + menu_check_strictly = #{menuCheckStrictly}, + dept_check_strictly = #{deptCheckStrictly}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where role_id = #{roleId} + + + + update sys_role set del_flag = '2' where role_id = #{roleId} + + + + update sys_role set del_flag = '2' where role_id in + + #{roleId} + + + + \ No newline at end of file diff --git a/jsowell-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml b/jsowell-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml new file mode 100644 index 000000000..e6edab66f --- /dev/null +++ b/jsowell-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + delete from sys_role_menu where role_id=#{roleId} + + + + delete from sys_role_menu where role_id in + + #{roleId} + + + + + insert into sys_role_menu(role_id, menu_id) values + + (#{item.roleId},#{item.menuId}) + + + + \ No newline at end of file diff --git a/jsowell-system/src/main/resources/mapper/system/SysUserMapper.xml b/jsowell-system/src/main/resources/mapper/system/SysUserMapper.xml new file mode 100644 index 000000000..c0d443f41 --- /dev/null +++ b/jsowell-system/src/main/resources/mapper/system/SysUserMapper.xml @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select + u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phone, u.password, u.sex, u.status, + u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, + d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.status as dept_status, + r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status + from sys_user u + left join sys_dept d on u.dept_id = d.dept_id + left join sys_user_role ur on u.user_id = ur.user_id + left join sys_role r on r.role_id = ur.role_id + + + + + + + + + + + + + + + + + + + + insert into sys_user( + user_id, + dept_id, + user_name, + nick_name, + email, + avatar, + phone, + sex, + password, + status, + create_by, + remark, + create_time + )values( + #{userId}, + #{deptId}, + #{userName}, + #{nickName}, + #{email}, + #{avatar}, + #{phone}, + #{sex}, + #{password}, + #{status}, + #{createBy}, + #{remark}, + sysdate() + ) + + + + update sys_user + + dept_id = #{deptId}, + user_name = #{userName}, + nick_name = #{nickName}, + email = #{email}, + phone = #{phone}, + sex = #{sex}, + avatar = #{avatar}, + password = #{password}, + status = #{status}, + login_ip = #{loginIp}, + login_date = #{loginDate}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where user_id = #{userId} + + + + update sys_user set status = #{status} where user_id = #{userId} + + + + update sys_user set avatar = #{avatar} where user_name = #{userName} + + + + update sys_user set password = #{password} where user_name = #{userName} + + + + update sys_user set del_flag = '2' where user_id = #{userId} + + + + update sys_user set del_flag = '2' where user_id in + + #{userId} + + + + \ No newline at end of file diff --git a/jsowell-system/src/main/resources/mapper/system/SysUserPostMapper.xml b/jsowell-system/src/main/resources/mapper/system/SysUserPostMapper.xml new file mode 100644 index 000000000..957980e51 --- /dev/null +++ b/jsowell-system/src/main/resources/mapper/system/SysUserPostMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + delete from sys_user_post where user_id=#{userId} + + + + + + delete from sys_user_post where user_id in + + #{userId} + + + + + insert into sys_user_post(user_id, post_id) values + + (#{item.userId},#{item.postId}) + + + + \ No newline at end of file diff --git a/jsowell-system/src/main/resources/mapper/system/SysUserRoleMapper.xml b/jsowell-system/src/main/resources/mapper/system/SysUserRoleMapper.xml new file mode 100644 index 000000000..b82560873 --- /dev/null +++ b/jsowell-system/src/main/resources/mapper/system/SysUserRoleMapper.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + delete from sys_user_role where user_id=#{userId} + + + + + + delete from sys_user_role where user_id in + + #{userId} + + + + + insert into sys_user_role(user_id, role_id) values + + (#{item.userId},#{item.roleId}) + + + + + delete from sys_user_role where user_id=#{userId} and role_id=#{roleId} + + + + delete from sys_user_role where role_id=#{roleId} and user_id in + + #{userId} + + + \ No newline at end of file diff --git a/jsowell-thirdparty/pom.xml b/jsowell-thirdparty/pom.xml new file mode 100644 index 000000000..ff891a05d --- /dev/null +++ b/jsowell-thirdparty/pom.xml @@ -0,0 +1,51 @@ + + + + jsowell-charger-web + com.jsowell + 1.0.0 + + 4.0.0 + + jsowell-thirdparty + + + + com.jsowell + jsowell-pile + 1.0.0 + + + + org.projectlombok + lombok + + + + + + 8 + 8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.8 + 1.8 + + /src/test/** + + utf-8 + + + + + + \ No newline at end of file diff --git a/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/service/LianLianService.java b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/service/LianLianService.java new file mode 100644 index 000000000..c297691f7 --- /dev/null +++ b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/service/LianLianService.java @@ -0,0 +1,10 @@ +package com.jsowell.thirdparty.service; + +public interface LianLianService { + + /** + * 根据运营商id,推送运营商信息 + * @param merchantId + */ + void pushMerchantInfo(Long merchantId); +} diff --git a/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/service/impl/LianLianServiceImpl.java b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/service/impl/LianLianServiceImpl.java new file mode 100644 index 000000000..464f8f7f7 --- /dev/null +++ b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/service/impl/LianLianServiceImpl.java @@ -0,0 +1,25 @@ +package com.jsowell.thirdparty.service.impl; + +import com.jsowell.pile.service.IPileMerchantInfoService; +import com.jsowell.thirdparty.service.LianLianService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class LianLianServiceImpl implements LianLianService { + + @Autowired + private IPileMerchantInfoService pileMerchantInfoService; + + + @Override + public void pushMerchantInfo(Long merchantId) { + // 通过id查询运营商信息 + + // 组装联联平台所需要的数据格式 + + // 调用联联平台接口 + + + } +} diff --git a/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/ChargeDetail.java b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/ChargeDetail.java new file mode 100644 index 000000000..5a799d8fc --- /dev/null +++ b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/ChargeDetail.java @@ -0,0 +1,54 @@ +package com.jsowell.thirdparty.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +/** + * 充电明细信息体 + */ +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ChargeDetail { + /** + * 开始时间 Y + * 格式“yyyy-MM-dd HH:mm:ss” + */ + private String DetailStartTime; + + /** + * 结束时间 Y + * 格式“yyyy-MM-dd HH:mm:ss” + */ + private String DetailEndTime; + + /** + * 时段电价(小数点后4位) Y + */ + private BigDecimal ElecPrice; + + /** + * 时段服务费价格(小数点后4位) Y + */ + private BigDecimal SevicePrice; + + /** + * 时段充电量(单位:度,小数点后2位) Y + */ + private BigDecimal DetailPower; + + /** + * 时段电费(小数点后2位) Y + */ + private BigDecimal DetailElecMoney; + + /** + * 时段服务费(小数点后2位) Y + */ + private BigDecimal DetailSeviceMoney; +} diff --git a/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/ConnectorChargeStatusInfo.java b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/ConnectorChargeStatusInfo.java new file mode 100644 index 000000000..a55c509ba --- /dev/null +++ b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/ConnectorChargeStatusInfo.java @@ -0,0 +1,122 @@ +package com.jsowell.thirdparty.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +/** + * 充电设备接口充电中状态信息 + */ +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ConnectorChargeStatusInfo { + /** + * 充电订单号 Y + * 对接平台系统订单编号 + */ + private String StartChargeSeq; + + /** + * 充电设备接口编码 Y + * 平台下唯一枪口号 + */ + private String ConnectorID; + + /** + * 充电设备接口状态 Y + * 1:空闲 + * 2:占用(未充电) + * 3:占用(充电中) + * 4:占用(预约锁定) + * 255:故障 + */ + private Integer ConnectorStatus; + + /** + * 车辆识别码 N + * 车辆识别号码或车架号码,由17位英数组成 + */ + private String Vin; + + /** + * A相电流 Y + * 单位:A,默认:0 含直流(输出) + */ + private BigDecimal CurrentA; + + /** + * B相电流 N + * 单位:A,默认:0 + */ + private BigDecimal CurrentB; + + /** + * C相电流 N + * 单位:A,默认:0 + */ + private BigDecimal CurrentC; + + /** + * A相电压 Y + * 单位:V,默认:0含直流(输出) + */ + private BigDecimal VoltageA; + + /** + * B相电压 N + * 单位:V,默认:0 + */ + private BigDecimal VoltageB; + + /** + * C相电压 N + * 单位:V,默认:0 + */ + private BigDecimal VoltageC; + + /** + * 电池剩余电量(默认:0) Y + */ + private BigDecimal Soc; + + /** + * 开始充电时间 Y + * 格式“yyyy-MM-dd HH:mm:ss” + */ + private String StartTime; + + /** + * 本次采样时间 Y + * 格式“yyyy-MM-dd HH:mm:ss” + */ + private String EndTime; + + /** + * 累计充电量 Y + * 单位:度,小数点后2位 + */ + private BigDecimal TotalPower; + + /** + * 累计电费 Y + * 单位:元,小数点后2位 + */ + private BigDecimal ElecMoney; + + /** + * 累计服务费 Y + * 单位:元,小数点后2位 + */ + private BigDecimal SeviceMoney; + + /** + * 累计总金额 Y + * 单位:元,小数点后2位 + */ + private BigDecimal TotalMoney; +} diff --git a/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/ConnectorInfo.java b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/ConnectorInfo.java new file mode 100644 index 000000000..8a468b627 --- /dev/null +++ b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/ConnectorInfo.java @@ -0,0 +1,64 @@ +package com.jsowell.thirdparty.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +/** + * 充电设备接口信息 + */ +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ConnectorInfo { + /** + * 充电设备接口编码 Y + * 充电设备接口编码,同一对接平台内唯一 + */ + private String ConnectorID; + + /** + * 充电设备接口名称 N + */ + private String ConnectorName; + + /** + * 充电设备接口类型 Y + * 1:家用插座(模式2) + * 2:交流接口插座(模式3,连接方式B ) + * 3:交流接口插头(带枪线,模式3,连接方式C) + * 4:直流接口枪头(带枪线,模式4) + */ + private Integer ConnectorType; + + /** + * 额定电压上限(单位:V) Y + */ + private Integer VoltageUpperLimits; + + /** + * 额定电压下限(单位:V) Y + */ + private Integer VoltageLowerLimits; + + /** + * 额定电流(单位:A) Y + */ + private Integer Current; + + /** + * 额定功率(单位:kW) Y + */ + private BigDecimal Power; + + /** + * 车位号 N + * 停车场车位编号 + */ + private String ParkNo; + +} diff --git a/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/ConnectorStatsInfo.java b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/ConnectorStatsInfo.java new file mode 100644 index 000000000..e2a6efb60 --- /dev/null +++ b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/ConnectorStatsInfo.java @@ -0,0 +1,29 @@ +package com.jsowell.thirdparty.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +/** + * 充电设备接口统计信息 + */ +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ConnectorStatsInfo { + /** + * 充电设备接口编码 Y + * 充电设备接口编码,同一对接平台内唯一 + */ + private String ConnectorID; + + /** + * 充电设备接口累计电量 + * 累计电量,单位kWh,精度0.1 + */ + private BigDecimal ConnectorElectricity; +} diff --git a/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/ConnectorStatusInfo.java b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/ConnectorStatusInfo.java new file mode 100644 index 000000000..88aae08c2 --- /dev/null +++ b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/ConnectorStatusInfo.java @@ -0,0 +1,42 @@ +package com.jsowell.thirdparty.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 充电设备接口状态信息 + */ +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class ConnectorStatusInfo { + /** + * 充电设备接口编码 Y + * 充电设备接口编码,同一对接平台内唯一 + */ + private String ConnectorID; + + /** + * 充电设备接口状态 Y + * 0:离网 + * 1:空闲 + * 2:占用(未充电) + * 3:占用(充电中) + * 4:占用(预约锁定) + * 255:故障 + */ + private String Status; + + /** + * 车位状态(0-未知;10-空闲;50-占用) N + */ + private String ParkStatus; + + /** + * 地锁状态(0-未知;10-已解锁;50-占用) N + */ + private String LockStatus; +} diff --git a/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/EquipmentInfo.java b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/EquipmentInfo.java new file mode 100644 index 000000000..e4f7becf7 --- /dev/null +++ b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/EquipmentInfo.java @@ -0,0 +1,102 @@ +package com.jsowell.thirdparty.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 充电设备信息 + */ +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class EquipmentInfo { + /** + * 设备编码 Y + * 设备唯一编码,对同一对接平台,保证唯一 + */ + private String EquipmentID; + + /** + * 设备生产商组织机构代码 Y + */ + private String ManufacturerID; + + /** + * 设备型号 N + * 由设备生厂商定义的设备型号 + */ + private String EquipmentModel; + + /** + * 设备名称 N + */ + private String EquipmentName; + + /** + * 设备生产日期 N + * YYYY-MM-DD + */ + private String ProductionDate; + + /** + * 建设时间 Y + * YYYY-MM-DD + */ + private String ConstructionTime; + + /** + * 设备类型(1-直流设备;2-交流设备;3-交直流一体设备) Y + */ + private Integer EquipmentType; + + /** + * 设备状态 Y + * 0:未知 + * 1:建设中 + * 5:关闭下线 + * 6:维护中 + * 50:正常使用 + */ + private Integer EquipmentStatus; + + /** + * 额定功率(单位:kW) Y + */ + private BigDecimal EquipmentPower; + + /** + * 新国标(0-否;1-是) Y + * 是否新国标 + */ + private Integer NewNationalStandard; + + /** + * 充电设备接口列表 Y + * 该充电设备所有的充电设备接口的信息对象集合 + */ + private List ConnectorInfos; + + /** + * 充电设备经度 N + * GCJ-02坐标系 + */ + private BigDecimal EquipmentLng; + + /** + * 充电设备纬度 N + * GCJ-02坐标系 + */ + private BigDecimal EquipmentLat; + + /** + * 是否支持VIN码识别(0-否;1-是) Y + */ + private Integer VinFlag; + +} diff --git a/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/EquipmentStatsInfo.java b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/EquipmentStatsInfo.java new file mode 100644 index 000000000..5a225e844 --- /dev/null +++ b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/EquipmentStatsInfo.java @@ -0,0 +1,36 @@ +package com.jsowell.thirdparty.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 充电设备统计信息 + */ +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class EquipmentStatsInfo { + /** + * 设备编码 Y + * 设备唯一编码,对同一对接平台,保证唯一 + */ + private String EquipmentID; + + /** + * 充电设备累计电量 Y + * 累计电量,单位kWh,精度0.1 + */ + private BigDecimal EquipmentElectricity; + + /** + * 充电设备接口统计信息列表 Y + * 充设备的所有充电设备接口统计对象集合 + */ + private List ConnectorStatsInfos; +} diff --git a/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/OperatorInfo.java b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/OperatorInfo.java new file mode 100644 index 000000000..d3239f693 --- /dev/null +++ b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/OperatorInfo.java @@ -0,0 +1,45 @@ +package com.jsowell.thirdparty.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 充电对接平台信息 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class OperatorInfo { + /** + * 对接平台ID(组织机构代码) Y + */ + private String OperatorID; + + /** + * 对接平台名称(机构全称) Y + */ + private String OperatorName; + + /** + * 对接平台电话(对接平台客服电话1) Y + */ + private String OperatorTel1; + + /** + * 对接平台电话2(对接平台客服电话2 ) N + */ + private String OperatorTel2; + + /** + * 对接平台注册地址 N + */ + private String OperatorRegAddress; + + /** + * 备注 N + */ + private String OperatorNote; +} diff --git a/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/OrderInfo.java b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/OrderInfo.java new file mode 100644 index 000000000..ea113e8f2 --- /dev/null +++ b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/OrderInfo.java @@ -0,0 +1,150 @@ +package com.jsowell.thirdparty.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 订单信息 + */ +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class OrderInfo { + /** + * 对接平台ID Y + * 组织机构代码 + */ + private String OperatorID; + + /** + * 设备所属运营商ID Y + * 设备所属运营商组织机构代码 + */ + private String EquipmentOwnerID; + + /** + * 充电站ID Y + * 对接平台自定义的唯一编码 + */ + private String StationID; + + /** + * 设备编码 Y + * 设备唯一编码,对同一对接平台,保证唯一 + */ + private String EquipmentID; + + /** + * 充电设备接口编码 Y + * 充电设备接口编码,同一对接平台内唯一 + */ + private String ConnectorID; + + /** + * 充电订单号 Y + * 对接平台系统订单编号 + */ + private String StartChargeSeq; + + /** + * 用户发起充电类型 Y + * 1:本平台注册用户 + * 2:卡用户 + * 3:互联互通用户 + * 10:其他 + */ + private Integer UserChargeType; + + /** + * 用户手机号 N + * 若用户发起充电类型为 APP,用户手机号必填 + */ + private String MobileNumber; + + /** + * 本次充电消费总金额(单位:元,保留小数点后2位) Y + */ + private BigDecimal Money; + + /** + * 本次充电电费总金额(单位:元,保留小数点后2位) Y + */ + private BigDecimal ElectMoney; + + /** + * 本次充电服务费金额(单位:元,保留小数点后2位) Y + */ + private BigDecimal ServiceMoney; + + /** + * 本次充电电量 Y + * 单位kWh,精度0.01,保留小数点后2位 + */ + private BigDecimal Elect; + + /** + * 本次充电开始时间 Y + * 格式“yyyy-MM-dd HH:mm:ss” + */ + private String StartTime; + + /** + * 本次充电结束时间 Y + * 格式“yyyy-MM-dd HH:mm:ss” + */ + private String EndTime; + + /** + * 支付金额 Y + */ + private BigDecimal PaymentAmount; + + /** + * 支付时间 N + */ + private String PayTime; + + /** + * 支付方式 Y + * 1:支付宝 + * 2:微信支付 + * 3:交通卡 + * 4:预充卡 + * 5:银联 + * 6:其他自定义 + */ + private Integer PayChannel; + + /** + * 优惠信息描述 N + * 描述支付的相关优惠信息,如优惠券,折扣等 + */ + private String DiscountInfo; + + /** + * 充电结束原因 Y + * 0:用户手动停止充电 + * 1:客户归属地运营商平台停止充电 + * 2:BMS停止充电 + * 3:充电机设备故障 + * 4:连接器断开 + * 5-99自定义 + */ + private Integer StopReason; + + /** + * 时段数N,范围:0~32 N + */ + private Integer SumPeriod; + + /** + * 充电明细信息 Y + */ + private List ChargeDetails; +} diff --git a/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/StationInfo.java b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/StationInfo.java new file mode 100644 index 000000000..8e6f667fd --- /dev/null +++ b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/StationInfo.java @@ -0,0 +1,318 @@ +package com.jsowell.thirdparty.vo; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 充电站信息 + */ +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class StationInfo { + /** + * 充电站ID Y + * 对接平台自定义的唯一编码 + */ + private String StationID; + + /** + * 对接平台ID Y + * 组织机构代码 + */ + private String OperatorID; + + /** + * 设备所属运营商ID Y + * 设备所属运营商组织机构代码 + */ + private String EquipmentOwnerID; + + /** + * 充电站名称 Y + * 充电站名称的描述 + */ + private String StationName; + + /** + * 是否独立报桩 (0-否;1-是) Y + * 如果是独立报桩需要填写户号以及容量 + */ + private Integer IsAloneApply; + + /** + * 户号 N + * 国网电费账单户号 + */ + private String AccountNumber; + + /** + * 容量(单位KW) N + * 独立电表申请的功率 + */ + private BigDecimal Capacity; + + /** + * 是否是公共停车场库 (0-否;1-是) Y + * 如果是公共停车场库需要填写场库编号 + */ + private Integer IsPublicParkingLot; + + /** + * 停车场库编号 N + * 公共停车场库编号 + */ + private String ParkingLotNumber; + + /** + * 充电站国家代码 Y + * 比如CN + */ + private String CountryCode; + + /** + * 充电站省市辖区编码 Y + * 填写内容为参照 GB/T2260-2015 + */ + private String AreaCode; + + /** + * 详细地址 Y + */ + private String Address; + + /** + * 站点电话 N + * 能够联系场站工作人员进行协助的联系电话 + */ + private String StationTel; + + /** + * 服务电话 Y + * 平台服务电话,例如400 的电话 + */ + private String ServiceTel; + + /** + * 站点类型 Y + * 1-公共 + * 50-个人 + * 100-公交(专用) + * 101-环卫(专用) + * 102-物流(专用) + * 103-出租车(专用) + * 104-分时租赁(专用) + * 105-小区共享(专用) + * 106-单位(专用) + * 255-其他 + */ + private Integer StationType; + + /** + * 站点状态 Y + * 0:未知 + * 1:建设中 + * 5:关闭下线 + * 6:维护中 + * 50:正常使用 + */ + private Integer StationStatus; + + /** + * 车位数量 Y + * 可停放进行充电的车位总数(默认:0-未知) + */ + private Integer ParkNums; + + /** + * 经度 Y + * GCJ-02坐标系 + */ + private BigDecimal StationLng; + + /** + * 纬度 Y + * GCJ-02坐标系 + */ + private BigDecimal StationLat; + + /** + * 站点引导 N + * 描述性文字,用于引导车主找到充电车位 + */ + private String SiteGuide; + + /** + * 建设场所 Y + * 1:居民区 + * 2:公共机构 + * 3:企事业单位 + * 4:写字楼 + * 5:工业园区 + * 6:交通枢纽 + * 7:大型文体设施 + * 8:城市绿地 + * 9:大型建筑配建停车场 + * 10:路边停车位 + * 11:城际高速服务区 + * 12:风景区 + * 13:公交场站 + * 14:加油加气站 + * 15:出租车 + * 255:其他 + */ + private Integer Construction; + + /** + * 站点照片 N + * 充电设备照片、充电车位照片、停车场入口照片 + */ + private List Pictures; + + /** + * 使用车型描述 N + * 描述该站点接受的车大小以及类型,如大巴、物流车、私家乘用车、出租车等 + */ + private String MatchCars; + + /** + * 车位楼层及数量描述 N + * 车位楼层以及数量信息 + */ + private String ParkInfo; + + /** + * 停车场产权方 N + * 停车场产权人 + */ + private String ParkOwner; + + /** + * 停车场管理方 N + * 停车场管理人(如:XX 物业) + */ + private String ParkManager; + + /** + * 全天开放 Y + * 是否全天开放(0-否;1-是),如果为0,则营业时间必填 + */ + private Integer OpenAllDay; + + /** + * 营业时间 N + * 营业时间描述,推荐格式:周一至周日00:00-24:00 + */ + private String BusineHours; + + /** + * 最低单价 Y + * 最低充电电费率 + */ + private BigDecimal MinElectricityPrice; + + /** + * 充电电费率 Y + * 充电费描述,推荐格式:XX 元/度 + */ + private String ElectricityFee; + + /** + * 服务费率 Y + * 服务费率描述,推荐格式:XX 元/度 + */ + private String ServiceFee; + + /** + * 免费停车 Y + * 是否停车免费(0-否;1-是) + */ + private Integer ParkFree; + + /** + * 停车费 N + * 停车费率描述 + */ + private String ParkFee; + + /** + * 支付方式 Y + * 支付方式:刷卡、线上、现金 其中电子钱包类卡为刷卡,身份鉴权卡、微信/ 支付宝、APP为线上 + */ + private String Payment; + + /** + * 是否支持预约 Y + * 充电设备是否需要提前预约后才能使用。(0-不支持预约;1-支持预约) 不填默认为0 + */ + private Integer SupportOrder; + + /** + * 备注 N + * 其他备注信息 + */ + private String Remark; + + /** + * 充电设备信息列表 Y + * 该充电站所有充电设备信息对象集合 + */ + private List EquipmentInfos; + + /** + * 停车收费类型 Y + * 0:停车收费; + * 1:停车免费; + * 2:限时免费; + * 3:充电限免 + */ + private Integer ParkFeeType; + + /** + * 是否靠近卫生间(0-否;1-是) Y + */ + private Integer ToiletFlag; + + /** + * 是否靠近便利店(0-否;1-是) Y + */ + private Integer StoreFlag; + + /** + * 是否靠近餐厅(0-否;1-是) Y + */ + private Integer RestaurantFlag; + + /** + * 是否靠近休息室(0-否;1-是) Y + */ + private Integer LoungeFlag; + + /** + * 是否有雨棚(0-否;1-是) Y + */ + private Integer CanopyFlag; + + /** + * 是否有小票机(0-否;1-是) Y + */ + private Integer PrinterFlag; + + /** + * 是否有道闸(0-否;1-是) Y + */ + private Integer BarrierFlag; + + /** + * 是否有地锁(0-否;1-是) Y + */ + private Integer ParkingLockFlag; + +} diff --git a/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/StationStatusInfo.java b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/StationStatusInfo.java new file mode 100644 index 000000000..55fb432a7 --- /dev/null +++ b/jsowell-thirdparty/src/main/java/com/jsowell/thirdparty/vo/StationStatusInfo.java @@ -0,0 +1,29 @@ +package com.jsowell.thirdparty.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 充电站状态信息 + */ +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class StationStatusInfo { + /** + * 充电站ID Y + * 对接平台自定义的唯一编码,不足长度在前方补0 + */ + private String StationID; + + /** + * 充电设备接口状态列表 Y + * 所有充电设备接口的状态 + */ + private List ConnectorStatusInfos; +} diff --git a/jsowell-ui/.editorconfig b/jsowell-ui/.editorconfig new file mode 100644 index 000000000..7034f9bf3 --- /dev/null +++ b/jsowell-ui/.editorconfig @@ -0,0 +1,22 @@ +# 告诉EditorConfig插件,这是根文件,不用继续往上查找 +root = true + +# 匹配全部文件 +[*] +# 设置字符集 +charset = utf-8 +# 缩进风格,可选space、tab +indent_style = space +# 缩进的空格数 +indent_size = 2 +# 结尾换行符,可选lf、cr、crlf +end_of_line = lf +# 在文件结尾插入新行 +insert_final_newline = true +# 删除一行中的前后空格 +trim_trailing_whitespace = true + +# 匹配md结尾的文件 +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/jsowell-ui/.env.development b/jsowell-ui/.env.development new file mode 100644 index 000000000..8bd201e1c --- /dev/null +++ b/jsowell-ui/.env.development @@ -0,0 +1,11 @@ +# 页面标题 +VUE_APP_TITLE = 举视后台管理系统 + +# 开发环境配置 +ENV = 'development' + +# 举视后台管理系统/开发环境 +VUE_APP_BASE_API = '/dev-api' + +# 路由懒加载 +VUE_CLI_BABEL_TRANSPILE_MODULES = true diff --git a/jsowell-ui/.env.production b/jsowell-ui/.env.production new file mode 100644 index 000000000..ce94db862 --- /dev/null +++ b/jsowell-ui/.env.production @@ -0,0 +1,8 @@ +# 页面标题 +VUE_APP_TITLE = 举视后台管理系统 + +# 生产环境配置 +ENV = 'production' + +# 举视后台管理系统/生产环境 +VUE_APP_BASE_API = '/prod-api' diff --git a/jsowell-ui/.env.staging b/jsowell-ui/.env.staging new file mode 100644 index 000000000..c740e5668 --- /dev/null +++ b/jsowell-ui/.env.staging @@ -0,0 +1,10 @@ +# 页面标题 +VUE_APP_TITLE = 举视后台管理系统 + +NODE_ENV = production + +# 测试环境配置 +ENV = 'staging' + +# 举视后台管理系统/测试环境 +VUE_APP_BASE_API = '/stage-api' diff --git a/jsowell-ui/.eslintignore b/jsowell-ui/.eslintignore new file mode 100644 index 000000000..89be6f659 --- /dev/null +++ b/jsowell-ui/.eslintignore @@ -0,0 +1,10 @@ +# 忽略build目录下类型为js的文件的语法检查 +build/*.js +# 忽略src/assets目录下文件的语法检查 +src/assets +# 忽略public目录下文件的语法检查 +public +# 忽略当前目录下为js的文件的语法检查 +*.js +# 忽略当前目录下为vue的文件的语法检查 +*.vue \ No newline at end of file diff --git a/jsowell-ui/.eslintrc.js b/jsowell-ui/.eslintrc.js new file mode 100644 index 000000000..b27a7fe76 --- /dev/null +++ b/jsowell-ui/.eslintrc.js @@ -0,0 +1,279 @@ +// ESlint 检查配置 +module.exports = { + root: true, + parserOptions: { + parser: "babel-eslint", + sourceType: "module", + }, + env: { + browser: true, + node: true, + es6: true, + }, + // globals: { + // AMap: "true", + // }, + extends: ["plugin:vue/recommended", "eslint:recommended"], + + // add your custom rules here + //it is base on https://github.com/vuejs/eslint-config-vue + rules: { + "vue/max-attributes-per-line": [ + 2, + { + singleline: 10, + multiline: { + max: 1, + allowFirstLine: false, + }, + }, + ], + "vue/singleline-html-element-content-newline": "off", + "vue/multiline-html-element-content-newline": "off", + "vue/name-property-casing": ["error", "PascalCase"], + "vue/no-v-html": "off", + "accessor-pairs": 2, + "arrow-spacing": [ + 2, + { + before: true, + after: true, + }, + ], + "block-spacing": [2, "always"], + "brace-style": [ + 2, + "1tbs", + { + allowSingleLine: true, + }, + ], + camelcase: [ + 0, + { + properties: "always", + }, + ], + "comma-dangle": [2, "never"], + "comma-spacing": [ + 2, + { + before: false, + after: true, + }, + ], + "comma-style": [2, "last"], + "constructor-super": 2, + curly: [2, "multi-line"], + "dot-location": [2, "property"], + "eol-last": 2, + eqeqeq: ["error", "always", { null: "ignore" }], + "generator-star-spacing": [ + 2, + { + before: true, + after: true, + }, + ], + "handle-callback-err": [2, "^(err|error)$"], + indent: [ + 2, + 2, + { + SwitchCase: 1, + }, + ], + "jsx-quotes": [2, "prefer-single"], + "key-spacing": [ + 2, + { + beforeColon: false, + afterColon: true, + }, + ], + "keyword-spacing": [ + 2, + { + before: true, + after: true, + }, + ], + "new-cap": [ + 2, + { + newIsCap: true, + capIsNew: false, + }, + ], + "new-parens": 2, + "no-array-constructor": 2, + "no-caller": 2, + "no-console": "off", + "no-class-assign": 2, + "no-cond-assign": 2, + "no-const-assign": 2, + "no-control-regex": 0, + "no-delete-var": 2, + "no-dupe-args": 2, + "no-dupe-class-members": 2, + "no-dupe-keys": 2, + "no-duplicate-case": 2, + "no-empty-character-class": 2, + "no-empty-pattern": 2, + "no-eval": 2, + "no-ex-assign": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-extra-boolean-cast": 2, + "no-extra-parens": [2, "functions"], + "no-fallthrough": 2, + "no-floating-decimal": 2, + "no-func-assign": 2, + "no-implied-eval": 2, + "no-inner-declarations": [2, "functions"], + "no-invalid-regexp": 2, + "no-irregular-whitespace": 2, + "no-iterator": 2, + "no-label-var": 2, + "no-labels": [ + 2, + { + allowLoop: false, + allowSwitch: false, + }, + ], + "no-lone-blocks": 2, + "no-mixed-spaces-and-tabs": 2, + "no-multi-spaces": 2, + "no-multi-str": 2, + "no-multiple-empty-lines": [ + 2, + { + max: 1, + }, + ], + "no-native-reassign": 2, + "no-negated-in-lhs": 2, + "no-new-object": 2, + "no-new-require": 2, + "no-new-symbol": 2, + "no-new-wrappers": 2, + "no-obj-calls": 2, + "no-octal": 2, + "no-octal-escape": 2, + "no-path-concat": 2, + "no-proto": 2, + "no-redeclare": 2, + "no-regex-spaces": 2, + "no-return-assign": [2, "except-parens"], + "no-self-assign": 2, + "no-self-compare": 2, + "no-sequences": 2, + "no-shadow-restricted-names": 2, + "no-spaced-func": 2, + "no-sparse-arrays": 2, + "no-this-before-super": 2, + "no-throw-literal": 2, + "no-trailing-spaces": 2, + "no-undef": 2, + "no-undef-init": 2, + "no-unexpected-multiline": 2, + "no-unmodified-loop-condition": 2, + "no-unneeded-ternary": [ + 2, + { + defaultAssignment: false, + }, + ], + "no-unreachable": 2, + "no-unsafe-finally": 2, + "no-unused-vars": [ + 2, + { + vars: "all", + args: "none", + }, + ], + "no-useless-call": 2, + "no-useless-computed-key": 2, + "no-useless-constructor": 2, + "no-useless-escape": 0, + "no-whitespace-before-property": 2, + "no-with": 2, + "one-var": [ + 2, + { + initialized: "never", + }, + ], + "operator-linebreak": [ + 2, + "after", + { + overrides: { + "?": "before", + ":": "before", + }, + }, + ], + "padded-blocks": [2, "never"], + quotes: [ + 2, + "single", + { + avoidEscape: true, + allowTemplateLiterals: true, + }, + ], + semi: [2, "never"], + "semi-spacing": [ + 2, + { + before: false, + after: true, + }, + ], + "space-before-blocks": [2, "always"], + "space-before-function-paren": [2, "never"], + "space-in-parens": [2, "never"], + "space-infix-ops": 2, + "space-unary-ops": [ + 2, + { + words: true, + nonwords: false, + }, + ], + "spaced-comment": [ + 2, + "always", + { + markers: [ + "global", + "globals", + "eslint", + "eslint-disable", + "*package", + "!", + ",", + ], + }, + ], + "template-curly-spacing": [2, "never"], + "use-isnan": 2, + "valid-typeof": 2, + "wrap-iife": [2, "any"], + "yield-star-spacing": [2, "both"], + yoda: [2, "never"], + "prefer-const": 2, + "no-debugger": process.env.NODE_ENV === "production" ? 2 : 0, + "object-curly-spacing": [ + 2, + "always", + { + objectsInObjects: false, + }, + ], + "array-bracket-spacing": [2, "never"], + }, +}; diff --git a/jsowell-ui/.gitignore b/jsowell-ui/.gitignore new file mode 100644 index 000000000..78a752d87 --- /dev/null +++ b/jsowell-ui/.gitignore @@ -0,0 +1,23 @@ +.DS_Store +node_modules/ +dist/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +**/*.log + +tests/**/coverage/ +tests/e2e/reports +selenium-debug.log + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.local + +package-lock.json +yarn.lock diff --git a/jsowell-ui/README.md b/jsowell-ui/README.md new file mode 100644 index 000000000..2f227da3d --- /dev/null +++ b/jsowell-ui/README.md @@ -0,0 +1,26 @@ +## 开发 + +# 进入项目目录 +cd jsowell-ui + +# 安装依赖 +npm install + +# 建议不要直接使用 cnpm 安装依赖,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题 +npm install --registry=https://registry.npmmirror.com + +# 启动服务 +npm run dev +``` + +浏览器访问 http://localhost:80 + +## 发布 + +```bash +# 构建测试环境 +npm run build:stage + +# 构建生产环境 +npm run build:prod +``` \ No newline at end of file diff --git a/jsowell-ui/babel.config.js b/jsowell-ui/babel.config.js new file mode 100644 index 000000000..c8267b2dd --- /dev/null +++ b/jsowell-ui/babel.config.js @@ -0,0 +1,13 @@ +module.exports = { + presets: [ + // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app + '@vue/cli-plugin-babel/preset' + ], + 'env': { + 'development': { + // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require(). + // This plugin can significantly increase the speed of hot updates, when you have a large number of pages. + 'plugins': ['dynamic-import-node'] + } + } +} \ No newline at end of file diff --git a/jsowell-ui/bin/build-sit.bat b/jsowell-ui/bin/build-sit.bat new file mode 100644 index 000000000..56aa9b136 --- /dev/null +++ b/jsowell-ui/bin/build-sit.bat @@ -0,0 +1,12 @@ +@echo off +echo. +echo [Ϣ] Weḅdistļ +echo. + +%~d0 +cd %~dp0 + +cd .. +npm run build:stage + +pause \ No newline at end of file diff --git a/jsowell-ui/bin/build.bat b/jsowell-ui/bin/build.bat new file mode 100644 index 000000000..dda590d22 --- /dev/null +++ b/jsowell-ui/bin/build.bat @@ -0,0 +1,12 @@ +@echo off +echo. +echo [Ϣ] Weḅdistļ +echo. + +%~d0 +cd %~dp0 + +cd .. +npm run build:prod + +pause \ No newline at end of file diff --git a/jsowell-ui/bin/package.bat b/jsowell-ui/bin/package.bat new file mode 100644 index 000000000..0e5bc0fb5 --- /dev/null +++ b/jsowell-ui/bin/package.bat @@ -0,0 +1,12 @@ +@echo off +echo. +echo [Ϣ] װWeḅnode_modulesļ +echo. + +%~d0 +cd %~dp0 + +cd .. +npm install --registry=https://registry.npmmirror.com + +pause \ No newline at end of file diff --git a/jsowell-ui/bin/run-web.bat b/jsowell-ui/bin/run-web.bat new file mode 100644 index 000000000..d30deae79 --- /dev/null +++ b/jsowell-ui/bin/run-web.bat @@ -0,0 +1,12 @@ +@echo off +echo. +echo [Ϣ] ʹ Vue CLI Web ̡ +echo. + +%~d0 +cd %~dp0 + +cd .. +npm run dev + +pause \ No newline at end of file diff --git a/jsowell-ui/package.json b/jsowell-ui/package.json new file mode 100644 index 000000000..f47a91539 --- /dev/null +++ b/jsowell-ui/package.json @@ -0,0 +1,89 @@ +{ + "name": "jsowell", + "version": "1.0.0", + "description": "举视管理系统", + "scripts": { + "dev": "vue-cli-service serve", + "build:prod": "vue-cli-service build", + "build:stage": "vue-cli-service build --mode staging", + "preview": "node build/index.js --preview", + "lint": "eslint --ext .js,.vue src" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "src/**/*.{js,vue}": [ + "eslint --fix", + "git add" + ] + }, + "keywords": [ + "vue", + "admin", + "dashboard", + "element-ui", + "boilerplate", + "admin-template", + "management-system" + ], + "dependencies": { + "@amap/amap-jsapi-loader": "^1.0.1", + "@riophae/vue-treeselect": "0.4.0", + "axios": "0.24.0", + "clipboard": "2.0.8", + "copy-webpack-plugin": "^6.0.3", + "core-js": "3.19.1", + "echarts": "^4.9.0", + "element-china-area-data": "^5.0.2", + "element-ui": "2.15.8", + "file-saver": "2.0.5", + "fuse.js": "6.4.3", + "highlight.js": "9.18.5", + "js-beautify": "1.13.0", + "js-cookie": "3.0.1", + "jsencrypt": "3.0.0-rc.1", + "nprogress": "0.2.0", + "qrcodejs2": "^0.0.2", + "quill": "1.3.7", + "screenfull": "5.0.2", + "sortablejs": "1.10.2", + "vue": "2.6.12", + "vue-count-to": "1.0.13", + "vue-cropper": "0.5.5", + "vue-meta": "2.4.0", + "vue-qr": "^4.0.9", + "vue-router": "3.4.9", + "vuedraggable": "2.24.3", + "vuex": "3.6.0" + }, + "devDependencies": { + "@vue/cli-plugin-babel": "4.4.6", + "@vue/cli-plugin-eslint": "4.4.6", + "@vue/cli-service": "4.4.6", + "babel-eslint": "10.1.0", + "babel-plugin-dynamic-import-node": "2.3.3", + "chalk": "4.1.0", + "compression-webpack-plugin": "5.0.2", + "connect": "3.6.6", + "eslint": "^7.15.0", + "eslint-plugin-vue": "7.2.0", + "lint-staged": "10.5.3", + "runjs": "4.4.2", + "sass": "1.32.13", + "sass-loader": "10.1.1", + "script-ext-html-webpack-plugin": "2.1.5", + "svg-sprite-loader": "5.1.1", + "vue-template-compiler": "2.6.12" + }, + "engines": { + "node": ">=8.9", + "npm": ">= 3.0.0" + }, + "browserslist": [ + "> 1%", + "last 2 versions" + ] +} diff --git a/jsowell-ui/public/favicon.ico b/jsowell-ui/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..225d890c2c5783e8b99ce43fa4c266fb4ededc82 GIT binary patch literal 67646 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 literal 0 HcmV?d00001 diff --git a/jsowell-ui/public/html/ie.html b/jsowell-ui/public/html/ie.html new file mode 100644 index 000000000..052ffcd64 --- /dev/null +++ b/jsowell-ui/public/html/ie.html @@ -0,0 +1,46 @@ + + + + + + 请升级您的浏览器 + + + + + + +

请升级您的浏览器,以便我们更好的为您提供服务!

+

您正在使用 Internet Explorer 的早期版本(IE11以下版本或使用该内核的浏览器)。这意味着在升级浏览器前,您将无法访问此网站。

+
+

请注意:微软公司对Windows XP 及 Internet Explorer 早期版本的支持已经结束

+

自 2016 年 1 月 12 日起,Microsoft 不再为 IE 11 以下版本提供相应支持和更新。没有关键的浏览器安全更新,您的电脑可能易受有害病毒、间谍软件和其他恶意软件的攻击,它们可以窃取或损害您的业务数据和信息。请参阅 微软对 Internet Explorer 早期版本的支持将于 2016 年 1 月 12 日结束的说明

+
+

您可以选择更先进的浏览器

+

推荐使用以下浏览器的最新版本。如果您的电脑已有以下浏览器的最新版本则直接使用该浏览器访问即可。

+ +
+ + \ No newline at end of file diff --git a/jsowell-ui/public/index.html b/jsowell-ui/public/index.html new file mode 100644 index 000000000..8ed4d7375 --- /dev/null +++ b/jsowell-ui/public/index.html @@ -0,0 +1,213 @@ + + + + + + + + + <%= webpackConfig.name %> + + + + + +
+
+
+
+
+
正在加载系统资源,请耐心等待
+
+
+ + diff --git a/jsowell-ui/public/robots.txt b/jsowell-ui/public/robots.txt new file mode 100644 index 000000000..77470cb39 --- /dev/null +++ b/jsowell-ui/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / \ No newline at end of file diff --git a/jsowell-ui/src/App.vue b/jsowell-ui/src/App.vue new file mode 100644 index 000000000..391d951c4 --- /dev/null +++ b/jsowell-ui/src/App.vue @@ -0,0 +1,19 @@ + + + diff --git a/jsowell-ui/src/api/billing/template.js b/jsowell-ui/src/api/billing/template.js new file mode 100644 index 000000000..b6620c847 --- /dev/null +++ b/jsowell-ui/src/api/billing/template.js @@ -0,0 +1,100 @@ +import request from "@/utils/request"; + +// 查询计费模板列表 +export function listTemplate(query) { + return request({ + url: "/billing/template/list", + method: "get", + params: query, + }); +} + +// 增加计费模板 +export function addBillingTemplate(data) { + console.log("增加计费模板", JSON.stringify(data)); + return request({ + method: "post", + url: "/billing/template/createBillingTemplate", + data, + }); +} + +// 更新计费模板 +export function updateBillingTemplate(data) { + console.log("更新计费模板", JSON.stringify(data)); + return request({ + method: "post", + url: "/billing/template/updateBillingTemplate", + data, + }); +} + +// 查询计费模板详细 +export function getTemplate(id) { + return request({ + url: "/billing/template/" + id, + method: "get", + }); +} + +// 新增计费模板 +export function addTemplate(data) { + return request({ + url: "/billing/template", + method: "post", + data: data, + }); +} + +// 修改计费模板 +export function updateTemplate(data) { + return request({ + url: "/billing/template", + method: "put", + data: data, + }); +} + +// 删除计费模板 +export function delTemplate(id) { + return request({ + url: "/billing/template/" + id, + method: "delete", + }); +} + +// 查询公共计费模板 +export function queryPublicBillingTemplateList() { + return request({ + url: "/billing/template/queryPublicBillingTemplateList", + method: "get", + }); +} + +// 查询站点计费模板 +export function queryStationBillingTemplateList(stationId) { + return request({ + url: "/billing/template/queryStationBillingTemplateList/" + stationId, + method: "get", + }); +} + +// 站点导入计费模板接口 +export function stationImportBillingTemplate(data) { + return request({ + url: "/billing/template/stationImportBillingTemplate", + method: "post", + data: data, + }); +} + +// 发布计费模板 +export function publishBillingTemplate(data) { + return request({ + url: "/billing/template/publishBillingTemplate", + method: "post", + data: data, + }); +} + +// 查询全部计费模板 diff --git a/jsowell-ui/src/api/index/index.js b/jsowell-ui/src/api/index/index.js new file mode 100644 index 000000000..a9c62d454 --- /dev/null +++ b/jsowell-ui/src/api/index/index.js @@ -0,0 +1,17 @@ +import request from '@/utils/request' + +// 查询首页信息 +export function getGeneralSituation(data) { + return request({ + url: '/index/getGeneralSituation', + method: 'post', + data: data + }) +} +export function getOrderInfo(data) { + return request({ + url: '/index/getOrderInfo', + method: 'post', + data: data + }) +} diff --git a/jsowell-ui/src/api/login.js b/jsowell-ui/src/api/login.js new file mode 100644 index 000000000..649f59c81 --- /dev/null +++ b/jsowell-ui/src/api/login.js @@ -0,0 +1,59 @@ +import request from '@/utils/request' + +// 登录方法 +export function login(username, password, code, uuid) { + const data = { + username, + password, + code, + uuid + } + return request({ + url: '/login', + headers: { + isToken: false + }, + method: 'post', + data: data + }) +} + +// 注册方法 +export function register(data) { + return request({ + url: '/register', + headers: { + isToken: false + }, + method: 'post', + data: data + }) +} + +// 获取用户详细信息 +export function getInfo() { + return request({ + url: '/getInfo', + method: 'get' + }) +} + +// 退出方法 +export function logout() { + return request({ + url: '/logout', + method: 'post' + }) +} + +// 获取验证码 +export function getCodeImg() { + return request({ + url: '/captchaImage', + headers: { + isToken: false + }, + method: 'get', + timeout: 20000 + }) +} \ No newline at end of file diff --git a/jsowell-ui/src/api/member/info.js b/jsowell-ui/src/api/member/info.js new file mode 100644 index 000000000..94f80e563 --- /dev/null +++ b/jsowell-ui/src/api/member/info.js @@ -0,0 +1,79 @@ +import request from '@/utils/request' + +// 查询会员基础信息列表 +export function listInfo(query) { + return request({ + url: '/member/info/list', + method: 'get', + params: query + }) +} + +// 查询会员基础信息详细 +export function getInfo(id) { + return request({ + url: '/member/info/' + id, + method: 'get' + }) +} + +// 查询会员个人桩信息 +export function getMemberPersonPileInfo(id) { + return request({ + url: '/member/info/getMemberPersonPileInfo/' + id, + method: 'get' + }) +} + +// 新增会员基础信息 +export function addInfo(data) { + return request({ + url: '/member/info', + method: 'post', + data: data + }) +} + +// 修改会员基础信息 +export function updateInfo(data) { + return request({ + url: '/member/info', + method: 'put', + data: data + }) +} + +// 删除会员基础信息 +export function delInfo(id) { + return request({ + url: '/member/info/' + id, + method: 'delete' + }) +} + +// 修改会员基础信息 +export function updateGiftBalance(data) { + return request({ + url: '/member/info/updateGiftBalance', + method: 'put', + data: data + }) +} + +// 获取用户账户余额变动信息 +export function getMemberBalanceChanges(data) { + return request({ + url: '/member/info/queryMemberBalanceChanges', + method: 'post', + data: data + }) +} + +// 获取会员交易流水 +export function getMemberTransactionRecordList(data) { + return request({ + url: '/member/info/selectMemberTransactionRecordList', + method: 'post', + data: data + }) +} diff --git a/jsowell-ui/src/api/menu.js b/jsowell-ui/src/api/menu.js new file mode 100644 index 000000000..faef101c4 --- /dev/null +++ b/jsowell-ui/src/api/menu.js @@ -0,0 +1,9 @@ +import request from '@/utils/request' + +// 获取路由 +export const getRouters = () => { + return request({ + url: '/getRouters', + method: 'get' + }) +} \ No newline at end of file diff --git a/jsowell-ui/src/api/monitor/cache.js b/jsowell-ui/src/api/monitor/cache.js new file mode 100644 index 000000000..72c5f6a3e --- /dev/null +++ b/jsowell-ui/src/api/monitor/cache.js @@ -0,0 +1,57 @@ +import request from '@/utils/request' + +// 查询缓存详细 +export function getCache() { + return request({ + url: '/monitor/cache', + method: 'get' + }) +} + +// 查询缓存名称列表 +export function listCacheName() { + return request({ + url: '/monitor/cache/getNames', + method: 'get' + }) +} + +// 查询缓存键名列表 +export function listCacheKey(cacheName) { + return request({ + url: '/monitor/cache/getKeys/' + cacheName, + method: 'get' + }) +} + +// 查询缓存内容 +export function getCacheValue(cacheName, cacheKey) { + return request({ + url: '/monitor/cache/getValue/' + cacheName + '/' + cacheKey, + method: 'get' + }) +} + +// 清理指定名称缓存 +export function clearCacheName(cacheName) { + return request({ + url: '/monitor/cache/clearCacheName/' + cacheName, + method: 'delete' + }) +} + +// 清理指定键名缓存 +export function clearCacheKey(cacheKey) { + return request({ + url: '/monitor/cache/clearCacheKey/' + cacheKey, + method: 'delete' + }) +} + +// 清理全部缓存 +export function clearCacheAll() { + return request({ + url: '/monitor/cache/clearCacheAll', + method: 'delete' + }) +} diff --git a/jsowell-ui/src/api/monitor/job.js b/jsowell-ui/src/api/monitor/job.js new file mode 100644 index 000000000..38155693a --- /dev/null +++ b/jsowell-ui/src/api/monitor/job.js @@ -0,0 +1,71 @@ +import request from '@/utils/request' + +// 查询定时任务调度列表 +export function listJob(query) { + return request({ + url: '/monitor/job/list', + method: 'get', + params: query + }) +} + +// 查询定时任务调度详细 +export function getJob(jobId) { + return request({ + url: '/monitor/job/' + jobId, + method: 'get' + }) +} + +// 新增定时任务调度 +export function addJob(data) { + return request({ + url: '/monitor/job', + method: 'post', + data: data + }) +} + +// 修改定时任务调度 +export function updateJob(data) { + return request({ + url: '/monitor/job', + method: 'put', + data: data + }) +} + +// 删除定时任务调度 +export function delJob(jobId) { + return request({ + url: '/monitor/job/' + jobId, + method: 'delete' + }) +} + +// 任务状态修改 +export function changeJobStatus(jobId, status) { + const data = { + jobId, + status + } + return request({ + url: '/monitor/job/changeStatus', + method: 'put', + data: data + }) +} + + +// 定时任务立即执行一次 +export function runJob(jobId, jobGroup) { + const data = { + jobId, + jobGroup + } + return request({ + url: '/monitor/job/run', + method: 'put', + data: data + }) +} \ No newline at end of file diff --git a/jsowell-ui/src/api/monitor/jobLog.js b/jsowell-ui/src/api/monitor/jobLog.js new file mode 100644 index 000000000..6e0be6166 --- /dev/null +++ b/jsowell-ui/src/api/monitor/jobLog.js @@ -0,0 +1,26 @@ +import request from '@/utils/request' + +// 查询调度日志列表 +export function listJobLog(query) { + return request({ + url: '/monitor/jobLog/list', + method: 'get', + params: query + }) +} + +// 删除调度日志 +export function delJobLog(jobLogId) { + return request({ + url: '/monitor/jobLog/' + jobLogId, + method: 'delete' + }) +} + +// 清空调度日志 +export function cleanJobLog() { + return request({ + url: '/monitor/jobLog/clean', + method: 'delete' + }) +} diff --git a/jsowell-ui/src/api/monitor/logininfor.js b/jsowell-ui/src/api/monitor/logininfor.js new file mode 100644 index 000000000..4d112b78a --- /dev/null +++ b/jsowell-ui/src/api/monitor/logininfor.js @@ -0,0 +1,34 @@ +import request from '@/utils/request' + +// 查询登录日志列表 +export function list(query) { + return request({ + url: '/monitor/logininfor/list', + method: 'get', + params: query + }) +} + +// 删除登录日志 +export function delLogininfor(infoId) { + return request({ + url: '/monitor/logininfor/' + infoId, + method: 'delete' + }) +} + +// 解锁用户登录状态 +export function unlockLogininfor(userName) { + return request({ + url: '/monitor/logininfor/unlock/' + userName, + method: 'get' + }) +} + +// 清空登录日志 +export function cleanLogininfor() { + return request({ + url: '/monitor/logininfor/clean', + method: 'delete' + }) +} diff --git a/jsowell-ui/src/api/monitor/online.js b/jsowell-ui/src/api/monitor/online.js new file mode 100644 index 000000000..bd2213780 --- /dev/null +++ b/jsowell-ui/src/api/monitor/online.js @@ -0,0 +1,18 @@ +import request from '@/utils/request' + +// 查询在线用户列表 +export function list(query) { + return request({ + url: '/monitor/online/list', + method: 'get', + params: query + }) +} + +// 强退用户 +export function forceLogout(tokenId) { + return request({ + url: '/monitor/online/' + tokenId, + method: 'delete' + }) +} diff --git a/jsowell-ui/src/api/monitor/operlog.js b/jsowell-ui/src/api/monitor/operlog.js new file mode 100644 index 000000000..a04bca840 --- /dev/null +++ b/jsowell-ui/src/api/monitor/operlog.js @@ -0,0 +1,26 @@ +import request from '@/utils/request' + +// 查询操作日志列表 +export function list(query) { + return request({ + url: '/monitor/operlog/list', + method: 'get', + params: query + }) +} + +// 删除操作日志 +export function delOperlog(operId) { + return request({ + url: '/monitor/operlog/' + operId, + method: 'delete' + }) +} + +// 清空操作日志 +export function cleanOperlog() { + return request({ + url: '/monitor/operlog/clean', + method: 'delete' + }) +} diff --git a/jsowell-ui/src/api/monitor/server.js b/jsowell-ui/src/api/monitor/server.js new file mode 100644 index 000000000..e1f9ca214 --- /dev/null +++ b/jsowell-ui/src/api/monitor/server.js @@ -0,0 +1,9 @@ +import request from '@/utils/request' + +// 获取服务信息 +export function getServer() { + return request({ + url: '/monitor/server', + method: 'get' + }) +} \ No newline at end of file diff --git a/jsowell-ui/src/api/order/abnormalRecord.js b/jsowell-ui/src/api/order/abnormalRecord.js new file mode 100644 index 000000000..52ca7d242 --- /dev/null +++ b/jsowell-ui/src/api/order/abnormalRecord.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询订单异常记录列表 +export function listAbnormalRecord(query) { + return request({ + url: '/order/abnormalRecord/list', + method: 'get', + params: query + }) +} + +// 查询订单异常记录详细 +export function getAbnormalRecord(id) { + return request({ + url: '/order/abnormalRecord/' + id, + method: 'get' + }) +} + +// 新增订单异常记录 +export function addAbnormalRecord(data) { + return request({ + url: '/order/abnormalRecord', + method: 'post', + data: data + }) +} + +// 修改订单异常记录 +export function updateAbnormalRecord(data) { + return request({ + url: '/order/abnormalRecord', + method: 'put', + data: data + }) +} + +// 删除订单异常记录 +export function delAbnormalRecord(id) { + return request({ + url: '/order/abnormalRecord/' + id, + method: 'delete' + }) +} diff --git a/jsowell-ui/src/api/order/order.js b/jsowell-ui/src/api/order/order.js new file mode 100644 index 000000000..47a4c1412 --- /dev/null +++ b/jsowell-ui/src/api/order/order.js @@ -0,0 +1,53 @@ +import request from '@/utils/request' + +// 查询订单列表 +export function listOrder(query) { + return request({ + url: '/order/order/list', + method: 'get', + params: query + }) +} + +// 查询订单详细 +export function getOrder(orderCode) { + return request({ + url: '/order/orderDetail/' + orderCode, + method: 'get' + }) +} + +// 新增订单 +export function addOrder(data) { + return request({ + url: '/order/order', + method: 'post', + data: data + }) +} + +// 修改订单 +export function updateOrder(data) { + return request({ + url: '/order/order', + method: 'put', + data: data + }) +} + +// 删除订单 +export function delOrder(id) { + return request({ + url: '/order/order/' + id, + method: 'delete' + }) +} + +// 获取订单总金额 +export function totalData(query) { + return request({ + url: '/order/order/totalData', + method: 'get', + params: query + }) +} diff --git a/jsowell-ui/src/api/pile/basic.js b/jsowell-ui/src/api/pile/basic.js new file mode 100644 index 000000000..329dbe311 --- /dev/null +++ b/jsowell-ui/src/api/pile/basic.js @@ -0,0 +1,88 @@ +import request from '@/utils/request' + +// 查询设备管理列表 +export function listBasic(query) { + return request({ + url: '/pile/basic/list', + method: 'get', + params: query + }) +} + +// 查询设备管理详细 +export function getBasic(id) { + return request({ + url: '/pile/basic/' + id, + method: 'get' + }) +} + +// 新增设备管理 +export function addBasic(data) { + return request({ + url: '/pile/basic', + method: 'post', + data: data + }) +} + +export function batchAddBasic(data) { + console.log("批量新增接口", data); + return request({ + url: '/pile/basic/batchAdd', + method: 'post', + data: data + }) +} + +// 修改设备管理 +export function updateBasic(data) { + return request({ + url: '/pile/basic', + method: 'put', + data: data + }) +} + +// 删除设备管理 +export function delBasic(id) { + return request({ + url: '/pile/basic/' + id, + method: 'delete' + }) +} + +// 查询设备管理详细 +export function getPileDetailById(data) { + return request({ + url: '/pile/basic/getPileDetailById', + method: 'post', + data: data + }) +} + +// 通过充电站id查询所有充电桩列表 +export function queryPileListByStationId(stationId) { + return request({ + url: '/pile/basic/queryPileListByStationId/' + stationId, + method: 'get' + }) +} + +// 批量更新充电桩 +export function batchUpdatePileList(data) { + return request({ + url: '/pile/basic/batchUpdatePileList', + method: 'post', + data: data + }) +} + +// 查询充电桩通讯日志 +export function getPileFeedList(data) { + return request({ + url: '/pile/basic/getPileFeedList', + method: 'post', + data: data + }) +} diff --git a/jsowell-ui/src/api/pile/connector.js b/jsowell-ui/src/api/pile/connector.js new file mode 100644 index 000000000..f8fa22a59 --- /dev/null +++ b/jsowell-ui/src/api/pile/connector.js @@ -0,0 +1,54 @@ +import request from '@/utils/request' + +// 查询充电桩枪口信息列表 +export function listConnector(query) { + return request({ + url: '/pile/connector/list', + method: 'get', + params: query + }) +} + +// 查询充电桩枪口信息详细 +export function getConnector(id) { + return request({ + url: '/pile/connector/' + id, + method: 'get' + }) +} + +// 新增充电桩枪口信息 +export function addConnector(data) { + return request({ + url: '/pile/connector', + method: 'post', + data: data + }) +} + +// 修改充电桩枪口信息 +export function updateConnector(data) { + return request({ + url: '/pile/connector', + method: 'put', + data: data + }) +} + +// 删除充电桩枪口信息 +export function delConnector(id) { + return request({ + url: '/pile/connector/' + id, + method: 'delete' + }) +} + +// 通过入参查询接口列表 +export function queryConnectorListByParams(data) { + console.log("通过入参查询接口列表", data); + return request({ + url: '/pile/connector/getConnectorInfoListByParams', + method: 'post', + data: data + }) +} diff --git a/jsowell-ui/src/api/pile/merchant.js b/jsowell-ui/src/api/pile/merchant.js new file mode 100644 index 000000000..90e08fa33 --- /dev/null +++ b/jsowell-ui/src/api/pile/merchant.js @@ -0,0 +1,53 @@ +import request from '@/utils/request' + +// 查询充电桩运营商信息列表 +export function listMerchant(query) { + return request({ + url: '/pile/merchant/list', + method: 'get', + params: query + }) +} + +// 查询充电桩运营商信息详细 +export function getMerchant(id) { + return request({ + url: '/pile/merchant/' + id, + method: 'get' + }) +} + +// 新增充电桩运营商信息 +export function addMerchant(data) { + return request({ + url: '/pile/merchant', + method: 'post', + data: data + }) +} + +// 修改充电桩运营商信息 +export function updateMerchant(data) { + return request({ + url: '/pile/merchant', + method: 'put', + data: data + }) +} + +// 删除充电桩运营商信息 +export function delMerchant(id) { + return request({ + url: '/pile/merchant/' + id, + method: 'delete' + }) +} + +// 获取运营商列表 不分页 +export function getMerchantList(query) { + return request({ + url: '/pile/merchant/getMerchantList', + method: 'get', + params: query + }) +} diff --git a/jsowell-ui/src/api/pile/model.js b/jsowell-ui/src/api/pile/model.js new file mode 100644 index 000000000..673321734 --- /dev/null +++ b/jsowell-ui/src/api/pile/model.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询充电桩型号信息列表 +export function listModel(query) { + return request({ + url: '/pile/model/list', + method: 'get', + params: query + }) +} + +// 查询充电桩型号信息详细 +export function getModel(id) { + return request({ + url: '/pile/model/' + id, + method: 'get' + }) +} + +// 新增充电桩型号信息 +export function addModel(data) { + return request({ + url: '/pile/model', + method: 'post', + data: data + }) +} + +// 修改充电桩型号信息 +export function updateModel(data) { + return request({ + url: '/pile/model', + method: 'put', + data: data + }) +} + +// 删除充电桩型号信息 +export function delModel(id) { + return request({ + url: '/pile/model/' + id, + method: 'delete' + }) +} diff --git a/jsowell-ui/src/api/pile/sim.js b/jsowell-ui/src/api/pile/sim.js new file mode 100644 index 000000000..79194c565 --- /dev/null +++ b/jsowell-ui/src/api/pile/sim.js @@ -0,0 +1,61 @@ +import request from '@/utils/request' + +// 查询充电桩SIM卡信息列表 +export function listSim(query) { + return request({ + url: '/pile/sim/list', + method: 'get', + params: query + }) +} + +// 后管查询充电桩SIM卡信息列表 +export function getSimInfo(data) { + return request({ + url: '/pile/sim/getSimInfo', + method: 'post', + params: data + }) +} + +// 查询充电桩SIM卡信息详细 +export function getSim(id) { + return request({ + url: '/pile/sim/' + id, + method: 'get' + }) +} + +// 新增充电桩SIM卡信息 +export function addSim(data) { + return request({ + url: '/pile/sim', + method: 'post', + data: data + }) +} + +// 修改充电桩SIM卡信息 +export function updateSim(data) { + return request({ + url: '/pile/sim', + method: 'put', + data: data + }) +} + +// 删除充电桩SIM卡信息 +export function delSim(id) { + return request({ + url: '/pile/sim/' + id, + method: 'delete' + }) +} + +export function simRenew(data) { + return request({ + url: '/pile/sim/simRenew', + method: 'post', + data: data + }) +} diff --git a/jsowell-ui/src/api/pile/station.js b/jsowell-ui/src/api/pile/station.js new file mode 100644 index 000000000..17bde61bf --- /dev/null +++ b/jsowell-ui/src/api/pile/station.js @@ -0,0 +1,74 @@ +import request from "@/utils/request"; +import {parseTime} from "@/utils/common"; + +// 查询充电站信息列表 +export function listStation(query) { + return request({ + url: "/pile/station/list", + method: "get", + params: query, + }); +} + +// 查询充电站信息详细 +export function getStation(id) { + return request({ + url: "/pile/station/" + id, + method: "get", + }); +} + +// 查询充电站信息详细 +export function getStationInfo(stationId) { + return request({ + url: "/pile/station/getStationInfo/" + stationId, + method: "get", + }); +} + +// 根据运营商id 查询充电站列表 +export function getStationListByMerchantId(merchantId) { + return request({ + url: "/pile/station/selectStationListByMerchantId", + method: "post", + data: { + merchantId: merchantId, + }, + }); +} + +// 新增充电站信息 +export function addStation(data) { + return request({ + url: "/pile/station", + method: "post", + data: data, + }); +} + +// 修改充电站信息 +export function updateStation(data) { + console.log("修改充电站信息data", data) + return request({ + url: "/pile/station", + method: "put", + data: data, + }); +} + +// 删除充电站信息 +export function delStation(id) { + return request({ + url: "/pile/station/" + id, + method: "delete", + }); +} + +// 快速建站接口 +export function fastCreateStation(data) { + return request({ + url: "/pile/station/fastCreateStation", + method: "post", + data: data, + }); +} diff --git a/jsowell-ui/src/api/system/config.js b/jsowell-ui/src/api/system/config.js new file mode 100644 index 000000000..a404d8254 --- /dev/null +++ b/jsowell-ui/src/api/system/config.js @@ -0,0 +1,60 @@ +import request from '@/utils/request' + +// 查询参数列表 +export function listConfig(query) { + return request({ + url: '/system/config/list', + method: 'get', + params: query + }) +} + +// 查询参数详细 +export function getConfig(configId) { + return request({ + url: '/system/config/' + configId, + method: 'get' + }) +} + +// 根据参数键名查询参数值 +export function getConfigKey(configKey) { + return request({ + url: '/system/config/configKey/' + configKey, + method: 'get' + }) +} + +// 新增参数配置 +export function addConfig(data) { + return request({ + url: '/system/config', + method: 'post', + data: data + }) +} + +// 修改参数配置 +export function updateConfig(data) { + return request({ + url: '/system/config', + method: 'put', + data: data + }) +} + +// 删除参数配置 +export function delConfig(configId) { + return request({ + url: '/system/config/' + configId, + method: 'delete' + }) +} + +// 刷新参数缓存 +export function refreshCache() { + return request({ + url: '/system/config/refreshCache', + method: 'delete' + }) +} diff --git a/jsowell-ui/src/api/system/dept.js b/jsowell-ui/src/api/system/dept.js new file mode 100644 index 000000000..2804676ff --- /dev/null +++ b/jsowell-ui/src/api/system/dept.js @@ -0,0 +1,68 @@ +import request from '@/utils/request' + +// 查询部门列表 +export function listDept(query) { + return request({ + url: '/system/dept/list', + method: 'get', + params: query + }) +} + +// 查询部门列表(排除节点) +export function listDeptExcludeChild(deptId) { + return request({ + url: '/system/dept/list/exclude/' + deptId, + method: 'get' + }) +} + +// 查询部门详细 +export function getDept(deptId) { + return request({ + url: '/system/dept/' + deptId, + method: 'get' + }) +} + +// 查询部门下拉树结构 +export function treeselect() { + return request({ + url: '/system/dept/treeselect', + method: 'get' + }) +} + +// 根据角色ID查询部门树结构 +export function roleDeptTreeselect(roleId) { + return request({ + url: '/system/dept/roleDeptTreeselect/' + roleId, + method: 'get' + }) +} + +// 新增部门 +export function addDept(data) { + return request({ + url: '/system/dept', + method: 'post', + data: data + }) +} + +// 修改部门 +export function updateDept(data) { + return request({ + url: '/system/dept', + method: 'put', + data: data + }) +} + +// 删除部门 +export function delDept(deptId) { + return request({ + url: '/system/dept/' + deptId, + method: 'delete' + }) +} \ No newline at end of file diff --git a/jsowell-ui/src/api/system/dict/data.js b/jsowell-ui/src/api/system/dict/data.js new file mode 100644 index 000000000..6c9eb79b4 --- /dev/null +++ b/jsowell-ui/src/api/system/dict/data.js @@ -0,0 +1,52 @@ +import request from '@/utils/request' + +// 查询字典数据列表 +export function listData(query) { + return request({ + url: '/system/dict/data/list', + method: 'get', + params: query + }) +} + +// 查询字典数据详细 +export function getData(dictCode) { + return request({ + url: '/system/dict/data/' + dictCode, + method: 'get' + }) +} + +// 根据字典类型查询字典数据信息 +export function getDicts(dictType) { + return request({ + url: '/system/dict/data/type/' + dictType, + method: 'get' + }) +} + +// 新增字典数据 +export function addData(data) { + return request({ + url: '/system/dict/data', + method: 'post', + data: data + }) +} + +// 修改字典数据 +export function updateData(data) { + return request({ + url: '/system/dict/data', + method: 'put', + data: data + }) +} + +// 删除字典数据 +export function delData(dictCode) { + return request({ + url: '/system/dict/data/' + dictCode, + method: 'delete' + }) +} diff --git a/jsowell-ui/src/api/system/dict/type.js b/jsowell-ui/src/api/system/dict/type.js new file mode 100644 index 000000000..a7a6e01fc --- /dev/null +++ b/jsowell-ui/src/api/system/dict/type.js @@ -0,0 +1,60 @@ +import request from '@/utils/request' + +// 查询字典类型列表 +export function listType(query) { + return request({ + url: '/system/dict/type/list', + method: 'get', + params: query + }) +} + +// 查询字典类型详细 +export function getType(dictId) { + return request({ + url: '/system/dict/type/' + dictId, + method: 'get' + }) +} + +// 新增字典类型 +export function addType(data) { + return request({ + url: '/system/dict/type', + method: 'post', + data: data + }) +} + +// 修改字典类型 +export function updateType(data) { + return request({ + url: '/system/dict/type', + method: 'put', + data: data + }) +} + +// 删除字典类型 +export function delType(dictId) { + return request({ + url: '/system/dict/type/' + dictId, + method: 'delete' + }) +} + +// 刷新字典缓存 +export function refreshCache() { + return request({ + url: '/system/dict/type/refreshCache', + method: 'delete' + }) +} + +// 获取字典选择框列表 +export function optionselect() { + return request({ + url: '/system/dict/type/optionselect', + method: 'get' + }) +} \ No newline at end of file diff --git a/jsowell-ui/src/api/system/menu.js b/jsowell-ui/src/api/system/menu.js new file mode 100644 index 000000000..f6415c656 --- /dev/null +++ b/jsowell-ui/src/api/system/menu.js @@ -0,0 +1,60 @@ +import request from '@/utils/request' + +// 查询菜单列表 +export function listMenu(query) { + return request({ + url: '/system/menu/list', + method: 'get', + params: query + }) +} + +// 查询菜单详细 +export function getMenu(menuId) { + return request({ + url: '/system/menu/' + menuId, + method: 'get' + }) +} + +// 查询菜单下拉树结构 +export function treeselect() { + return request({ + url: '/system/menu/treeselect', + method: 'get' + }) +} + +// 根据角色ID查询菜单下拉树结构 +export function roleMenuTreeselect(roleId) { + return request({ + url: '/system/menu/roleMenuTreeselect/' + roleId, + method: 'get' + }) +} + +// 新增菜单 +export function addMenu(data) { + return request({ + url: '/system/menu', + method: 'post', + data: data + }) +} + +// 修改菜单 +export function updateMenu(data) { + return request({ + url: '/system/menu', + method: 'put', + data: data + }) +} + +// 删除菜单 +export function delMenu(menuId) { + return request({ + url: '/system/menu/' + menuId, + method: 'delete' + }) +} \ No newline at end of file diff --git a/jsowell-ui/src/api/system/notice.js b/jsowell-ui/src/api/system/notice.js new file mode 100644 index 000000000..c274ea5ba --- /dev/null +++ b/jsowell-ui/src/api/system/notice.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询公告列表 +export function listNotice(query) { + return request({ + url: '/system/notice/list', + method: 'get', + params: query + }) +} + +// 查询公告详细 +export function getNotice(noticeId) { + return request({ + url: '/system/notice/' + noticeId, + method: 'get' + }) +} + +// 新增公告 +export function addNotice(data) { + return request({ + url: '/system/notice', + method: 'post', + data: data + }) +} + +// 修改公告 +export function updateNotice(data) { + return request({ + url: '/system/notice', + method: 'put', + data: data + }) +} + +// 删除公告 +export function delNotice(noticeId) { + return request({ + url: '/system/notice/' + noticeId, + method: 'delete' + }) +} \ No newline at end of file diff --git a/jsowell-ui/src/api/system/post.js b/jsowell-ui/src/api/system/post.js new file mode 100644 index 000000000..1a8e9ca04 --- /dev/null +++ b/jsowell-ui/src/api/system/post.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询岗位列表 +export function listPost(query) { + return request({ + url: '/system/post/list', + method: 'get', + params: query + }) +} + +// 查询岗位详细 +export function getPost(postId) { + return request({ + url: '/system/post/' + postId, + method: 'get' + }) +} + +// 新增岗位 +export function addPost(data) { + return request({ + url: '/system/post', + method: 'post', + data: data + }) +} + +// 修改岗位 +export function updatePost(data) { + return request({ + url: '/system/post', + method: 'put', + data: data + }) +} + +// 删除岗位 +export function delPost(postId) { + return request({ + url: '/system/post/' + postId, + method: 'delete' + }) +} diff --git a/jsowell-ui/src/api/system/role.js b/jsowell-ui/src/api/system/role.js new file mode 100644 index 000000000..4b455e130 --- /dev/null +++ b/jsowell-ui/src/api/system/role.js @@ -0,0 +1,111 @@ +import request from '@/utils/request' + +// 查询角色列表 +export function listRole(query) { + return request({ + url: '/system/role/list', + method: 'get', + params: query + }) +} + +// 查询角色详细 +export function getRole(roleId) { + return request({ + url: '/system/role/' + roleId, + method: 'get' + }) +} + +// 新增角色 +export function addRole(data) { + return request({ + url: '/system/role', + method: 'post', + data: data + }) +} + +// 修改角色 +export function updateRole(data) { + return request({ + url: '/system/role', + method: 'put', + data: data + }) +} + +// 角色数据权限 +export function dataScope(data) { + return request({ + url: '/system/role/dataScope', + method: 'put', + data: data + }) +} + +// 角色状态修改 +export function changeRoleStatus(roleId, status) { + const data = { + roleId, + status + } + return request({ + url: '/system/role/changeStatus', + method: 'put', + data: data + }) +} + +// 删除角色 +export function delRole(roleId) { + return request({ + url: '/system/role/' + roleId, + method: 'delete' + }) +} + +// 查询角色已授权用户列表 +export function allocatedUserList(query) { + return request({ + url: '/system/role/authUser/allocatedList', + method: 'get', + params: query + }) +} + +// 查询角色未授权用户列表 +export function unallocatedUserList(query) { + return request({ + url: '/system/role/authUser/unallocatedList', + method: 'get', + params: query + }) +} + +// 取消用户授权角色 +export function authUserCancel(data) { + return request({ + url: '/system/role/authUser/cancel', + method: 'put', + data: data + }) +} + +// 批量取消用户授权角色 +export function authUserCancelAll(data) { + return request({ + url: '/system/role/authUser/cancelAll', + method: 'put', + params: data + }) +} + +// 授权用户选择 +export function authUserSelectAll(data) { + return request({ + url: '/system/role/authUser/selectAll', + method: 'put', + params: data + }) +} \ No newline at end of file diff --git a/jsowell-ui/src/api/system/user.js b/jsowell-ui/src/api/system/user.js new file mode 100644 index 000000000..08c317f6b --- /dev/null +++ b/jsowell-ui/src/api/system/user.js @@ -0,0 +1,127 @@ +import request from '@/utils/request' +import { parseStrEmpty } from "@/utils/common"; + +// 查询用户列表 +export function listUser(query) { + return request({ + url: '/system/user/list', + method: 'get', + params: query + }) +} + +// 查询用户详细 +export function getUser(userId) { + return request({ + url: '/system/user/' + parseStrEmpty(userId), + method: 'get' + }) +} + +// 新增用户 +export function addUser(data) { + return request({ + url: '/system/user', + method: 'post', + data: data + }) +} + +// 修改用户 +export function updateUser(data) { + return request({ + url: '/system/user', + method: 'put', + data: data + }) +} + +// 删除用户 +export function delUser(userId) { + return request({ + url: '/system/user/' + userId, + method: 'delete' + }) +} + +// 用户密码重置 +export function resetUserPwd(userId, password) { + const data = { + userId, + password + } + return request({ + url: '/system/user/resetPwd', + method: 'put', + data: data + }) +} + +// 用户状态修改 +export function changeUserStatus(userId, status) { + const data = { + userId, + status + } + return request({ + url: '/system/user/changeStatus', + method: 'put', + data: data + }) +} + +// 查询用户个人信息 +export function getUserProfile() { + return request({ + url: '/system/user/profile', + method: 'get' + }) +} + +// 修改用户个人信息 +export function updateUserProfile(data) { + return request({ + url: '/system/user/profile', + method: 'put', + data: data + }) +} + +// 用户密码重置 +export function updateUserPwd(oldPassword, newPassword) { + const data = { + oldPassword, + newPassword + } + return request({ + url: '/system/user/profile/updatePwd', + method: 'put', + params: data + }) +} + +// 用户头像上传 +export function uploadAvatar(data) { + return request({ + url: '/system/user/profile/avatar', + method: 'post', + data: data + }) +} + +// 查询授权角色 +export function getAuthRole(userId) { + return request({ + url: '/system/user/authRole/' + userId, + method: 'get' + }) +} + +// 保存授权角色 +export function updateAuthRole(data) { + return request({ + url: '/system/user/authRole', + method: 'put', + params: data + }) +} diff --git a/jsowell-ui/src/api/tool/gen.js b/jsowell-ui/src/api/tool/gen.js new file mode 100644 index 000000000..45069278f --- /dev/null +++ b/jsowell-ui/src/api/tool/gen.js @@ -0,0 +1,76 @@ +import request from '@/utils/request' + +// 查询生成表数据 +export function listTable(query) { + return request({ + url: '/tool/gen/list', + method: 'get', + params: query + }) +} +// 查询db数据库列表 +export function listDbTable(query) { + return request({ + url: '/tool/gen/db/list', + method: 'get', + params: query + }) +} + +// 查询表详细信息 +export function getGenTable(tableId) { + return request({ + url: '/tool/gen/' + tableId, + method: 'get' + }) +} + +// 修改代码生成信息 +export function updateGenTable(data) { + return request({ + url: '/tool/gen', + method: 'put', + data: data + }) +} + +// 导入表 +export function importTable(data) { + return request({ + url: '/tool/gen/importTable', + method: 'post', + params: data + }) +} + +// 预览生成代码 +export function previewTable(tableId) { + return request({ + url: '/tool/gen/preview/' + tableId, + method: 'get' + }) +} + +// 删除表数据 +export function delTable(tableId) { + return request({ + url: '/tool/gen/' + tableId, + method: 'delete' + }) +} + +// 生成代码(自定义路径) +export function genCode(tableName) { + return request({ + url: '/tool/gen/genCode/' + tableName, + method: 'get' + }) +} + +// 同步数据库 +export function synchDb(tableName) { + return request({ + url: '/tool/gen/synchDb/' + tableName, + method: 'get' + }) +} diff --git a/jsowell-ui/src/assets/401_images/401.gif b/jsowell-ui/src/assets/401_images/401.gif new file mode 100644 index 0000000000000000000000000000000000000000..cd6e0d9433421b3f29d0ec0c40f755e354728000 GIT binary patch literal 164227 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⋘jgFP=;s&56II*5tLII8*PlN8jx1nb5rMA@;htGb{z% zve2><;}yOubOr{5YaftlUuqy8r|o>kA;qejtE{ueLbnW3n0j!_zLM+XfD^AMUVGvQ zs}P_)1h$TeB5~k*IXhfJOx(AeZ|Q0Ax4h*Cf8H&Bm1`<*A&?!(h;qS1LlJTdyo{4a zhX)~fMUn%q^eHMa)mdlPR{nrfK?BbGyhs-zMc( z+Vp3#A1am_Mfp!_+kYe8{)=kc^L`Wx2Ru$wNSRp9DN4=yA!tFzapx2Qsff0eAn{W8 z&|4>52&Y$Yh__xJudZR7TCMU*BnyeMQ*KzXKl#T4B|mHiKMGz}*oM>(RzwcC?Tl9a zxc6PZ{?mQA>;&!zONQNqVU}Dw%E(jJg{_Ct{)2+U>^roj4~xL@DOV4LD-J?`hm8vC z%Dw0a-E(0Ne4fxddE$=NhSFPe?Q0U&2Tm+>Zsn5H+IWN#ewihgao{!QZZn)35*&>=fAf?}>!NZO63TX0YjSv+I(`#rGCgo6SD zsTnZiUf$EU7es9+yG1w;!$_F&`0m0e@^gakZKs8^6X7Xmie7d=pU%6*4$~u4Txr0ijuSyAHUDzv7)B?hg z23_hVXO`A!bKJx#S0}IoMI)Cy8WVpEEU_x65c37RK3j5+29mR`4f`#G1ES*M5255` zl4TVWzV_Vzupj!@NADMZ`Q>lkOXtOk5OgFz=*6iEZ?PoP8GYQGn;k<9D`C!DaGc{t z$tyiNj9A`l5mT@-2B)4Aja#T`cN*^bdy$~G&mu@$?vJ*X_BEYd+;NK$^tt+ZS;&gH zx~w$c+B2Gj`E`%lOuHj43We@Cj004Ks0AMvH5Y$`iK&6D9p7>?T7ZSS}-jLZX z_HdB6)DW-R&S|Xz1$I(+ycLn_Jy@KuiBPoWh{$w%#LHbws_W^f5%Ik*x2@Y32_847 z=)>AE@kdi5P)KDI3YU|UrIL~MyN38*4}ia;GD!0_FTXIc(`V{YObVYk4`#eSJLcIZzBXj+Cb$?tz!F($d5`e2~FXRh&j{eh8l zTW~Y4ga(v-`P3rpu}_a|_YGWZ<@m5;PY>)0Lf%p3C()-5;d1Pq?R_vB>^^MeQdCHF zab7L&CF{yg44g{BX~ePBylc^kl#47}2<7ky1nwLSf`k&shv=aQAjSz=uOz$TB*4X1 zw*#SlGJqJG?}q7-!1Q;NbEw94VStMORHB%)FczJghsLHzZRhn%D=W80$%~_KwYc?l zIJxQyNHO-;b+-SGxL{wXu`Hg4q`&@fscDciSl08%`<4}TCs=OnAMbQz#LmkhG)0J* zdA*n!hf}RoOidqG7WyrId}}uBJ-w)WFHF4i+$SLYP17_F&hCA;3&`*!K6KDaG+t%* ziLcOr;}^1G#dQsLvSo+X03Q#(;=?&Z1X-!vz*{D(@O8N2v(CdLi&tS`7kK;VJWE2F zVo@OjriBv|+J4xcLlCQY{xS@7#1H4b*z_JOT(MHIN&~NYjUDd0BgpXyk(fC1gJ`gg z+{Zsa3X9$-pP!FGV|k?y3hzpt%~fs{g^qZ?^;dez@X`H2hvdj7*&Y z#^#(IwO-IaOp$zr3KQbQ2@phtGKcc`Xb#GbH{*3;%oR7%yZRz46D*?Er0e?{?s~QF zwfOC3#nLCgF0zIzWZrNSqef1=;}b^gRnqv0Yes~Jk2*s%6p2xB3}Z)%6+p_PFA*jc zAO+KJnOz>X;Yd}y5!BlIBDvxftg~LJW+JjAxq2XgPOGUqtI-IpSv%)-{*Es+@a&%( zXtO^Ytmdg(^RvYSC=h7a?vIFyNlfzfk8=#7(bEs1sOrLKjH)aOS6nEk7>uVTWB^&8 z4MAMBczX#_e4_*NA13*3kHo)53hRHu&Ar`~wV7)taHfm_sTlYvmhTper$8(D@P;8TD9}p24Z4*_I!T~aC?0Uw zlbS^HjrUGWrl(sXBb9iC^YYx4^$ul}GBkD98JVDPOq_HrPJRn^-Tt!H6DlaQzJ1;1 zMr4Z~mVC0W->TO_{`{Y!r}`fhbz)bDoBj4L%TlwmkeVEyfrV`9U8GQ0(%p!<^6RH1>$ac zFf9b03CUlLo}njRp}biqjiJ3}%41^T!}BI=II;jZdd7yMi1=5!>yqo-*>aoieZ4Rz zd2Ow<7!IZWm?c%Gg~8LHsZIz-VXlrIT_wq05RS^Jj~i|JW)~vNquy!1AyX(wcONlK zOcKox6L(k)4fgRLpl;^Hn(k6iSck?2Y9wS;RuLmqgwbX=+~&sZXyHv!)u-At1KjJa z+@y^|+I#zC6vUexaNB$O|NB*)|3)~{a^M$JWIA_KI~$}^TdJLQkh}Tb4N6!VjtE^V zRX}82b|wMYuo8mdEF={g)KW^sA~IkbBE4nCGPwsjg&#?!hb3TQR~=-Q9XhF@@+iLG ze$COf7>zvZI0+ zQi&s6%1}{_AY6`FE1?4FDn8L7VGLV>p@P zlb-#R6+a^Im}H_b+GD3Wub$GvPQtb~q1+r15!^?q078%A*Mx*^=X|#hUbd4vMDQ^; zY-`Zt&1+>V9k+;ln18&V?+$x`_IoVh^COT5rCU~K*U#%-2+d(jpjVqDS)SkX+LG8- z!J19-l$VBZ*$zyw2Bos)*fwO1IM&e`St>17cf6}x?VKwn zrsvNRng4sxk;3TX>7HZ?0;d@vp>qxq&MDq2X9Pc)drTV9KdU+2Lk?4ZCCSw|cMjcH z8ak)jO;FP%i%H@>_KZqK+~&Gr9E&lKe?-Mz)s@ucm!b~wa2EtVjQbdoxAzD3RQh|= zxO(b{2U=x6K8~H&HSqlUJ%OL$u#(V@xP~PLvdl6JufAm-3{9~*UX@uV3ko=`lF4sx zTmx$2_L8oNg@|)86EmO5?%w{;$RE*ATUZ&1+Aou)ijLBvC4z6^GGwzDH6b@|%U(5J6>~JNhEslJ)n-(M6fY*n423C^ z=54G#S*3Dfw_M4oKYyC#e=^Y|Yzc~Sl+caYiQDSfwgHv3n;+dI7pl zI7ZF*GfxMNSzhm6P@ng_HUYO6r&xaCf)-{Bt~x+@w&`jQCi16#Wn##ELgKhxM?{4V zTXOt8NL)R$O4O?lPJMf1bru#0TQ&cpgeNUcY3{&0o|i7RZ613eTrT5(X1qmYSR`%m z`3chD?e+Ixv};GE+yd>wDAJw9&yLswQaG77JeHT7oMLVT6j2$O8`JI6fcjG$73+iv zEnJ$?{F}53|IQ#Oz8D*`0xcn8)O|+X7c7LgbG*8ngUN>=VJ7Kh3O!v+LZVHN@jfsW z%@3O}?D_NPI^cyPkvZnvHwE&^$WgdpKI_RkO&Py~eM# zId*DXoWTq(rJIrLL*@`NCcn~$tI=NCdxh+OHDoSosUO8FuEI`ZjQJ)QAd0A4a-KdB?rrp47%i(x!Tr5s*&Zp&cE zaAyY)47_jT>(?S-)Nbxr)0}m7^p~T;$gI_KXK%hHu4n(W?l3GJHV6a%X>fgZ{0l}O z|3>nVqW`noPMsgu)s0($?u~CG>T=xj5)%Ny9a`5dq<%Q8Wg?LY0Lv>O6Cu?!eXKxO61TqJvg~H4vHD48-H(#qEaCX99 z`euZk3NOdE+Ufu-mcti@1v=5?@iGrvlJ^YB3tHwA&C42qJNB>!XwQY(Dg!#4=g7kX zWiCwpW*dPD_~mjQRDG!SyvXUNBGQ*Ph~7E1qG(;o-PtPDbEA}>KZizoW%l#EJ zpLs0X6Z;VTELFEL=bcUR%&`WZ^5BpS_ggT#1=Y2LTS=9Pjm7b89qXF!nx`yYZ-f}b z*Z=DX2H6g*rARkK#FDZz%uMqN#U0&?NI^<_q{A60!l^a1aI_MC|*XdL^5YKrX+ai6QS(3l}erb1RpG GY5fOuZZib{ literal 0 HcmV?d00001 diff --git a/jsowell-ui/src/assets/404_images/404.png b/jsowell-ui/src/assets/404_images/404.png new file mode 100644 index 0000000000000000000000000000000000000000..3d8e2305cc973ad2121403aee4bf08728f76c461 GIT binary patch literal 98071 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 literal 0 HcmV?d00001 diff --git a/jsowell-ui/src/assets/404_images/404_cloud.png b/jsowell-ui/src/assets/404_images/404_cloud.png new file mode 100644 index 0000000000000000000000000000000000000000..c6281d09013e0a2c5f8e699a0a6038d9480291e5 GIT binary patch literal 4766 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 literal 0 HcmV?d00001 diff --git a/jsowell-ui/src/assets/icons/index.js b/jsowell-ui/src/assets/icons/index.js new file mode 100644 index 000000000..2c6b309c9 --- /dev/null +++ b/jsowell-ui/src/assets/icons/index.js @@ -0,0 +1,9 @@ +import Vue from 'vue' +import SvgIcon from '@/components/SvgIcon'// svg component + +// register globally +Vue.component('svg-icon', SvgIcon) + +const req = require.context('./svg', false, /\.svg$/) +const requireAll = requireContext => requireContext.keys().map(requireContext) +requireAll(req) diff --git a/jsowell-ui/src/assets/icons/svg/404.svg b/jsowell-ui/src/assets/icons/svg/404.svg new file mode 100644 index 000000000..6df50190a --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/404.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/bug.svg b/jsowell-ui/src/assets/icons/svg/bug.svg new file mode 100644 index 000000000..d45ff4fd1 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/bug.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/jsowell-ui/src/assets/icons/svg/build.svg b/jsowell-ui/src/assets/icons/svg/build.svg new file mode 100644 index 000000000..97c468863 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/build.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/button.svg b/jsowell-ui/src/assets/icons/svg/button.svg new file mode 100644 index 000000000..904fddc85 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/button.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/cascader.svg b/jsowell-ui/src/assets/icons/svg/cascader.svg new file mode 100644 index 000000000..e256024f9 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/cascader.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/chargeandpark.svg b/jsowell-ui/src/assets/icons/svg/chargeandpark.svg new file mode 100644 index 000000000..5ffc6eb34 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/chargeandpark.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/jsowell-ui/src/assets/icons/svg/chargingstation.svg b/jsowell-ui/src/assets/icons/svg/chargingstation.svg new file mode 100644 index 000000000..914cfce52 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/chargingstation.svg @@ -0,0 +1,36 @@ + + + + + + diff --git a/jsowell-ui/src/assets/icons/svg/chart.svg b/jsowell-ui/src/assets/icons/svg/chart.svg new file mode 100644 index 000000000..27728fb0b --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/chart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/checkbox.svg b/jsowell-ui/src/assets/icons/svg/checkbox.svg new file mode 100644 index 000000000..013fd3a27 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/checkbox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/clipboard.svg b/jsowell-ui/src/assets/icons/svg/clipboard.svg new file mode 100644 index 000000000..90923ff62 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/clipboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/code.svg b/jsowell-ui/src/assets/icons/svg/code.svg new file mode 100644 index 000000000..ed4d23cf4 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/code.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/color.svg b/jsowell-ui/src/assets/icons/svg/color.svg new file mode 100644 index 000000000..44a81aab1 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/component.svg b/jsowell-ui/src/assets/icons/svg/component.svg new file mode 100644 index 000000000..29c345809 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/component.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/dashboard.svg b/jsowell-ui/src/assets/icons/svg/dashboard.svg new file mode 100644 index 000000000..5317d3702 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/dashboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/date-range.svg b/jsowell-ui/src/assets/icons/svg/date-range.svg new file mode 100644 index 000000000..fda571e70 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/date-range.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/date.svg b/jsowell-ui/src/assets/icons/svg/date.svg new file mode 100644 index 000000000..52dc73eec --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/date.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/dict.svg b/jsowell-ui/src/assets/icons/svg/dict.svg new file mode 100644 index 000000000..484937730 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/dict.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/documentation.svg b/jsowell-ui/src/assets/icons/svg/documentation.svg new file mode 100644 index 000000000..704312289 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/documentation.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/download.svg b/jsowell-ui/src/assets/icons/svg/download.svg new file mode 100644 index 000000000..c89695134 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/drag.svg b/jsowell-ui/src/assets/icons/svg/drag.svg new file mode 100644 index 000000000..4185d3cee --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/drag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/druid.svg b/jsowell-ui/src/assets/icons/svg/druid.svg new file mode 100644 index 000000000..a2b4b4ed2 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/druid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/edit.svg b/jsowell-ui/src/assets/icons/svg/edit.svg new file mode 100644 index 000000000..d26101f29 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/education.svg b/jsowell-ui/src/assets/icons/svg/education.svg new file mode 100644 index 000000000..7bfb01d18 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/education.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/electric.svg b/jsowell-ui/src/assets/icons/svg/electric.svg new file mode 100644 index 000000000..7dc874c08 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/electric.svg @@ -0,0 +1,33 @@ + + + + + + diff --git a/jsowell-ui/src/assets/icons/svg/email.svg b/jsowell-ui/src/assets/icons/svg/email.svg new file mode 100644 index 000000000..74d25e21a --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/email.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/example.svg b/jsowell-ui/src/assets/icons/svg/example.svg new file mode 100644 index 000000000..46f42b532 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/example.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/excel.svg b/jsowell-ui/src/assets/icons/svg/excel.svg new file mode 100644 index 000000000..74d97b802 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/excel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/exit-fullscreen.svg b/jsowell-ui/src/assets/icons/svg/exit-fullscreen.svg new file mode 100644 index 000000000..485c128b6 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/exit-fullscreen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/eye-open.svg b/jsowell-ui/src/assets/icons/svg/eye-open.svg new file mode 100644 index 000000000..88dcc98e6 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/eye-open.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/eye.svg b/jsowell-ui/src/assets/icons/svg/eye.svg new file mode 100644 index 000000000..16ed2d872 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/eye.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/form.svg b/jsowell-ui/src/assets/icons/svg/form.svg new file mode 100644 index 000000000..dcbaa185a --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/form.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/fullscreen.svg b/jsowell-ui/src/assets/icons/svg/fullscreen.svg new file mode 100644 index 000000000..0e86b6fa8 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/fullscreen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/github.svg b/jsowell-ui/src/assets/icons/svg/github.svg new file mode 100644 index 000000000..db0a0d430 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/guide.svg b/jsowell-ui/src/assets/icons/svg/guide.svg new file mode 100644 index 000000000..b27100179 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/guide.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/icon.svg b/jsowell-ui/src/assets/icons/svg/icon.svg new file mode 100644 index 000000000..82be8eeed --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/icons8-bookmark.svg b/jsowell-ui/src/assets/icons/svg/icons8-bookmark.svg new file mode 100644 index 000000000..33c291cf9 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/icons8-bookmark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/input.svg b/jsowell-ui/src/assets/icons/svg/input.svg new file mode 100644 index 000000000..ab91381e6 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/input.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/international.svg b/jsowell-ui/src/assets/icons/svg/international.svg new file mode 100644 index 000000000..e9b56eee2 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/international.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/job.svg b/jsowell-ui/src/assets/icons/svg/job.svg new file mode 100644 index 000000000..2a93a2519 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/job.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/language.svg b/jsowell-ui/src/assets/icons/svg/language.svg new file mode 100644 index 000000000..0082b577a --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/language.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/link.svg b/jsowell-ui/src/assets/icons/svg/link.svg new file mode 100644 index 000000000..48197ba4d --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/list.svg b/jsowell-ui/src/assets/icons/svg/list.svg new file mode 100644 index 000000000..20259eddb --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/list.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/lock.svg b/jsowell-ui/src/assets/icons/svg/lock.svg new file mode 100644 index 000000000..74fee543d --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/log.svg b/jsowell-ui/src/assets/icons/svg/log.svg new file mode 100644 index 000000000..d879d33b6 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/log.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/logininfor.svg b/jsowell-ui/src/assets/icons/svg/logininfor.svg new file mode 100644 index 000000000..267f84474 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/logininfor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/merchant.svg b/jsowell-ui/src/assets/icons/svg/merchant.svg new file mode 100644 index 000000000..fe6ef90e6 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/merchant.svg @@ -0,0 +1,23 @@ + + + + + + diff --git a/jsowell-ui/src/assets/icons/svg/message.svg b/jsowell-ui/src/assets/icons/svg/message.svg new file mode 100644 index 000000000..14ca81728 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/message.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/money.svg b/jsowell-ui/src/assets/icons/svg/money.svg new file mode 100644 index 000000000..c1580de10 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/money.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/monitor.svg b/jsowell-ui/src/assets/icons/svg/monitor.svg new file mode 100644 index 000000000..bc308cb0f --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/monitor.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/nested.svg b/jsowell-ui/src/assets/icons/svg/nested.svg new file mode 100644 index 000000000..06713a86c --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/nested.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/number.svg b/jsowell-ui/src/assets/icons/svg/number.svg new file mode 100644 index 000000000..ad5ce9af2 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/number.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/online.svg b/jsowell-ui/src/assets/icons/svg/online.svg new file mode 100644 index 000000000..330a20293 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/online.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/password.svg b/jsowell-ui/src/assets/icons/svg/password.svg new file mode 100644 index 000000000..6c64defe3 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/password.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/pdf.svg b/jsowell-ui/src/assets/icons/svg/pdf.svg new file mode 100644 index 000000000..957aa0cc3 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/pdf.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/people.svg b/jsowell-ui/src/assets/icons/svg/people.svg new file mode 100644 index 000000000..2bd54aeb7 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/people.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/peoples.svg b/jsowell-ui/src/assets/icons/svg/peoples.svg new file mode 100644 index 000000000..aab852e52 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/peoples.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/phone.svg b/jsowell-ui/src/assets/icons/svg/phone.svg new file mode 100644 index 000000000..ab8e8c4e5 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/phone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/post.svg b/jsowell-ui/src/assets/icons/svg/post.svg new file mode 100644 index 000000000..2922c613b --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/post.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/qq.svg b/jsowell-ui/src/assets/icons/svg/qq.svg new file mode 100644 index 000000000..ee13d4ec2 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/qq.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/question.svg b/jsowell-ui/src/assets/icons/svg/question.svg new file mode 100644 index 000000000..cf75bd4be --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/question.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/radio.svg b/jsowell-ui/src/assets/icons/svg/radio.svg new file mode 100644 index 000000000..0cde34521 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/radio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/rate.svg b/jsowell-ui/src/assets/icons/svg/rate.svg new file mode 100644 index 000000000..aa3b14d7d --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/rate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/redis-list.svg b/jsowell-ui/src/assets/icons/svg/redis-list.svg new file mode 100644 index 000000000..98a15b2a6 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/redis-list.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/redis.svg b/jsowell-ui/src/assets/icons/svg/redis.svg new file mode 100644 index 000000000..2f1d62dfc --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/redis.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/row.svg b/jsowell-ui/src/assets/icons/svg/row.svg new file mode 100644 index 000000000..078099222 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/row.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/search.svg b/jsowell-ui/src/assets/icons/svg/search.svg new file mode 100644 index 000000000..84233ddaa --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/select.svg b/jsowell-ui/src/assets/icons/svg/select.svg new file mode 100644 index 000000000..d6283828b --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/select.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/server.svg b/jsowell-ui/src/assets/icons/svg/server.svg new file mode 100644 index 000000000..ca37b001e --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/server.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/shopping.svg b/jsowell-ui/src/assets/icons/svg/shopping.svg new file mode 100644 index 000000000..87513e7c5 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/shopping.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/size.svg b/jsowell-ui/src/assets/icons/svg/size.svg new file mode 100644 index 000000000..ddb25b8d5 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/size.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/skill.svg b/jsowell-ui/src/assets/icons/svg/skill.svg new file mode 100644 index 000000000..a3b731218 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/skill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/slider.svg b/jsowell-ui/src/assets/icons/svg/slider.svg new file mode 100644 index 000000000..fbe4f39f0 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/slider.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/star.svg b/jsowell-ui/src/assets/icons/svg/star.svg new file mode 100644 index 000000000..6cf86e66a --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/swagger.svg b/jsowell-ui/src/assets/icons/svg/swagger.svg new file mode 100644 index 000000000..05d4e7bce --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/swagger.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/switch.svg b/jsowell-ui/src/assets/icons/svg/switch.svg new file mode 100644 index 000000000..0ba61e38d --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/switch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/system.svg b/jsowell-ui/src/assets/icons/svg/system.svg new file mode 100644 index 000000000..dba28cf6f --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/system.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/tab.svg b/jsowell-ui/src/assets/icons/svg/tab.svg new file mode 100644 index 000000000..b4b48e480 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/tab.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/table.svg b/jsowell-ui/src/assets/icons/svg/table.svg new file mode 100644 index 000000000..0e3dc9dea --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/textarea.svg b/jsowell-ui/src/assets/icons/svg/textarea.svg new file mode 100644 index 000000000..2709f292e --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/textarea.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/theme.svg b/jsowell-ui/src/assets/icons/svg/theme.svg new file mode 100644 index 000000000..5982a2f78 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/theme.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/time-range.svg b/jsowell-ui/src/assets/icons/svg/time-range.svg new file mode 100644 index 000000000..13c1202bd --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/time-range.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/time.svg b/jsowell-ui/src/assets/icons/svg/time.svg new file mode 100644 index 000000000..b376e32a6 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/time.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/tool.svg b/jsowell-ui/src/assets/icons/svg/tool.svg new file mode 100644 index 000000000..c813067ef --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/tool.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/toolsbox.svg b/jsowell-ui/src/assets/icons/svg/toolsbox.svg new file mode 100644 index 000000000..24047858f --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/toolsbox.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/jsowell-ui/src/assets/icons/svg/tree-table.svg b/jsowell-ui/src/assets/icons/svg/tree-table.svg new file mode 100644 index 000000000..8aafdb829 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/tree-table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/tree.svg b/jsowell-ui/src/assets/icons/svg/tree.svg new file mode 100644 index 000000000..dd4b7dd22 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/tree.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/upload.svg b/jsowell-ui/src/assets/icons/svg/upload.svg new file mode 100644 index 000000000..bae49c0a5 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/user.svg b/jsowell-ui/src/assets/icons/svg/user.svg new file mode 100644 index 000000000..0ba0716a6 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/validCode.svg b/jsowell-ui/src/assets/icons/svg/validCode.svg new file mode 100644 index 000000000..cfb10214c --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/validCode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/wechat.svg b/jsowell-ui/src/assets/icons/svg/wechat.svg new file mode 100644 index 000000000..c586e5511 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/wechat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svg/zip.svg b/jsowell-ui/src/assets/icons/svg/zip.svg new file mode 100644 index 000000000..f806fc482 --- /dev/null +++ b/jsowell-ui/src/assets/icons/svg/zip.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jsowell-ui/src/assets/icons/svgo.yml b/jsowell-ui/src/assets/icons/svgo.yml new file mode 100644 index 000000000..d11906aec --- /dev/null +++ b/jsowell-ui/src/assets/icons/svgo.yml @@ -0,0 +1,22 @@ +# replace default config + +# multipass: true +# full: true + +plugins: + + # - name + # + # or: + # - name: false + # - name: true + # + # or: + # - name: + # param1: 1 + # param2: 2 + +- removeAttrs: + attrs: + - 'fill' + - 'fill-rule' diff --git a/jsowell-ui/src/assets/images/dark.svg b/jsowell-ui/src/assets/images/dark.svg new file mode 100644 index 000000000..f646bd7ea --- /dev/null +++ b/jsowell-ui/src/assets/images/dark.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jsowell-ui/src/assets/images/dingdan.png b/jsowell-ui/src/assets/images/dingdan.png new file mode 100644 index 0000000000000000000000000000000000000000..f52bfa0447bf316caa7ba5943c7fb941c770e198 GIT binary patch literal 3277 zcmds4X;4#H77iLEY$DJQCLNZD+BFzaDChy2Cd!VWs0|pDB@Nn$EMYMa76}FjU?9L? zv&bejDs}}SAQ@u9(yt-JHVOir&^brU8&$(-sd+oMvWfp`S5f4h#%DKB@+A_eN&TN(F`E{DpwP zm+!WLOx30=`2t-2V~yjj&9mFB>3Ysbkv@SZaB0egTbgzU0sH$WI+c?-oDRJ0Q6gn~ zQP3pqQ>p6VtMrtJD#1y(Q7DxJlXD&EN&b{PjJvrc;+Fn=YhRcL z2C&Z{b2n$Erltmd8n{#6z$Cv&K|8Bg_qf)PiK|vI<9z;Ns{Z^_Nl*15svowd5j60* zJ~K_iuzq8cOyigR78J>$I8l@i;%*3 z>y`;Pv&>9OOzbGw0nPuuISu=nFK}GJ0V$Xi4?UV zRJO3!a>5Gm{1VZYp<}+>6i&agj=^_vLDG0gFXxFEau(iRyt)cbo$~Q-OAq4E-zK4c zi=~Q$U#pA3)Nx#dVjU!<{dQ5_t)sE_rNR4TP3j zAN;I4Qx6+6)<#h9K?-_g_eR8H#Y8SY<#YO~15FjFv33pnD}U)LKDsaTKvNi}g0LEI zlBu6SMkbJ(t#9gH@@l>S;$pO=_O`&J|NHJrPP9SJ&WP>%a4%}{PKnHZZDO~lqq;OF zOlvU?Dzo`4LY%y2e%(J#$&yrbL(?RpO2GQQG)HQIym7+7 zi=~ux?mw1bTyfc$QJg(Xe(gR5jB<@vc$&mVi8*OR+5IF+m@P8rCilX{WnP2Y_Eq0DmI!Gh6`o1B^TbO;I(aE<`xH=}@mT#^25BBqU( zo4kYRlzH?3;Ks_z&5rBJetfzY6hRnkO`PlR5K%2ySmu}#M5$SVV+*D^LD~$pP4W5 zkZ+k~4UM4B)dn`7*kFZAC-=K=fN^9Bk1d5pC7?z z!!_d7{4oGE@Y6Lz`M0ssPMP5}xgq@PiZdl~VFfQUgYD;-I#j>wPz84-AQnJXsE&{VmmA^C*UwMSbCRK!O*9&AKL(gv$G`q6MpB&23KH1N zL;X^KUSU)wT{Vsxj(f#}`pQhTLpo*j@!(@q*%Tj^x|)FuB17Hx49+YfoVEbW595oV zw)_ZYBQKWQCv$uMB6q{A5rsHg*GSv<((iM4Ul8IGA1422v+>`Aa~;w=IAxRy_|VoT zTu8z6l@Ox!aazH>3LYnew`lIjZqG|Sc07|!D>T7%7Gh#mjUC7fZ(Z^CW>5|8It{g< zh|${!v2}4=r#Yq7TJ*cDqbCJG8Z#EQI5*2ACy|W1OE0TN&d&_hLn=6?GY@lqS|*ci zX`48<=YM-ek;z65q7?OVL|jd!#0o za2pE~d1Dpz$2}H@$>FP3l7OJ&CFSP$HZkH-dp^aWduR1zBd&Mfb~vR_Zr`|!5N&wZ oac!=#GPXSbJBt2~4+Uv$>KopI>x0>Y&@%<*>3Ytk(TPC)C&}30!~g&Q literal 0 HcmV?d00001 diff --git a/jsowell-ui/src/assets/images/light.svg b/jsowell-ui/src/assets/images/light.svg new file mode 100644 index 000000000..ab7cc088f --- /dev/null +++ b/jsowell-ui/src/assets/images/light.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jsowell-ui/src/assets/images/lightning.png b/jsowell-ui/src/assets/images/lightning.png new file mode 100644 index 0000000000000000000000000000000000000000..7252e48f74bb38ea395b28c96dfddeed0d44ce98 GIT binary patch literal 5672 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 literal 0 HcmV?d00001 diff --git a/jsowell-ui/src/assets/images/login-background.jpg b/jsowell-ui/src/assets/images/login-background.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8a89eb8291d5cb7d9f37ec4f275deab911c9e28e GIT binary patch literal 521275 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¥}>uauIoY!`8n(7niSl_m2d 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

LZs5vs<4*dn)6_orH;Y;`bzi*q=#N#x6 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

8A%Us3k< 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𝔫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

&!CIz1Ns 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⩛TtXwH2N8q zQ}n(0>F0RN227W9%@2Qk&Xj3t?0afJbuDz^CK#Jb=GXp(cm&(NdJ9m5#dO@!?s8ip zF9y$M_$l6f;FmcY31Ej^_3&timWoPOf;$OOE#7Bhy-DuJRD6N4+(+ck?n4RFk&OtP zQxN8sg@*65BcwIx^2ni;?k`3?si;1alGNYn>mEkt+1Yz#JLZHbkeeky0{8(Ju|98;bZf{e>t?w2Ax?SO&@yy2 z@dGGJ8P9Au0OR982w(kNEp_rWghHTrMnDfu%deJQATHjU^|p-6yxT ze^+kxzBQ*w^mn4WRP_&j?>@y|@<46mmlUsb9OY>qUYWh8#mq#121|DCasv+BI&-?S zlM)|Tyu|}4dFEosW|IJjanQUXV?Fk-1B~o`)HoQpN9qj)ZyUKAX{{xXN{WkXT^g2Z zu(larg~ci^o!wu2G<6gia%`lBuFZO|=on-_rYM|xZK~&|_xM)lGjWC- zsGU42nQG-3Gf2ia7at=F5l42E9C|u0OfWxHC}N!1_@iL9MmHME&(PGr->h7cV5$r*X1 z5ljzRX#*}AfT2T~%&0hg>V+m*AhZeX?cN|UWE7bI=Xjo_O+RMWyFJkhbRM9Pmj2Er zRk%oAyV*pGPRkVlX=~5CA>+|0{{tYzq;ChWyx^^u9A5?kEK-ZDm@GPiZe10d!;))! z&T&1Q56+2fdbmhi4bkAG# zS+sh72+XPaiD2o{#bP-1X@+=ouHYv-#fJDXiSMSY?AixaP^XV4M*}pexyER7d3QFY zJhOq%@^t&-gr7DNcePxTPVQY}11Bx1)0X_tUhGhfUVaHoiE-263^oy8JY^_Ov%brf z+~WaV_lwr<@5r@x9RgKC@nk6>Ci*YQs5A1stDeq5!c9UAwrm22li4`iq)#UA>tVn2$Ybq;GC?~#KLHF0f%{#Xg_^=#CBQW`RV3wdAknH+knl9X1o~k3) z%HkKn$2%DoV^6n7B%Y=Xc~#QdC5SsNewKFtsD=S9-&qbz*j8O?sA_zeXJ#<%$q87_ z3Y;l_m*llSXWh?~v~sJgRv8F%*m>U)C#Vwmd}m3}8^|F+gYoVv zh8J@sx5<^!;Q7YyZ}PsKF$`cjZaSsa9P#mWg77=b^!su5Wqgxwm>~d;;b>|~y^#hb zoL8cbo}AU+o%NJqV5T)hY|AeQ zl6R89%yjjk@QcH~64k*Eh^Kyz|1+zek=Z0_K73dsz!IIee6mk!!fp;2tT-gv;9$JV zdg^sqNjRJki`OIv~9p1@2Om8UkDr8SIG^s~&= zL?my07m8uC5WaqhaJ^f|poF$erwFN*d8MB76X6FvcUiAv(V(NdJU%mvC4sxMIVSm4 z<&`LDph8>_Qi7B*>~3$g{`x&mt-6p@euFJxFtTlKSN1!p`8;o{_57Wmg%XhAJh6N+9P7Y7P%8!7TVHJ|ql)QFQYyV5bcw;K zw0WseFooe6Q{1)oiT9b`MvyRNO{VyS?>=v6v4Mb4%A)Fal5s{++Vn%Eh!}~qXR_ot z&aFD(0(4he=lvSNZOW}4m$F8ob~i}o@@B{@^&nH&uA^xZ<$x}>zk?IN=29L8%Nx?v zKiluCgQl)MDv9{$iqYsE74qRR_Yyb(-42%9ZHsk`^OMXgQbuGk?TJLHEXv8SC$6() zqiopvmN;>Q({f!As)wp72kElAV>_zP(siz|T|@K0^WKLvuTax95B@a@7W>9oJ{%q# z$+%DtQhGU~<@(A192F6Tv+r|B6-Qd6-7U^=2z0AtWHKB;G6)GvFKCmF{g3(K&({{h zm@D70dMNt)#W%mM@-{iBl%ivN5q%S%o~)Vz^P!Kshr^BgNowY-kgq@JO@#anW}O?O zvbBl@GWnLW#XH#a^UyQ= zfX_#ha;D~=vu}WIv8tpYQBWnxW_2BfZ-;olqV}<=NYy2?^%981+pb)zq}`$dL@maE z3$G(#%n}^z_nln9^IlgZE_2863x%1pn^NZ9DW?wLWs36)`|=e^ZYQw22o!qX*5cU z-Wh4rH9Rz|m6rS;Kn53SITk4rGdAiwp&!2X)MxbQ(*9Q-=wEqN+;n5<^Rt;4L6Ptr z{(E54S(%X(10Bb5%M}I(Uy|FUKj@lo>6puEPKr~BS>6zvU{qMqXWqThWj}OjW zlKDYx+%GNSrS3G{Sjl>6H4sQlpXZZ{$Swg zs0z84!v{CSE3={h7(GPstBugBxOJjA3Y1Y)I0_u^xOr9}D);8S0MTsZE7k6Fhk0#9 zXx_9vT=nX%yHA{s>$Hv?S+uVMZjzvpH=;FCSf4C39e(MNxvP~;jaa{y!hwo3#RcPv zQ$_fN-|}ljg>nB6Fa`$7N9yk35M2-Mv5B>>ulqk(84TU+U@WRlY)IZ0D;P;FejS?Z zo}(Tl#a!l|z@G8b9+2D?C0P`CdyJz}%U#n%bMpCj*dybxH%yTcZL!I-LSDqKTA|Q8 zT-1`(f<>**s8KXD;@m=vxki42Yz*S!BHUZw)kcSTzOXMcsP&*}%P7|QQk= z)Vmjt%=05fokaCXAU0>YLJ49@2JwbX+RDF^DWW?+czAY-nvlc_QFQ&I9B@>O|HJb* z1s$bCu2Hh6vdqGgX=ydT`vg*K!w$+6)9dY44Q_A*-7Wurr{Z9Kl*K(z7?>9hD|wy&uY)NSd7i&l&SmEejw)aXCwNy#TvuO0PH-}$IS={)b|QxDl7-QE(7p+-x<(^N~( z{vUn*_Hptkj5or*h5M|3_NR!|#7Ln{u6bI>&i2_bZb-M1`uCahn=H<=jDKU}G8Mv9gBsCZ`~6Llox0O}*t=<7wIWt6{PF0h8oTJFg{kjUWb2J+XlhFmNs3w)3Aq4jX z)I$C@Ilb^)kpKB1bi(H4x3(LYQJ=pfKXu~L1h<3fPjW;SDE66ey!a#8b=e}2AU8Q} z-3M|&v}F4Fr%PUstcTCiO>+n8aua-y2Fe{u%eQsN|W$TKz4!B%sP`CqSF zSHMXv5Ig9t8avPH{oT>D)!xDbi7O56a57|*%q8g|+~KYCTZf&OK0Q4T)I0s1`;Mp@ z`I-P0WdPHw1^TrNqJ5>XM;B zN-JXlJC~!{-I4=%-DtZ3b;7B! z`lJ~~a**)1N=2Gp2AFgy`VWF(X|#|l)-wH52Uh?8`i4TP9BYU^=( z0oWy5|39yX-?G0TuJ(*@xD<46!(KfM^j3dAkJr0!X`(6lHl2~t`;{_sA#Ihb@< zVqCM6wV{VG9nFgq>=M=H5<ep&VB*moyCU}*C?Nbh_jqn~)$G5a9;3t}Pl8*^)2r=~ zM?D%ktN}Bu__ZRI>IwAI3>P=*~chalp z*=81cl`15sHMlB$Xu>7uA+5r0g0p_&b&= zcB?zc)UA2J^!$<{ofQIbULZG9FYfJ%{`$rZpfH#eagtj zY{;ElnBt)ip{5G?w|L<(OAE*%dXe2Wrz$=ViBp`!_?w1=E33f`LYn$wb!oB?O#q-a zA+rj;bfdOr<7)fojd(=TX;s53&deEO;2OS!5G|Mkkxl2mZW$Q|vu+*1Z`VvZu&D-C zRc-<5L(dwkf*KC{+O(DNqvZeG)W!RH{M$uyMJ%03AP0qT2DMqz%}syHV%x6WgLP4d ztY@X4`=~=30>^^QZKd>S&#k=R_4mc1k!lLp)oFq6N5*9;^`olvn3mWSLe~>>-W;ob zZzh%4I#Vdf!G$Fw%;3ley9HElsTYHEVpjfK?=rB_6#2L&eM)=ZR)H7>we^H@5QKQI zrNS?yQD_=ET{{((>(7a2LI z_yPS==R^yP*jGTuJu!6Kv5IY4sEn+71nXeEGLakU4A7OATqo;eVyc|eo_(n;mDbjO z8oC0jVrBoBRD*15vwoUamgrLOIF&Gv#-t6VRnDGbK;p*^K9JLDJRE`>b$Ks%aZ(B= z`}`6uH!Isk0gowm>7qR}WTY6T%m#@7RwF-7yr?R1n*tWkF$kJy#XRIME)r5AW&mS$ zUOg1wgER62FjY$%R0r1~a&x`;c|mug9#7~1!*qOLsGIf?ECM0TMJK-sa`dyxLPYs3 zF`}*{F(V=Re3+D}0gYL)D8SuIw)4s21zg0$>8&oE2pw_hJf#0Bo4>0H1uerU&}6zA ziTdyJ3^F!8f>FOHG~O%4;c#W;rNwb+#aEq8=JvB!4K9;5{BMxwv>Q`{%_dG7HF;%8 z$HBkt<(PeD^WLX=5^TPKXn#txCExkT)Ih%L<{fP>z_YL4^E z4Z}DCN^4pL=744^3p*H5<5DvoPeXpkk7ZY|JZ-%IQF zmgjZvymUgCJ#(CKm3fFT1!$Ge6+T|@LH%iBsVOmw7_8?$>3x2kH#uk_pXmGzILqMtguOVu`xb~Z&k8oX_!ZCVdYEGQjb z+lT2TH`Q{wy!1}atEK2t|K-9cEO@geK8`p3?EFiBZ}ux~W%{V>o(P256UicYnV@V6 zJSnr&zv=`3GmVvQC>@bXqH~EXlj-y~b#>ARAs6+KCoHV*DuFN7<1N1FjgJD3>nsnF zV+^JgpcyIniteVG=NFKLCr0@+f-yHY_Bm9)m9MX#_##zwyj9U# zELi6-cU>?!KX&iqnr3yb<)a)KFOIKL^@VoA{V%&nTw1Ca&WSHCoDr zbqDeylLXpT70pcXW8%dZ=Jue_-pg5hf1(Q?Z{EJ8d%z>?&7+=HGvO$s_0?{i)LeYI zkh0T=pd%RfDA@6N8A;kbT)wtTr{$=uuWK-_D2b%ax~bvj62v(3b)49^S_pk)|4M(9 zl-^OyCEFbflYAyNR#m(f+I{`y!fuL@s)JrtwIzOO!uzU)^w0;>C(*%#`DadSG(RJ< z(df8o;!0S;ShGmgH7Y$px|*CU%kdG;lmomzx?%B=-H4YVg2ojcM~|jj&5K|)fh%wWH2$| zn5t0Tk9Nny%X3`)2b;h9GA&LZKjGRY5J4Ev@5d7sVOpYKCkrM9pe*=l^mTSYlG6WkLl&wrDQURopFbX*TEjFf%%IW6yrrSW%}rX7OJ z|3m|q{Lha>mX#PG5=d$hNU3ic15Ob1>i3gb4E@H{ayq!Iw(5Pmx4z(dZ{>i#E{Kr{ zYwnn8bE(xjJiNln=4u!IV>7g5_I9gQxjkJ9N4dnak4SJCb8=H^=k)&o%f}~iYuDig z=Cr01m6A!hf(J)WIqY*l5-MQ$t;UuRv#np)X~a*Nyf-ajVq}V>f+A<2?L&aK4mt6ludLt>wA~;6A?48!g7T zII-TOWdUx0SmjldhPG(GxaY)!S~KP`x&~H-&QV{BW|N@H^#8}vc|WrGzHK-m_TI!M z_Gl@J+Qc3~jM}>rr6{evXUy2r+AFasHCnAbs%ix-9cngo*sb|_^L_t-{Fdi=?)$pV z^EeiuX?<$I-%V`yh9Gu4YN>V?X$x}V=0Dk6e7gs^8d}8fxNcSCsUjc^dBYFnds1|` zPxh9vJc#R;7qXQPHJATB4d;K^MVh6&nEcr3V)6ayW41ibuMh@dH!u?_pLnQOX2V`e z;0a+0Lt;xe{EiMNREv(G^(g+qBA@GvHjW3jXetU9xLuc3 z9GITCCwx4zJG5Mil1RIE`_KvacpqkiA_pe7DsLVB(IbJQofb8xulfe}4tcHm|BW`b zk%YmRh6_nk3xifbWi&{EJe%T^6f0#=t_ecy{>80--;Sbmy)A$f)7e`5+f)0NlfHun zwrnSLRHS_2IAtCSetEzXrun?;g8nia`6fBKN-3w9L^n8zRtNFgVt*N9W7`v?L(+m6 z%pZ*{@7fPkdR77q70HkZX76*)T&Hfa%WnK`M9`!?M%X*c0wjb50dKR6>&9QSz@OdR zx*4tT#(n41rWnZ5Z9sqeEi&6iD* z^b{T)y|#J>zmG~5pkaDKk@0OU1$saR#2%mXYaWn}yg%5ttYF)U(_zzz`y{YV%#a(sUF{g2nd994a}66JO-7!I?3S6vA|_vt!1C6M*eNg2PaS0OV5^m;@! z4cbsN4VnFQFgK&<%G3< zl0J=X_c%HI;~QOF^W^AKnl_UFpvV0C6Sht$Bx5Jbt-diH>0wnF#`7YOzEazdQI;<+ zBa|hRv6e-*Ov}xu+?ngXxwX$gatWJ{)nGX_aJHDmD(VNYUTXf)TG9kPGL!=uS`!K! zwx{}h$8ljyI+lN>T)o5q#f3U%m%$Ti}rFLIn!H2sPAtN9BrY_6}r!f}F;$dq?btW^d# z5z_OAZ?%ccXOLp$+Vw?K5tWEmD??j}X7N9Y`{j+e8kjJOll5JP&^Vtaq(i`i6e-YO zdH3cuF_#kh6RU$)XyZ43?h`l`NBzcF!gw*;Z(?Po;T8|7_x0@}D&ba4v4D^L+BEAi zq~I&X1K=kC&@%>$yL@N0?D(%q45#5+p~0DI+|hn@?>btLQkJ^A+T)2ixw95<{0W@v z{`;jDCcN_2EFsc9uhHW}M#4_|jGT{&%juTwNeP|cJ7?kiJ;Vt_f3h&T2Eo0~gY-im za|%6=c^`5sqz|Rm9nw-=X-eT%_)u; z_!-|ng92GPS43QdtME9fi}7z$RDqQ;GrjW7kj3=&pU3xYn8i;z>5tyBNg|O7yIcV3 z{KQ<3h7V7BYkvK@toW6VEq4$Lab3(`|&gzGsV;_uV+_F7*5; zc#4cOgaNILl&fX~tE7x&=A;>y|b-RGkaj?~~2u=(o+3{I6K^AE3*C&%WwB5c?m%qo(Pzk^Jj?J>T(~Sy0Yx zWip%!+l*TOL_Qs@k>De~LOUPX?JxLM<&OoSR~2T-H!m+PqX6&Ly?vu4hU*h*7IPeP zoA2IEl!KFWLv~*Q&K7eD$t5f&u=wmz(?t{N%K8-j_N?bMf`Q2?Rc?l$I12pnBZP0L zP1I$K7I8RICRN_JZJ2<-75q!dx7nb0&!2S8Vx1?o5c2{fs&k9;ignp#Zy^Q2>awFl zok08re_MAZiII(abA<~J0Ki|&a$cTm`29*~Ft-t)4{nseGJg(3*YdyM6_v}oy~KRm zhc8jVm`WVgB%I&;>px!nX=sUNXVXFzdo`8Z*g#=R0N`xx4hOBat#Diebxvmi=jm*@YFKjiT{xRt2r38FZ0<|daxWet(NL)R z9oE??zb97^OrswP757IP&9`*F$&ZYaR=KzgG);sO>(J-n5pEHO1nC7QwcsMHI62&r zzgC>1ku(iLtdCg5%Z^CTP0n2$&)}%7QL$>KmPd-PB*t3hStu!8IId+6rrvU}D*QmX zA$P`OCzcX{*vO-5@vkNA$G0|c!$u9OM z<9qRGOM#=qGmIDNT4TcvujO5omlf?l7QziqZk{om&a$DJL*{aU{0nTS$}?mJ zEK9XUHLo6X$?ygxNUg>rsTQ@&^?ea%CnI;u?MX-uAs^iGjc*I-k&EeaMQXf4if-LN zCkEKkWre+SJ1i}&tuc(zfw$!r!s}k$Ht$G1sB`D4X4$UABWsYf?+qKu$SKSYZ?mY< z$par#b|Jka-|eyihMjZ7zq?^o1i5qCQ@3c=wBElb7*G)rn8UIVQsAO~j#CB>4hh9jY6FW0!v8!w6jz-lE!up0rC( zjMH?)sPyqvj14Sc<{j)&%YAin+N%r<2brObYm>ZmCLK`?;2nAhq@hF zo!L{Q{JeL{JUPgN#4EzF;P$S@gE5xS+tn|OtCX-&JXmioME z0j6@*VskUZczL(R8z%~VIfvR#M`*Yd&dpHLJl~YX_BXYo5v+httGgy7?%SeLVf_=5 zyOg{o`^-&HNxBC>6Y3QUw7qko7q1Tc(7wrZBwsLl$BMevSt1!6cL!r)^>e@B zn(Du<+lbQ1U@CGMy*tW2N|uBx{?eZ~u~K#BYD#_Z|9nRMbOjW@BT=H2v#X^+%;s0L zTEC!si)LSVJBpoY!)35xPvr)lB|d8C@%&d9wm7?(%5WN8#`gJjg11(sK`kZ-vXU-v zP8&Eojb*&|&z&;mP}c?|Wk`F!CFpeLKS2DW zCjzL`pAw6@wG}SC9g^d_iGJuuLvKko&NTlBe~HRzb5y1?&aSs28UH2h3pQa8=K$hA zeomlH2qVfQH@_uXiwQT?(Z7spxl>Wna513q;kj3~-KdGwK2R)v>uHg?D!j`#x5l}f z&Q<##hsIv0A1Cp&WfKv^h2dIC69(oY#y5PI8IROVSR0xnw& z=Ce61v*n*VFuE!Klty8xPm<;OE-^*(6Cq%O%!Cz*b%+xW-hMp6_51WZgso-Jge7!= zdHI6zQS1+5*mULWkV{<*hG&gJ0p!kC7O%_u*gN+j38MuI(zi}JH0Qn@{vNKeFx4<~hYdY~|&)ff87 zsW~8X0qYheL?&gNkZBews7dE!oA6qdIk+z<*9lK9P9T?^8?i@vV9M+H7sa14hsiYO z2P))+QT{R)E>q%CS3{#I2jOpfv@Vx^G^ zMI&I?sr5JU{qKqr+4T`7GFBZ2#@UZNvG-(tPRn<=pMtJ6%I5 zs|(F`v3-}@I2#*koD(HUBurH*aI^i;$5F}vkbf|Sy=k&<7NzXn-(fkHv%5Aw+4*6iDPgF< z3&u=_Z~>I7sUzkV{m}mYU0dV{a5m424GXz?^WfXgJlb6GmTs0 zedfRUqP11SnK4ttliT#N+iN3rYz`6+BiBLWa96_Mm0Ln(Z5sU?}W zK2N;h+{CI{&ti`RhVR8a5a@;~JTJ4ETsI`0!T3H!;sLo!Dj9*)^qaeEJ%SKSsD;hTk9lS5nk&*^qxN6X0;BY2m+I z(bI%BVB^u3yE;5S=^XS&?V(C$bO+d^2=e{U+G!usu;47IyIW&FUcOBlm;X&Y;d<)X zBs{86R2rpJrJ#7uFnNHR&;@Zucj)rK&Lts2&|~9J66|{pJ2}qUC&Y;P*J_Q_uL~wH zAocfN5@>7%IBJl1Q7FEf%}Xo>J4Iii zv7}+2vX%+NP4d7X=wlN@ThC~RxE0uylG-*Q^<8yWy`&$(DWvSd{p_02GR68L-cNOM zJ6bK0Zyy*p$_#Wi&NCg$i%X9Ev9j{l=$;!LRE)@B@6L#IeWbU%6nj`q%1MJ-R2Vb( zP?^6V(dy}wWveeqAIOSs=!KX6{&@hhBW0uQ`?enT_UPdq>K{{w(|6u&*V=frVc(-` zMOU_Ho+bWT$>#jpG5)(7#4nu*<6o=TUi$qg&WQQr6Al~2RzwO8iTGzmCr|x+MZCKz zuk}7g;i*hri2#c)?N3E2`z8uYPd|1w$JIx59@@@2awG5gU#7CnmauKkoBHuAm`Gb} zni;#_e}zQ)xu)MX6v!Wi@3uZ;vTkxrF{9QQ^Lc!6vQIJ9_%o!P2%C>>cnKcM@9>`B z1ME&Kp73it&m5z)Q+h{fjbgFtrE6x^WzjVz{bKD>T{d9tm@u|iigrm?$JT{MG7h)H1&ggz3t?lNSq@9@;vV^3uKGWf21;uy2)HfIzFN$J61!}510pc6 zvX6d?5v#|Y@%r+@md7Kwkb%G@*RCUDMG4=DY$;{@pqiIP!kfsWW3PV#!RM<*xsFJp5H7ISrQqIdGRByqB@b#JsQ_`_dfvX`PsXD zH!W%6m+OMRxWU1%f$oqZ^Qn!IG7+3%>43tZ4m6@wlIyZKIkb5&E&}M1iHemyS$G>B zP5jX`>R>nhPcg%5?Pu0fG)P%Ks2~+b--SI9anl>&)aN2_7 zBRm^)^g2OZudLo7jG2MtrFa^0DH!0~8gRn-;C{>>$*AN~5%)m4)Y56WJ{TpQcy@8U zQZI)!yUEYxYA)S0p9qS0KA_y_nykVt?T*B$Y09R2sWLHrbr0ZuptIDA*tj1>O<7)} zXvrFk|6x<^#>}su;2aMK`qi^6h}9WIR$)1h_!<>@Ry0U0Scti4lIPy3aol)yo z34|-X^3{;G&*958Yv(*6=g{moZ8G_joQUEn4dWcWAQYRC?!TKax%ZvB0Vdq}ffmfA ztuEh%>H!<~3|kDpJ6vR9tf#9$<5Kt7J{1`%<$|HOSaN0hLYNF*O73ZKGZf9$ak_Wpx~wC zWy}!097m-s7(#I4)JZl78UxWtIwl*(-21Juc07^LK61xU#cEVOdW(B5CdQd7_#_Yd z^d1{67a5d_h^gamKg3@3YpBQP;hH|$%*_oJ;72giZ68h=#koR>b?}%!5&0*3jUyXz+0}!30R`;8fuiG;jPuMS^1AR+c-~xp!7cHg= z=?Yibj$+Kq{zuQU@LhgA5;)XT()!XC<(y$kuAv`br^^+U^g?&{I*zz$A8Hm$jzh+V z442Q3BP)DxL?G&w7W{6VgqCvDEJHE>i|Rh5A0ncZ*v^hIa&q$gOc{#0$Hsh+X(TQQ z$+ZcYOmBV_Q7Mg?qP;K?>|h7tgRTRxE;>V>ynYHWSI3bKr?RQyZd?=zkh7;~10ENl zD!Qs5Yylpq*g^#&k;6d&W7@vhwi;C3eYjGbuH+u9a-c9Bz`Ve8ey!>18_@QvtgA=> zXn6yZ*wS!K-JKukE{+@BJEXKG9^+$#GckDZ7K~YLq2vJ$`D0i=mm*mUF6pKDOZ-gO zc}No%z_2m0<_EwVPupW^vctj;uHU?I=AP}fq4g2a^IN3|g1C{hf#X3aMk08W`lP5r zn6ys{T*?|}y8;Z2(6ZI3vi#8W_3zoj>S&6wA{)+}?ecNOBoTeS!2i|Cuo?I8`-CuMG$NjRW~ z%e&97`x;#Q#~q9}KC($iCTrJ`oBPs-0k-D#)(pA^Um1 zRlrj2X>%mYqS=PKteVs#Z_wy~+irH5n5o}FwMWIF8#arpc zI=BWC9MzN{;lespwa!qe4a#zfhSbS^#ORS6zBaDvJtFr}wI997wMnH_etY@Id(Xxz z--4K4AK9YVUEIr;9|N}tE5pJ+#@n z#V=8dd`ojpMqH?e)TY-;CfKiNgn$;_&MLO5tNKW6mcxToeGRBbajmT+4Zwlhx5H2& zY3EfS-N4gr4+ngrB9+L9{;-4h0;S~6e3y0!aF$<7eQzn$6l2r4q{tKI#czp;N~O+I z{qiyU79Gbw_gZwFrd zni9H-QO6JpYj|56FcB9)3hSC5fArQk8b!?D;2qKmWm;i=!$}Erh(k76tc-SUK z(2sd;aravNL8M0VmO^W-(>3>z`m%s?V`JzF#qwL?9!5Q)D|uAbD?(Cygw%MsnOFj{ z^EVQD=n04fXiO&~ag(6_(UU4oi1z z#)UyQJF0#ea)=C}#?RfvYHGu@Pk9CJ#LK^DePIKar2|&6`D_|fFH{1})wu(D$^>dk zb!WmvgzjLCUszA`_?R$4QD6Fcgb6n^pEZ#byc;!Xq>!i^C&WUSP2T;)Vh^6J121oI zlP*kor*w>hgyM6wqq@BF>ojg{2y+j^k3Nlh00Z9trf1-G=piW)_1$oEbnZD@$DVq! z3|rk)9y1iP0oG9GipwJ$@ZpwUi|psG`~_c-m~ffMzk5g`eiDy;mhYkb*Q&1ADl=b; zxSZ8oZ?H=*XznQ0UJs5sx*e&7nu76$G&Ev9Z?s#;B)Vlw*yo4VG+r46lG;}&ap8`W?3X|?nyDpB-56eN(ONDYT()(Fpr~!`}l6=fmhFx3a5{P_m-zct7|`4YC+}3Pa|W%Y+Ga z6Wn#o#h*q%$0lf;=amPxC%|{`MFs7EXnkg>hr6h1^ zU#pa|lC$M&0L(cCF2F!OhGqAS!FS+!{cqTjHt~}J2*?({OJYmMW?ftz*Fq7{)Mm z3*ZTY`g(?SY*rSMkPkh0CXkj;pSRCKzhas6sN??3b-!_aQEpqR3md3I^i9Z=0>Ar@ zeom1)MO_)D9p$r{KZlp=y8t+M13dj>n6J~_*Nsy3Vebg+es_$IZyzS&9FBoZb7a}M zD}6U_zJX~&3bk^B9s=G}7o1;Lmk{-%d^EVd(6=G$URF&^4hdzGtq6kLhb>QjM(T_~HI&!OO;$6FJlNjY(iwzvrvP4dh znTGCGQZ}t>ui7OeJI2eMS-v##!N2^}Vu}svyn)%bOlIHJ4%B{RHxK<2>$Zm%M5cg< z2Ic%~-Kx-!0_{&0naFG2=SfPL3$P4^SD2o>UCS@r!}Qj*WZT-aJD{wk$XVxf#xX!| zw6m2%Rs=kn$LX`Z+K&XAI;7$8@g)Pl+Id7sB?)C%J39G3FO0ZtJfpQPqp`Zk|A_vD z7qqrtE>N@txu6?|EoScpFxM~dd_9ZwrP-;_;NPoP?eLzoh2VEezYp%R)oD&IDihhJ z#m?k|F{vDY3M~JjMM%B!*~ur%)h-oM;`G>(lSpQZI5D|oAuQA7FH43q|9~QRv<2rf`?@yb^6+d0K1`nsAnmdMZ2UJI#1zUP%^GwW$Q*NpZcg$ zbCu()!brZ9dMU1x>4a9Zucd!H21qdcxwqLn-6=UPQ%4~P_6DIMe5!`FSh_cw3MzGTug9?nN0 zz^FaTU?FIpbW=K2@>Bs`4Fr#=TY6R%DcNY;`)1s3ClEg?96#H|z5iovFT)l@%}22n z^R0qG@+9aT^le0~q4=mAPL1L`m|W9s2HF@a`)(U|PWo|vQK)2hAG;&Df0_3m;B7)a z(RJRsYV?Xv%OiIA>@8Zj)=u7V+Xny~PE=_t{zMob5cn&*XV;&w@Xf}HC{5!c3)x6X z=~k|k&JRmV1*SBawd}7m7s@)*T~0>if@`1pPa3nx#G|Ph7%6&Y}Dn5rWGt9h6aXZo|k( z7%%MT{txhh^yDO4R-?|OIij-*6uwjbTH}9eDcO8ZdulRk9bGc6hLRaFNvJvniSh}0 ze3(U0UMx=X1uNxhk%(o(=-2$#_dTlq*F-PMUaqwb_gj1rk2c$P*AmW|hzFwwJempp z?h4MLjj3r?bfIp{P~dMqx9q|lawc{L99v;#m7&Zj;ANhONQTSsvgS|!b|@Wl)(^VJ zI~2s8RBDP5+N8`HH5-CHEp@T46RV(} ze@4f(bl#ifMs7J$z@a;+_HofZM?(Vt+KcOcXXnYyizwB@im5|2PZGp)kzDH8K-C z9<|KU!5w{vhVy+LS(-+RGpq9Nk^)K_|LS&e?qs?zvFlRy- z3d~c8t!x1f1!y=Afws58qcDZgqTBDyYx}47m7Xv}>zoOrV*}dxz#x)TMUB@CK*DfY zBy`RAgH>ZGI8Xze(h}K)!IK`>Q8@W^lhF%34HY!|lrZp7xwBKLdYMDbbs?;00AZE+ zM&KX9Ax5u4bMiqB`1CE;vTfKf+6I8uGWyVHt*F2%;E~)hRyQ(UaLDRakT99alYB(e zRoA+qvs%K?GILY>w_f)a>ODOsOTUH>-jG{Ib+W%;KCfmw+!Xp5?q$uoM96o-Y*k3| zJ*&+z?#D_=%!3n14Lx7%+h$1@Ex)|7XH@BV%ep*xYq&Dsg^hom80naL zjVJ&P6nH{jux=J%wQ|Q%u<%ExWIPrNG@uZ4g!%jicK|1qF>^_MuaKg&ft=n z#cX{&3Q|^9Z<|#%Y&ehHwk)<%hN=o#`(*^H{0HFu7a$;2S$Z!l@akCGa{E%lf{7t} zfoeV%Nwncvrpx7P*O6LPC;j-I5b+2NK^e|JxbK1(AuyuDwz8gCJE?FiM(h04zIhAZ z9l4Gwq?WKgQoD18qUSu>OHi@e`#2T?HV8JH^M&C8_AqV*|^>CHP(Q4o&zTv=!N<|XgG_b#LJg%>gVU60>7>) zO6RU9PxMd`MUgfWBb%VDM41PFGt&Q|y4H<;)zH>I>prx8T(?Fjpxa_rMoh&IEs|l+j!Cv&9B2d>ME}SGw?V-ZGnF8qD{&CzcjIcp(xKx!!da}o{ zzP76M-eaLZ$-zjp535{NIs0iFb+S1v9c(1pEp2X}EfgoWh|N{bC5ML1r2udK7R=m- zS)k`8Ju=f)7Nzu!IPiwlH1si5eH;U;?wnO*aaEHLFtb`VMKOAC=$H_qW3@0%^_cr? zbWjpS7;Ht2WY_AxUr1#513JO5O-ZD-BWb|_#3YCF>s||PRmIX-J!yoeMT%=vg zh89O=o87#fbF%ezC3n_KRX73BGVQxo6wkKV*Df@_pai6MeiX18_^5*a{P09>)E4~sODJrKm3Yv)_3Otx2DbhZLkokGFwACYPVJj+mkzD< z3CMh~O>^4w9JO}D4{6(ao@^3sM!sxXw@`@|e7|Yao{u^b%uR;K@vH4Li0y9Y9ED7< z)>#@}3re%4FPPMj-VD5*I8bk1+%+$O%z?9nHTO=5wJX`G;n;V=-K|s|qSG^jJL@06 zeT@TM_0LZXlSRB+HGa0Y4Q=Jv`=H2dd>DEWxo8Kgi|B>|A%uo$m3=B1@7_MYXrh<& zqM~JDpiptiyeo#SYSt-@-6cR7;*>KsNpbD|=PP_)zYC{)U8XT+XQCwTNl~Qowb$m| zAh}(N^1G6BC4o_@pnsm{>yb48Z`&Di4bStF4sZRJe$kpaHenb*WK_ql%L_zJN`Dw) zZ`#_x^l(6rNqs9*zAI0l^98e(;|28Qx#L%YWr5}`$(Y&G*u*v)(oX$NLl*%)z8I&d zEMKXk{r?u1UwQN)vwph#B%hke?N=1O!}wjJXW>KGBdpN3ijW**X{jBD^3n$Ibz*8o zgYhjhje0xPf@Ewxm8`gg3)^~-j)Zx_5<8r|cYSZc2$T?0n_%%T)-@mke`;w%9ph3S zyS-o5uo#+-$^&0eQ5f-LOUJx9k=Wl}q9=L+aWlcOa@#B7nVHLn{0qx`N>_QymXWR( zk)H;KW^Hq;k6FZn`xf1RkVj*kRRo$aRj>{PaArC`>r zYJE!BG*uZ~GKvvzeVw-r;z?g5>%!h|6uZfSCHJ|I6|GJ&gR(q3h>*CRAfXwW_%O=v=zcA|t;kge~8-^3b#6=HL zy{yS=rJxo#vyl)O+JHt0q}3&|K3*e4FLMJiyt~{lZYFj`e_=%?nP2q9Vn@1ZI?d4!4S};N;C~r|Yn@R+VkC5j^{b z+H764+#)j~466w9Lc=7x6zY5t1Ppa9RhwanOQ~B6D7Bx@ymEf^uXmqd)~qH`>e*u} zLo_w! zVGiRDk_zG}nZj(c68x`(h_R1}@+Izp#;mKr8NBgpc~IqPYab+vWKyA-k;#M2zG&G% zz&@kBWAjs;d&j&&giir2$~L89_dGVWI&!x0p?z}*PZ5?6Datlr$Cf*j9w_Vni&VJiPcbOI}hQR7RB=~`lZI%+Spsd=}{suG}3 zk&dK1I#m%%wJMhU^TYw{S4G|23HA4Cs!x@V2qirSMPmE1tY0%1&iGwQj9l>|zXkN? zmMOh&(;_8K(NNwwEDjf7>z|uSF-2%#D^elPhXVR#M}#K%sGyUBNNT@2KKWbUE3Ziy z9}1Lmh}yTfr4~JKm+!rQ;Z`Ay=@xan>WrdgsR;rvbx6%{2kX{7RhsRCsF?!-^M@}! zG&Rmtxtbu|9?H05D<)pm0*xHlVSz|x>$9NCs8o0!d0d-2UXqNJ1rwHpbk!6bxxW`J z`h;?>2HIrl*5mdrU%o41P=vmigW`wmqknv->&6RokKZZV7Jg6Ad{C&?FN0yU{V}0T zCt^TeEl1>gqxR~ISVCwO!73*zb1bSMu!f*bU+CM>DD4PZb`wvi8gW!X6iKaFjy0lmd;jJwT`kuW@ zQC@b-R`_uX;$5oqsX5wpFXqkCRM&4h9{0Pp^5 zAsW@qZvBWhRgPTct1VCio{#)=!Pbe89S!}Wr?xp)RfA|+OdnpS zvb*<8s`J2Krcf*{>;iyA;uo``srbwMw&&a8pVA&WS={5W{9OBr_-qh#+dwQ1;9L&! zu4dKZwfB=96Pslz1x1{Bw)s}La7BNT?EZSvo!LbfQalqt3v*-B}w&gxkw zf#)XrSDH_VM@Tyti2VEW#Sh)H;0x>e%pOV8Cr{02`+X}bfjBMkp-F+bP`7OI?x}p6 ze(|7V)y3vuGwdD7bI0-5hWe@006fLMYFc+(@5>CGkg3XD9x{Vn>xuY!Wqfvl%oNL{ z%kLNDLh1kHsVuRH+9MEVav&u1vtzk()zN^E;Ng3TrOilT*#`a6*VU^-C*KS;b5+hZ zEI|F;s2L~E?5F~2c)0aXUWDN5_5zdBc$=C~ZC#iVzSS@{PRbZY4}y7}GC$f(#|fd&^@rdK%Y6Ur?coZ+2h-a-Nnt~7nd=z=6#X8+$@*{OA~%GmD-w39wyfClW_WR!t->~OQ{{h%A`7VVf?yQSN2OBmm z;>6=7c^o`^PZ0#pjp|Rli}XJ~9enqI}qjITRB~Z)-4xzuG4SWjZn_pneY~ zHT*Y$plHaGcqs4a32xF)D>Lzrv5Jib%5@J1UhoCRI`2&X3Q3kTTl4f#*(V){w z<&y^U?v@TTj|_c_9)qS9C#;3u=*nicLEAj^(|+d%l<<8~x7;}Y0N~vP+5Z3=;bQy0 z1e@;U1x;+9ksh6gbiA9ALN=m}#m|Y+%Lvut@@2mXR=uwg;Y%j2O5;whmuEqn?VJmGdYBZN#(Y0MfazHw$!K(R zn*dV^al&nPk)tQuUnN2eDyjt;2hFbC%)T>DJ)CEiZ5EMdHQ)&6#M;?^${>5dzb{6_odRxc)8oD!Xbd9JB2r%H-;*OO1O6EVLYs})RIr#0+nKIm{ zc>liXLs*hR!erCU zue&}OZF--09Mho@2y&L0PLVZ2S&F61+*q>B{ofWr+<>> z>@Ka-VZ~#avUKIhA|-<(<0*bg>8R~5s(qqFm|e!b4NCBxj& z+HA7n1UR?-<6w&2FB=M^nAl1uIj7O@?F6q56xka&}?)x!vV* zPx3A`i-uzQ4A~k(kCz;PVzM;SL}oYv>lS9cffIo9sXN7N6cB|mbgg9Ddx4 zI-hf?dda`dIYkDB=bO1CPVXptMfC?bldzR5xq=wvl}U5equPCAQ!2khydI1#7=fGg z+b>4svTCe2g9!3YcU}1-Hmm&7?;73R~^@ zh5Rh%HNSQXa||f$7%l0+?7zCMWrib70Xd78 zZ+a}-CK);qE`I`Z% z3dZFA*;@*&{gQX1y*ijz(`;(v^;5woot@u|?z#tEt3R8xzisPacQuklFST#VFPraZ z+(O$Hl!nn6eA!5tlfYmY2j{@dl6SH*KQ`*T&o*IPg~vrbg@QU|i{G@JCQtEPM|a^q zQzA+C{{ygV%JztiGDD5T9x=8d8r zBg_iTT1|Qd?7x0nSv5kN1~U&Qo5i|cmuxO{^}dX2a?%@TRb#d4`aMWG(@PEB=xVdE zj4vf$8f4nqNnzTNfQ)U~ZmpWf3kuuK^?TuUO zY9G<}-K-?3#i~G1We^@b{(LjNejoMmgY0c57T%HmYW=SQBzx4I0Dye{EZxn(b)(z; zIm%?M2JGhkL|ntC%ZQ^icL2vVI3Fd2AnC4DAzooo2X(4^;*xm^(9&8dsqrGdwBfRc zKkXCuq}*$B2!1qc0i4PBqTR!K02TXxbZ*f-OQ{fco!zxKIQ0x{ zd-T;XSWDDJEJklp43};zTx>^1J+*D8O6jcqu)j2;^F?g(ualkuh)q&{;`VO2 zio(S1WyGrwd2$}r3IgXLF$$u<<{(O>P`MMK4GK^5#J5<2ib zz<}T4@InHm>iDh{Hm&0~+%-mbJ1kZMjZED0cQL{<{v*#iNI-t{F!o7QKEe)~7*(YG zkg((`SI*2ZPwJ`{B{_aeq587+jaIvA%KBuNUdXt&P!G~a`lKtn8k zA)wTBA!z+4Rh61d4ulqQ@nC!YC{G_n;;tnKl6E-7QxMO0x4_w@X>^A1ke4>D1^=n{ z%tHxLC~*gI%omEC021=cOTm!CV;k1iYW+O=njaEqZHEM^77D}FFP}t$ZDv3UI5JlL zAnSYZlGg|YGikrgkOs{C=NFdIdKG6?9_{|r9YsYOef;w35`^975!SWAE~aeW2fn3{ z>>+*7rfT|?01tS%m%v#X(cH}ZT7LNE7Oi}~sb_qnXg#8`Iz?-tbhDEjQD_6VM_hc; zdj)5lz<@6S2s{dzy2 z9`$9`7Cc>s0J&Yd;{^ZV#%oc5&k5X>g42?N78?_%cHqtk=)D0|h9_kAv$fLv6dFf|WZHM>N3WzYeWe7<#n z{3pvgi>Lac$qD5$-h_T{88E>#;rXBqpQB7!IxSsa3xk_BoaUFB14C?7A8F?W^rFHQO## zRAg+bkSYf?Br;Zov23}DOPwWWAyDt`kY1tRzceVa4UZX%LHwD`-!5xNfr;a)W^8Er zWxDN64xuY7Y^896a9>}}7?DAy<5F<=+WXE$x3$mD=Vli84YCFZa9KE;C9~6#$CqV)%6-W0ox3t%d5;^&e^)6qSRT>1}?f77UI#xR5M zBYTuW1+RD!GEkEtGB^;5Ks4A9^}nlvq8*&x&#rE@1t=d6ypyRLEqz}wdO~K?eo<8y z-lY*nktj>Zc8wxX18WI9l{9dgbf)q;P2WDnxGLhPMQQyHP+RW7BvM!XkDpgwm|fAW z6(t3z>>$geSZkeMvxc(3*~5Q|-xeyqOfL2GeAHpnkwT&wes2XtJzm0H@~P{p&c}V154=a1Lp?OW19&P?}btL}R>N{3LTD!BzQW#(%%Q-Utived*qXrLhU8JdGzJ zUkzKnS?**c>px^mG{ueBcM37vIdSfT=-XLS^a5iquE|ztgm1ljqb5BcHmxh-==al6 z9GLa~(~iVt9G~5awD{b|53lv=>+=>L`28G?r#JL3-HBk!Vf_yfg5%17e`nQUXTxHZ zdMClVvhVN^Zb0a??>pDngsZr&XKHt&H$8nSzK52?s;E|tS}CV(KA;8&=?KI)u+hLD zOy`z=y=xC%pwfSS)r+uoLaMLia&_wmCsOc-hc&c$X%Ese$$aax&% z+hoG3e*RghGXzKQ-9f~OB+^QYaG5Yb?KG!}6)Y@9g}{?R8h#kW1juC>Rs zZ$Ao3)S;39H9httm$j5j38RY?EYyWg>)aQ*JRkhJX*pP?cHZM7sR0dj{#w*M!dq@l zuLrIfo__cK@4tA)NA9jl{cx?h<}Wvj-|*Z)nI3-?d11SIs5ZQPKI}UG=y(5<&W?ch zo46r8LK=UuymXU}tQJ2fsr^`2GU0w==fAj}f%%{2PG*pYrV|24UemA|hwf@Lu;AO? z2r!d$6-n;@`uW>-XlptW$^STdft}w|To7c)GNluEUr~=3OyqMWcyX&6bD)h>q=OjP zT4w%T^`4rnY+M{xWfFRh8uYw-mOlYjHfBb=9=I+d&_3AYA+I@IU(Zd(_+-%CCi!Rl zJ)$?lge=u&^70?_x#AHjTC%b&3}$H1Ti(%Xs=LGg)_kMEp>sva#hZqvDgmeJ!E_)C zu?i)ki|Q>8&`2#F{uUthE%V0_tkRr&kgOFP`@^;yYX3{Uq?3&?HN0KAzmtB`` zIpq>^$~q+_jV6bDuXluP1~d(YoODWY-ohMsqL_Qcx{}4IOY(?SBxz3LZRP}pjfu$P zfS0-;l-rfer6G(B1=1ED(GyUE`?C4$O&8JhmpufctE-1%eW+u_2q`}K+J2?q^yGyB zIOZx{OIyV)mTJc@+TX5N@~8&TCNe+H(7hvp&yB;!32ImdJK2?GpFE93XBjfn&eUJM z1@`veB~R?SKpIet`y1xP9p6~?VTPSj5#a}n>*AGz?^-qTgnXNDd3w~uR%3sjg=^V9 z*Y39fTiakuHd3#_!X2QGtPJC|T=W@sS4MtQ0XewKzzzP1S;D zp$uVDb;`!Z_N@)&ftU8UK3ESs%A#s)#N381t;cmt0vI;en_oDg(ruTads^*H`JBJ@ z1x33uoi{s03pN>?W}fSK-{VAb`_N$N+r@9(Zhh@x>QHy(!7p!|Agj{#%xW&N+wx<2 z%dV2{6SNWzW?OHb0S5O2ytaYgQuwKG!IxGqu!qnM7;fh4m-q+grYf0J0Iv-evb zp9r-MI%gL7WAv+BJc01+;U6@Aw4=2p9&}jYgJTl!tWS`L=3A`EZmiVLVmzHUyQxr} zp|1lf0R%1R94ui@mc@#12FRCOd;h+?m@p8lV?vLm_cR2RpA-R5jFB#1FH)uE$R$HR zsM%E;Tnz2>8aPH7-TBcNQGH&*FI!UlYie8P%z&Y6j02Xc%snlJ%@}Auc8ICe34BwY zr(dr!L#l<#^Gb{0M_F}|w8hX3;-minUZz_Db9Esa>Wk^6#}_6kp&s#QG2w@Th6$w( z$c55!0s7|?Mu;EMV^NF^bWv}IwO2RHeup8d9>A)^;=wpW3o3O7+sQWG1-y8)pnKDH z`-W*Aab95D~7A4b`v?w0`2-Kv5N+7*`OHgk%8*wiQ zgBD^1D@q1lB_27latuc5y(R+Ak4YnAamS%>#u~uAe-SCc4NoI)Yw-MPSWzsuZl4m# zo_p?TJvv#5rP8TZ|CRgwq=2jPqf1UzEz$w!O+er29^({Jk$mKvUB$TrlMmPs`Vn64 z;pwFONjYsze5pYRHpFh?cCRh{zEpxF@OCaBc&_MVrGvj{qCp>cP~x)ReR)*GAK8x& zi-Clq)9PCwV*n=EDJ=K$A-6oc1Vi1k++8J~U1yn0S-aRafCZbJ>3;xKOZ=cuMyg)` zr;l=_GNE!RRzemNmSb8h)>L#j`r5u_cTewgpzQIRZ!qzqqDtwQXU-M&H%qJuuOSI< z9ph&Pu=iwSIzp-NG#j)Rl>koxnKK5w6%1)$1|dDBQSkyfWrfCAP(^Hl=sJPTed|_V z1Eh2Q-l`4jZR-D)8HnaDsVRdI~il0NYcz@Lai0 z9@;CxmjI|5z@WlKc_qdoxDOz=R=Vd~_aESux@evX_#r{2(yOMyi%h20TVUa|(#dZK ze?#U8jgt?qs0ub&u@KHM#=Py#;=W64h(g9v4ZnP^6hdcap9JBR=sxd`*VJaUvXn}Z zU%tX^ZOYFuO{C`9RIQg5Q5H>>jLrq9o7%r(hr{c{j8>KT`1P}i?HGK4#@(4;`KkLs z*Z)3Mhj!v~dZBzv7IZ2yt`N{-bWteEQ!d!-VdHmEcfeHD3VGr=!5wzj>5A#X0ga_( z#HPt~MaJH^@}sMPV&bEuk8k;#E)*G?kMdw}TCwW%AURXvou^(|n23-jNW60^2QfgED#BS6Ps+tp<<;R{~nuNb}1dhO$+M3ftH6*{#YbZv59& z#`nrra;>ahTui;Q&oVTpbqP2-M6OIaMChrGu$5J|fclp-V?EF8`>Y`|U9a_7c)-+s z{9KgPag{&B?NPsKN}f0@9PH6>Jv*_i{Vn-4rsF`^?J%+D6SqN{Yhwi#5raLUBrHac z4i3BcXx`bSz_fYMWSShpCVIDwwuD#ks(Ehg$orwS==axwo?#`C zaA^vG$Nu+v;LBnTv#ZI@nc|mM+zx}57%W~6LC2kYRDx`dYx&k?CvFD>13{TBZ?(_QVn(fhhQ0L}PY zgc4HmtjgE^7Vj&+5=_G@)V`d3WG|?c=f(;#uL7hEd~wL{6i&BujhgLkkBFYq$Etf~ zFbxI&a=X5N$VIzn+t;i2r|#kL>b%=v&-7Cf_0I?tRk_soxu92Yb+O^F=+hFMvDklr ztvjcW5B&@&!d3pZ2ASB)rX%$|j&Y7ji$c1@oBZ*)Y>@rG0}DB6pTo^h?P0l(gvt}dFH8rC|8OvbG>tSf%3;(z+tO8e{9

Wf@+FD?DGvA4he?g#aN51zy(O$Xy! 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

Acrih5KDW9ugPSk3y-@mXy^-HnY=Jp77llly?k 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 z≺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 literal 0 HcmV?d00001 diff --git a/jsowell-ui/src/assets/images/qianbao.png b/jsowell-ui/src/assets/images/qianbao.png new file mode 100644 index 0000000000000000000000000000000000000000..05c301533050c207dc906dafa238de8dce951f0e GIT binary patch literal 10664 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 RVqaiB&{Wj})hXM;{tteYPPza9 literal 0 HcmV?d00001 diff --git a/jsowell-ui/src/assets/images/shebei.png b/jsowell-ui/src/assets/images/shebei.png new file mode 100644 index 0000000000000000000000000000000000000000..1f42f20b502ad970b019ed4e2b9a244dda844032 GIT binary patch literal 4675 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�T`cQY<N_9f5U}9t1 zBf>Q!nx$>9_ez-xN~>*r`96q}tf^TElp&xB^j)-^8z#<`ecf;cgUjUebwIh!@*1;1 z8smU`Fl%l4l5HYxrf-5IEUg1&{cQx7o-)`?oyvSYElQ0O8GHnTGME2sr1&<26`Lc` zpiK1K>3fFRz9=(q{ibR}!pH!1^SM4l~!Cikwxds|qqMZL?5TV%e zM|g_9qHlCaRGg}bTLAg+tQsO&$;HV5+Y2ihCvh!8GzUA51Wn9&XEV!@x^LNK(fSHM z!lQRrAt|#Cn~^NgZHk&4xTJh60!R9%7s`D^~Dx4o&`p+1>>>x{fDb1OZF tQ@T^0w6FOB9Ut3&+X!TJ6eCYtbH=8iPLVMEP&*s~c^y9KL3a!O>Aw!J11SIi literal 0 HcmV?d00001 diff --git a/jsowell-ui/src/assets/images/yue.png b/jsowell-ui/src/assets/images/yue.png new file mode 100644 index 0000000000000000000000000000000000000000..92921aa440fd1c55984c2b378a170ddb347dc1cd GIT binary patch literal 8372 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 I&t8oBZ$0=u6951J literal 0 HcmV?d00001 diff --git a/jsowell-ui/src/assets/images/zongfeiyong.png b/jsowell-ui/src/assets/images/zongfeiyong.png new file mode 100644 index 0000000000000000000000000000000000000000..f3f5183f1ebfd780990d29c12a43d0f1fa86d7c0 GIT binary patch literal 9084 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 literal 0 HcmV?d00001 diff --git a/jsowell-ui/src/assets/logo/logo.png b/jsowell-ui/src/assets/logo/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..8d4e1a3de6b9842235ec3106a9fba718df986178 GIT binary patch literal 4154 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 Eg8QrY0ssI2 literal 0 HcmV?d00001 diff --git a/jsowell-ui/src/assets/styles/btn.scss b/jsowell-ui/src/assets/styles/btn.scss new file mode 100644 index 000000000..e6ba1a8e1 --- /dev/null +++ b/jsowell-ui/src/assets/styles/btn.scss @@ -0,0 +1,99 @@ +@import './variables.scss'; + +@mixin colorBtn($color) { + background: $color; + + &:hover { + color: $color; + + &:before, + &:after { + background: $color; + } + } +} + +.blue-btn { + @include colorBtn($blue) +} + +.light-blue-btn { + @include colorBtn($light-blue) +} + +.red-btn { + @include colorBtn($red) +} + +.pink-btn { + @include colorBtn($pink) +} + +.green-btn { + @include colorBtn($green) +} + +.tiffany-btn { + @include colorBtn($tiffany) +} + +.yellow-btn { + @include colorBtn($yellow) +} + +.pan-btn { + font-size: 14px; + color: #fff; + padding: 14px 36px; + border-radius: 8px; + border: none; + outline: none; + transition: 600ms ease all; + position: relative; + display: inline-block; + + &:hover { + background: #fff; + + &:before, + &:after { + width: 100%; + transition: 600ms ease all; + } + } + + &:before, + &:after { + content: ''; + position: absolute; + top: 0; + right: 0; + height: 2px; + width: 0; + transition: 400ms ease all; + } + + &::after { + right: inherit; + top: inherit; + left: 0; + bottom: 0; + } +} + +.custom-button { + display: inline-block; + line-height: 1; + white-space: nowrap; + cursor: pointer; + background: #fff; + color: #fff; + -webkit-appearance: none; + text-align: center; + box-sizing: border-box; + outline: 0; + margin: 0; + padding: 10px 15px; + font-size: 14px; + border-radius: 4px; +} diff --git a/jsowell-ui/src/assets/styles/common.css b/jsowell-ui/src/assets/styles/common.css new file mode 100644 index 000000000..b1bd1152e --- /dev/null +++ b/jsowell-ui/src/assets/styles/common.css @@ -0,0 +1,282 @@ +@charset "UTF-8"; +/** + * 通用css样式布局处理 + */ +/** 基础通用 **/ +.pt5 { + padding-top: 5px; +} + +.pr5 { + padding-right: 5px; +} + +.pb5 { + padding-bottom: 5px; +} + +.mt5 { + margin-top: 5px; +} + +.mr5 { + margin-right: 5px; +} + +.mb5 { + margin-bottom: 5px; +} + +.mb8 { + margin-bottom: 8px; +} + +.ml5 { + margin-left: 5px; +} + +.mt10 { + margin-top: 10px; +} + +.mr10 { + margin-right: 10px; +} + +.mb10 { + margin-bottom: 10px; +} + +.ml10 { + margin-left: 10px; +} + +.mt20 { + margin-top: 20px; +} + +.mr20 { + margin-right: 20px; +} + +.mb20 { + margin-bottom: 20px; +} + +.ml20 { + margin-left: 20px; +} + +.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 { + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; +} + +.el-dialog:not(.is-fullscreen) { + margin-top: 6vh !important; +} + +.el-dialog__wrapper.scrollbar .el-dialog .el-dialog__body { + overflow: auto; + overflow-x: hidden; + max-height: 70vh; + padding: 10px 20px 0; +} + +.el-table .el-table__header-wrapper th, .el-table .el-table__fixed-header-wrapper th { + word-break: break-word; + background-color: #f8f8f9; + color: #515a6e; + height: 40px; + font-size: 13px; +} + +.el-table .el-table__body-wrapper .el-button [class*="el-icon-"] + span { + margin-left: 1px; +} + +/** 表单布局 **/ +.form-header { + font-size: 15px; + color: #6379bb; + border-bottom: 1px solid #ddd; + margin: 8px 10px 25px 10px; + padding-bottom: 5px; +} + +/** 表格布局 **/ +.pagination-container { + position: relative; + height: 25px; + margin-bottom: 10px; + margin-top: 15px; + padding: 10px 20px !important; +} + +/* tree border */ +.tree-border { + margin-top: 5px; + border: 1px solid #e5e6e7; + background: #FFFFFF none; + border-radius: 4px; +} + +.pagination-container .el-pagination { + right: 0; + position: absolute; +} + +@media (max-width: 768px) { + .pagination-container .el-pagination > .el-pagination__jump { + display: none !important; + } + .pagination-container .el-pagination > .el-pagination__sizes { + display: none !important; + } +} + +.el-table .fixed-width .el-button--mini { + padding-left: 0; + padding-right: 0; + width: inherit; +} + +/** 表格更多操作下拉样式 */ +.el-table .el-dropdown-link { + cursor: pointer; + color: #409EFF; + margin-left: 5px; +} + +.el-table .el-dropdown, .el-icon-arrow-down { + font-size: 12px; +} + +.el-tree-node__content > .el-checkbox { + margin-right: 8px; +} + +.list-group-striped > .list-group-item { + border-left: 0; + border-right: 0; + border-radius: 0; + padding-left: 0; + padding-right: 0; +} + +.list-group { + padding-left: 0px; + list-style: none; +} + +.list-group-item { + border-bottom: 1px solid #e7eaec; + border-top: 1px solid #e7eaec; + margin-bottom: -1px; + padding: 11px 0px; + font-size: 13px; +} + +.pull-right { + float: right !important; +} + +.el-card__header { + padding: 14px 15px 7px; + min-height: 40px; +} + +.el-card__body { + padding: 0 20px; +} + +.card-box { + padding-right: 15px; + padding-left: 15px; + margin-bottom: 10px; +} + +/* button color */ +.el-button--cyan.is-active, +.el-button--cyan:active { + background: #20B2AA; + border-color: #20B2AA; + color: #FFFFFF; +} + +.el-button--cyan:focus, +.el-button--cyan:hover { + background: #48D1CC; + border-color: #48D1CC; + color: #FFFFFF; +} + +.el-button--cyan { + background-color: #20B2AA; + border-color: #20B2AA; + color: #FFFFFF; +} + +/* text color */ +.text-navy { + color: #1ab394; +} + +.text-primary { + color: inherit; +} + +.text-success { + color: #1c84c6; +} + +.text-info { + color: #23c6c8; +} + +.text-warning { + color: #f8ac59; +} + +.text-danger { + color: #ed5565; +} + +.text-muted { + color: #888888; +} + +/* image */ +.img-circle { + border-radius: 50%; +} + +.img-lg { + width: 120px; + height: 120px; +} + +.avatar-upload-preview { + position: absolute; + top: 50%; + transform: translate(50%, -50%); + width: 200px; + height: 200px; + border-radius: 50%; + box-shadow: 0 0 4px #ccc; + overflow: hidden; +} + +/* 拖拽列样式 */ +.sortable-ghost { + opacity: .8; + color: #fff !important; + background: #42b983 !important; +} + +.top-right-btn { + position: relative; + float: right; +} diff --git a/jsowell-ui/src/assets/styles/common.min.css b/jsowell-ui/src/assets/styles/common.min.css new file mode 100644 index 000000000..2fb7cb79d --- /dev/null +++ b/jsowell-ui/src/assets/styles/common.min.css @@ -0,0 +1 @@ +.pt5{padding-top:5px}.pr5{padding-right:5px}.pb5{padding-bottom:5px}.mt5{margin-top:5px}.mr5{margin-right:5px}.mb5{margin-bottom:5px}.mb8{margin-bottom:8px}.ml5{margin-left:5px}.mt10{margin-top:10px}.mr10{margin-right:10px}.mb10{margin-bottom:10px}.ml10{margin-left:10px}.mt20{margin-top:20px}.mr20{margin-right:20px}.mb20{margin-bottom:20px}.ml20{margin-left:20px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.el-dialog:not(.is-fullscreen){margin-top:6vh !important}.el-dialog__wrapper.scrollbar .el-dialog .el-dialog__body{overflow:auto;overflow-x:hidden;max-height:70vh;padding:10px 20px 0}.el-table .el-table__header-wrapper th,.el-table .el-table__fixed-header-wrapper th{word-break:break-word;background-color:#f8f8f9;color:#515a6e;height:40px;font-size:13px}.el-table .el-table__body-wrapper .el-button [class*="el-icon-"]+span{margin-left:1px}.form-header{font-size:15px;color:#6379bb;border-bottom:1px solid #ddd;margin:8px 10px 25px 10px;padding-bottom:5px}.pagination-container{position:relative;height:25px;margin-bottom:10px;margin-top:15px;padding:10px 20px !important}.tree-border{margin-top:5px;border:1px solid #e5e6e7;background:#FFFFFF none;border-radius:4px}.pagination-container .el-pagination{right:0;position:absolute}@media (max-width: 768px){.pagination-container .el-pagination>.el-pagination__jump{display:none !important}.pagination-container .el-pagination>.el-pagination__sizes{display:none !important}}.el-table .fixed-width .el-button--mini{padding-left:0;padding-right:0;width:inherit}.el-table .el-dropdown-link{cursor:pointer;color:#409EFF;margin-left:5px}.el-table .el-dropdown,.el-icon-arrow-down{font-size:12px}.el-tree-node__content>.el-checkbox{margin-right:8px}.list-group-striped>.list-group-item{border-left:0;border-right:0;border-radius:0;padding-left:0;padding-right:0}.list-group{padding-left:0px;list-style:none}.list-group-item{border-bottom:1px solid #e7eaec;border-top:1px solid #e7eaec;margin-bottom:-1px;padding:11px 0px;font-size:13px}.pull-right{float:right !important}.el-card__header{padding:14px 15px 7px;min-height:40px}.el-card__body{padding:0 20px}.card-box{padding-right:15px;padding-left:15px;margin-bottom:10px}.el-button--cyan.is-active,.el-button--cyan:active{background:#20B2AA;border-color:#20B2AA;color:#FFFFFF}.el-button--cyan:focus,.el-button--cyan:hover{background:#48D1CC;border-color:#48D1CC;color:#FFFFFF}.el-button--cyan{background-color:#20B2AA;border-color:#20B2AA;color:#FFFFFF}.text-navy{color:#1ab394}.text-primary{color:inherit}.text-success{color:#1c84c6}.text-info{color:#23c6c8}.text-warning{color:#f8ac59}.text-danger{color:#ed5565}.text-muted{color:#888888}.img-circle{border-radius:50%}.img-lg{width:120px;height:120px}.avatar-upload-preview{position:absolute;top:50%;transform:translate(50%, -50%);width:200px;height:200px;border-radius:50%;box-shadow:0 0 4px #ccc;overflow:hidden}.sortable-ghost{opacity:.8;color:#fff !important;background:#42b983 !important}.top-right-btn{position:relative;float:right} diff --git a/jsowell-ui/src/assets/styles/common.scss b/jsowell-ui/src/assets/styles/common.scss new file mode 100644 index 000000000..c09174da5 --- /dev/null +++ b/jsowell-ui/src/assets/styles/common.scss @@ -0,0 +1,274 @@ + /** + * 通用css样式布局处理 + */ + + /** 基础通用 **/ +.pt5 { + padding-top: 5px; +} +.pr5 { + padding-right: 5px; +} +.pb5 { + padding-bottom: 5px; +} +.mt5 { + margin-top: 5px; +} +.mr5 { + margin-right: 5px; +} +.mb5 { + margin-bottom: 5px; +} +.mb8 { + margin-bottom: 8px; +} +.ml5 { + margin-left: 5px; +} +.mt10 { + margin-top: 10px; +} +.mr10 { + margin-right: 10px; +} +.mb10 { + margin-bottom: 10px; +} +.ml10 { + margin-left: 10px; +} +.mt20 { + margin-top: 20px; +} +.mr20 { + margin-right: 20px; +} +.mb20 { + margin-bottom: 20px; +} +.ml20 { + margin-left: 20px; +} + +.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 { + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; +} + +.el-dialog:not(.is-fullscreen) { + margin-top: 6vh !important; +} + +.el-dialog__wrapper.scrollbar .el-dialog .el-dialog__body { + overflow: auto; + overflow-x: hidden; + max-height: 70vh; + padding: 10px 20px 0; +} + +.el-table { + .el-table__header-wrapper, .el-table__fixed-header-wrapper { + th { + word-break: break-word; + background-color: #f8f8f9; + color: #515a6e; + height: 40px; + font-size: 13px; + } + } + .el-table__body-wrapper { + .el-button [class*="el-icon-"] + span { + margin-left: 1px; + } + } +} + +/** 表单布局 **/ +.form-header { + font-size:15px; + color:#6379bb; + border-bottom:1px solid #ddd; + margin:8px 10px 25px 10px; + padding-bottom:5px +} + +/** 表格布局 **/ +.pagination-container { + position: relative; + height: 25px; + margin-bottom: 10px; + margin-top: 15px; + padding: 10px 20px !important; +} + +/* tree border */ +.tree-border { + margin-top: 5px; + border: 1px solid #e5e6e7; + background: #FFFFFF none; + border-radius:4px; +} + +.pagination-container .el-pagination { + right: 0; + position: absolute; +} + +@media ( max-width : 768px) { + .pagination-container .el-pagination > .el-pagination__jump { + display: none !important; + } + .pagination-container .el-pagination > .el-pagination__sizes { + display: none !important; + } +} + +.el-table .fixed-width .el-button--mini { + padding-left: 0; + padding-right: 0; + width: inherit; +} + +/** 表格更多操作下拉样式 */ +.el-table .el-dropdown-link { + cursor: pointer; + color: #409EFF; + margin-left: 5px; +} + +.el-table .el-dropdown, .el-icon-arrow-down { + font-size: 12px; +} + +.el-tree-node__content > .el-checkbox { + margin-right: 8px; +} + +.list-group-striped > .list-group-item { + border-left: 0; + border-right: 0; + border-radius: 0; + padding-left: 0; + padding-right: 0; +} + +.list-group { + padding-left: 0px; + list-style: none; +} + +.list-group-item { + border-bottom: 1px solid #e7eaec; + border-top: 1px solid #e7eaec; + margin-bottom: -1px; + padding: 11px 0px; + font-size: 13px; +} + +.pull-right { + float: right !important; +} + +.el-card__header { + padding: 14px 15px 7px; + min-height: 40px; +} + +.el-card__body { + // 原本样式 + // padding: 15px 20px 20px 20px; + padding: 0 20px; +} + +.card-box { + padding-right: 15px; + padding-left: 15px; + margin-bottom: 10px; +} + +/* button color */ +.el-button--cyan.is-active, +.el-button--cyan:active { + background: #20B2AA; + border-color: #20B2AA; + color: #FFFFFF; +} + +.el-button--cyan:focus, +.el-button--cyan:hover { + background: #48D1CC; + border-color: #48D1CC; + color: #FFFFFF; +} + +.el-button--cyan { + background-color: #20B2AA; + border-color: #20B2AA; + color: #FFFFFF; +} + +/* text color */ +.text-navy { + color: #1ab394; +} + +.text-primary { + color: inherit; +} + +.text-success { + color: #1c84c6; +} + +.text-info { + color: #23c6c8; +} + +.text-warning { + color: #f8ac59; +} + +.text-danger { + color: #ed5565; +} + +.text-muted { + color: #888888; +} + +/* image */ +.img-circle { + border-radius: 50%; +} + +.img-lg { + width: 120px; + height: 120px; +} + +.avatar-upload-preview { + position: absolute; + top: 50%; + transform: translate(50%, -50%); + width: 200px; + height: 200px; + border-radius: 50%; + box-shadow: 0 0 4px #ccc; + overflow: hidden; +} + +/* 拖拽列样式 */ +.sortable-ghost{ + opacity: .8; + color: #fff!important; + background: #42b983!important; +} + +.top-right-btn { + position: relative; + float: right; +} diff --git a/jsowell-ui/src/assets/styles/element-ui.scss b/jsowell-ui/src/assets/styles/element-ui.scss new file mode 100644 index 000000000..363092a63 --- /dev/null +++ b/jsowell-ui/src/assets/styles/element-ui.scss @@ -0,0 +1,92 @@ +// cover some element-ui styles + +.el-breadcrumb__inner, +.el-breadcrumb__inner a { + font-weight: 400 !important; +} + +.el-upload { + input[type="file"] { + display: none !important; + } +} + +.el-upload__input { + display: none; +} + +.cell { + .el-tag { + margin-right: 0px; + } +} + +.small-padding { + .cell { + padding-left: 5px; + padding-right: 5px; + } +} + +.fixed-width { + .el-button--mini { + padding: 7px 10px; + width: 60px; + } +} + +.status-col { + .cell { + padding: 0 10px; + text-align: center; + + .el-tag { + margin-right: 0px; + } + } +} + +// to fixed https://github.com/ElemeFE/element/issues/2461 +.el-dialog { + transform: none; + left: 0; + position: relative; + margin: 0 auto; +} + +// refine element ui upload +.upload-container { + .el-upload { + width: 100%; + + .el-upload-dragger { + width: 100%; + height: 200px; + } + } +} + +// dropdown +.el-dropdown-menu { + a { + display: block + } +} + +// fix date-picker ui bug in filter-item +.el-range-editor.el-input__inner { + display: inline-flex !important; +} + +// to fix el-date-picker css style +.el-range-separator { + box-sizing: content-box; +} + +.el-menu--collapse + > div + > .el-submenu + > .el-submenu__title + .el-submenu__icon-arrow { + display: none; +} \ No newline at end of file diff --git a/jsowell-ui/src/assets/styles/element-variables.scss b/jsowell-ui/src/assets/styles/element-variables.scss new file mode 100644 index 000000000..1615ff289 --- /dev/null +++ b/jsowell-ui/src/assets/styles/element-variables.scss @@ -0,0 +1,31 @@ +/** +* I think element-ui's default theme color is too light for long-term use. +* So I modified the default color and you can modify it to your liking. +**/ + +/* theme color */ +$--color-primary: #1890ff; +$--color-success: #13ce66; +$--color-warning: #ffba00; +$--color-danger: #ff4949; +// $--color-info: #1E1E1E; + +$--button-font-weight: 400; + +// $--color-text-regular: #1f2d3d; + +$--border-color-light: #dfe4ed; +$--border-color-lighter: #e6ebf5; + +$--table-border: 1px solid #dfe6ec; + +/* icon font path, required */ +$--font-path: '~element-ui/lib/theme-chalk/fonts'; + +@import "~element-ui/packages/theme-chalk/src/index"; + +// the :export directive is the magic sauce for webpack +// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass +:export { + theme: $--color-primary; +} diff --git a/jsowell-ui/src/assets/styles/index.scss b/jsowell-ui/src/assets/styles/index.scss new file mode 100644 index 000000000..96095ef6b --- /dev/null +++ b/jsowell-ui/src/assets/styles/index.scss @@ -0,0 +1,191 @@ +@import './variables.scss'; +@import './mixin.scss'; +@import './transition.scss'; +@import './element-ui.scss'; +@import './sidebar.scss'; +@import './btn.scss'; + +body { + height: 100%; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; +} + +label { + font-weight: 700; +} + +html { + height: 100%; + box-sizing: border-box; +} + +#app { + height: 100%; +} + +*, +*:before, +*:after { + box-sizing: inherit; +} + +.no-padding { + padding: 0px !important; +} + +.padding-content { + padding: 4px 0; +} + +a:focus, +a:active { + outline: none; +} + +a, +a:focus, +a:hover { + cursor: pointer; + color: inherit; + text-decoration: none; +} + +div:focus { + outline: none; +} + +.fr { + float: right; +} + +.fl { + float: left; +} + +.pr-5 { + padding-right: 5px; +} + +.pl-5 { + padding-left: 5px; +} + +.block { + display: block; +} + +.pointer { + cursor: pointer; +} + +.inlineBlock { + display: block; +} + +.clearfix { + &:after { + visibility: hidden; + display: block; + font-size: 0; + content: " "; + clear: both; + height: 0; + } +} + +aside { + background: #eef1f6; + padding: 8px 24px; + margin-bottom: 20px; + border-radius: 2px; + display: block; + line-height: 32px; + font-size: 16px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + color: #2c3e50; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + a { + color: #337ab7; + cursor: pointer; + + &:hover { + color: rgb(32, 160, 255); + } + } +} + +//main-container全局样式 +.app-container { + padding: 20px; +} + +.components-container { + margin: 30px 50px; + position: relative; +} + +.pagination-container { + margin-top: 30px; +} + +.text-center { + text-align: center +} + +.sub-navbar { + height: 50px; + line-height: 50px; + position: relative; + width: 100%; + text-align: right; + padding-right: 20px; + transition: 600ms ease position; + background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%); + + .subtitle { + font-size: 20px; + color: #fff; + } + + &.draft { + background: #d0d0d0; + } + + &.deleted { + background: #d0d0d0; + } +} + +.link-type, +.link-type:focus { + color: #337ab7; + cursor: pointer; + + &:hover { + color: rgb(32, 160, 255); + } +} + +.filter-container { + padding-bottom: 10px; + + .filter-item { + display: inline-block; + vertical-align: middle; + margin-bottom: 10px; + } +} + +//refine vue-multiselect plugin +.multiselect { + line-height: 16px; +} + +.multiselect--active { + z-index: 1000 !important; +} diff --git a/jsowell-ui/src/assets/styles/mixin.scss b/jsowell-ui/src/assets/styles/mixin.scss new file mode 100644 index 000000000..06fa06125 --- /dev/null +++ b/jsowell-ui/src/assets/styles/mixin.scss @@ -0,0 +1,66 @@ +@mixin clearfix { + &:after { + content: ""; + display: table; + clear: both; + } +} + +@mixin scrollBar { + &::-webkit-scrollbar-track-piece { + background: #d3dce6; + } + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-thumb { + background: #99a9bf; + border-radius: 20px; + } +} + +@mixin relative { + position: relative; + width: 100%; + height: 100%; +} + +@mixin pct($pct) { + width: #{$pct}; + position: relative; + margin: 0 auto; +} + +@mixin triangle($width, $height, $color, $direction) { + $width: $width/2; + $color-border-style: $height solid $color; + $transparent-border-style: $width solid transparent; + height: 0; + width: 0; + + @if $direction==up { + border-bottom: $color-border-style; + border-left: $transparent-border-style; + border-right: $transparent-border-style; + } + + @else if $direction==right { + border-left: $color-border-style; + border-top: $transparent-border-style; + border-bottom: $transparent-border-style; + } + + @else if $direction==down { + border-top: $color-border-style; + border-left: $transparent-border-style; + border-right: $transparent-border-style; + } + + @else if $direction==left { + border-right: $color-border-style; + border-top: $transparent-border-style; + border-bottom: $transparent-border-style; + } +} diff --git a/jsowell-ui/src/assets/styles/sidebar.scss b/jsowell-ui/src/assets/styles/sidebar.scss new file mode 100644 index 000000000..ed308b8dc --- /dev/null +++ b/jsowell-ui/src/assets/styles/sidebar.scss @@ -0,0 +1,227 @@ +#app { + + .main-container { + min-height: 100%; + transition: margin-left .28s; + margin-left: $base-sidebar-width; + position: relative; + } + + .sidebarHide { + margin-left: 0!important; + } + + .sidebar-container { + -webkit-transition: width .28s; + transition: width 0.28s; + width: $base-sidebar-width !important; + background-color: $base-menu-background; + height: 100%; + position: fixed; + font-size: 0px; + top: 0; + bottom: 0; + left: 0; + z-index: 1001; + overflow: hidden; + -webkit-box-shadow: 2px 0 6px rgba(0,21,41,.35); + box-shadow: 2px 0 6px rgba(0,21,41,.35); + + // reset element-ui css + .horizontal-collapse-transition { + transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out; + } + + .scrollbar-wrapper { + overflow-x: hidden !important; + } + + .el-scrollbar__bar.is-vertical { + right: 0px; + } + + .el-scrollbar { + height: 100%; + } + + &.has-logo { + .el-scrollbar { + height: calc(100% - 50px); + } + } + + .is-horizontal { + display: none; + } + + a { + display: inline-block; + width: 100%; + overflow: hidden; + } + + .svg-icon { + margin-right: 16px; + } + + .el-menu { + border: none; + height: 100%; + width: 100% !important; + } + + .el-menu-item, .el-submenu__title { + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; + } + + // menu hover + .submenu-title-noDropdown, + .el-submenu__title { + &:hover { + background-color: rgba(0, 0, 0, 0.06) !important; + } + } + + & .theme-dark .is-active > .el-submenu__title { + color: $base-menu-color-active !important; + } + + & .nest-menu .el-submenu>.el-submenu__title, + & .el-submenu .el-menu-item { + min-width: $base-sidebar-width !important; + + &:hover { + background-color: rgba(0, 0, 0, 0.06) !important; + } + } + + & .theme-dark .nest-menu .el-submenu>.el-submenu__title, + & .theme-dark .el-submenu .el-menu-item { + background-color: $base-sub-menu-background !important; + + &:hover { + background-color: $base-sub-menu-hover !important; + } + } + } + + .hideSidebar { + .sidebar-container { + width: 54px !important; + } + + .main-container { + margin-left: 54px; + } + + .submenu-title-noDropdown { + padding: 0 !important; + position: relative; + + .el-tooltip { + padding: 0 !important; + + .svg-icon { + margin-left: 20px; + } + } + } + + .el-submenu { + overflow: hidden; + + &>.el-submenu__title { + padding: 0 !important; + + .svg-icon { + margin-left: 20px; + } + + } + } + + .el-menu--collapse { + .el-submenu { + &>.el-submenu__title { + &>span { + height: 0; + width: 0; + overflow: hidden; + visibility: hidden; + display: inline-block; + } + } + } + } + } + + .el-menu--collapse .el-menu .el-submenu { + min-width: $base-sidebar-width !important; + } + + // mobile responsive + .mobile { + .main-container { + margin-left: 0px; + } + + .sidebar-container { + transition: transform .28s; + width: $base-sidebar-width !important; + } + + &.hideSidebar { + .sidebar-container { + pointer-events: none; + transition-duration: 0.3s; + transform: translate3d(-$base-sidebar-width, 0, 0); + } + } + } + + .withoutAnimation { + + .main-container, + .sidebar-container { + transition: none; + } + } +} + +// when menu collapsed +.el-menu--vertical { + &>.el-menu { + .svg-icon { + margin-right: 16px; + } + } + + .nest-menu .el-submenu>.el-submenu__title, + .el-menu-item { + &:hover { + // you can use $subMenuHover + background-color: rgba(0, 0, 0, 0.06) !important; + } + } + + // the scroll bar appears when the subMenu is too long + >.el-menu--popup { + max-height: 100vh; + overflow-y: auto; + + &::-webkit-scrollbar-track-piece { + background: #d3dce6; + } + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-thumb { + background: #99a9bf; + border-radius: 20px; + } + } +} diff --git a/jsowell-ui/src/assets/styles/transition.scss b/jsowell-ui/src/assets/styles/transition.scss new file mode 100644 index 000000000..4cb27cc81 --- /dev/null +++ b/jsowell-ui/src/assets/styles/transition.scss @@ -0,0 +1,48 @@ +// global transition css + +/* fade */ +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.28s; +} + +.fade-enter, +.fade-leave-active { + opacity: 0; +} + +/* fade-transform */ +.fade-transform-leave-active, +.fade-transform-enter-active { + transition: all .5s; +} + +.fade-transform-enter { + opacity: 0; + transform: translateX(-30px); +} + +.fade-transform-leave-to { + opacity: 0; + transform: translateX(30px); +} + +/* breadcrumb transition */ +.breadcrumb-enter-active, +.breadcrumb-leave-active { + transition: all .5s; +} + +.breadcrumb-enter, +.breadcrumb-leave-active { + opacity: 0; + transform: translateX(20px); +} + +.breadcrumb-move { + transition: all .5s; +} + +.breadcrumb-leave-active { + position: absolute; +} diff --git a/jsowell-ui/src/assets/styles/variables.scss b/jsowell-ui/src/assets/styles/variables.scss new file mode 100644 index 000000000..34484d47e --- /dev/null +++ b/jsowell-ui/src/assets/styles/variables.scss @@ -0,0 +1,54 @@ +// base color +$blue:#324157; +$light-blue:#3A71A8; +$red:#C03639; +$pink: #E65D6E; +$green: #30B08F; +$tiffany: #4AB7BD; +$yellow:#FEC171; +$panGreen: #30B08F; + +// 默认菜单主题风格 +$base-menu-color:#bfcbd9; +$base-menu-color-active:#f4f4f5; +$base-menu-background:#304156; +$base-logo-title-color: #ffffff; + +$base-menu-light-color:rgba(0,0,0,.70); +$base-menu-light-background:#ffffff; +$base-logo-light-title-color: #001529; + +$base-sub-menu-background:#1f2d3d; +$base-sub-menu-hover:#001528; + +// 自定义暗色菜单风格 +/** +$base-menu-color:hsla(0,0%,100%,.65); +$base-menu-color-active:#fff; +$base-menu-background:#001529; +$base-logo-title-color: #ffffff; + +$base-menu-light-color:rgba(0,0,0,.70); +$base-menu-light-background:#ffffff; +$base-logo-light-title-color: #001529; + +$base-sub-menu-background:#000c17; +$base-sub-menu-hover:#001528; +*/ + +$base-sidebar-width: 200px; + +// the :export directive is the magic sauce for webpack +// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass +:export { + menuColor: $base-menu-color; + menuLightColor: $base-menu-light-color; + menuColorActive: $base-menu-color-active; + menuBackground: $base-menu-background; + menuLightBackground: $base-menu-light-background; + subMenuBackground: $base-sub-menu-background; + subMenuHover: $base-sub-menu-hover; + sideBarWidth: $base-sidebar-width; + logoTitleColor: $base-logo-title-color; + logoLightTitleColor: $base-logo-light-title-color +} diff --git a/jsowell-ui/src/assets/图标.webp b/jsowell-ui/src/assets/图标.webp new file mode 100644 index 0000000000000000000000000000000000000000..b3e63fa3df94ae9163d54ce854804ccc62a50388 GIT binary patch literal 5266 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 j;9XApQPk<~-H= + + + + {{ item.meta.title }} + {{ item.meta.title }} + + + + + + + + diff --git a/jsowell-ui/src/components/Crontab/day.vue b/jsowell-ui/src/components/Crontab/day.vue new file mode 100644 index 000000000..fe3eaf0c4 --- /dev/null +++ b/jsowell-ui/src/components/Crontab/day.vue @@ -0,0 +1,161 @@ + + + diff --git a/jsowell-ui/src/components/Crontab/hour.vue b/jsowell-ui/src/components/Crontab/hour.vue new file mode 100644 index 000000000..4b1f1fcdb --- /dev/null +++ b/jsowell-ui/src/components/Crontab/hour.vue @@ -0,0 +1,114 @@ + + + diff --git a/jsowell-ui/src/components/Crontab/index.vue b/jsowell-ui/src/components/Crontab/index.vue new file mode 100644 index 000000000..3963df28e --- /dev/null +++ b/jsowell-ui/src/components/Crontab/index.vue @@ -0,0 +1,430 @@ + + + + diff --git a/jsowell-ui/src/components/Crontab/min.vue b/jsowell-ui/src/components/Crontab/min.vue new file mode 100644 index 000000000..43cab900d --- /dev/null +++ b/jsowell-ui/src/components/Crontab/min.vue @@ -0,0 +1,116 @@ + + + \ No newline at end of file diff --git a/jsowell-ui/src/components/Crontab/month.vue b/jsowell-ui/src/components/Crontab/month.vue new file mode 100644 index 000000000..fd0ac384f --- /dev/null +++ b/jsowell-ui/src/components/Crontab/month.vue @@ -0,0 +1,114 @@ + + + diff --git a/jsowell-ui/src/components/Crontab/result.vue b/jsowell-ui/src/components/Crontab/result.vue new file mode 100644 index 000000000..aea6e0e46 --- /dev/null +++ b/jsowell-ui/src/components/Crontab/result.vue @@ -0,0 +1,559 @@ + + + diff --git a/jsowell-ui/src/components/Crontab/second.vue b/jsowell-ui/src/components/Crontab/second.vue new file mode 100644 index 000000000..e7b776171 --- /dev/null +++ b/jsowell-ui/src/components/Crontab/second.vue @@ -0,0 +1,117 @@ + + + diff --git a/jsowell-ui/src/components/Crontab/week.vue b/jsowell-ui/src/components/Crontab/week.vue new file mode 100644 index 000000000..1cec700e8 --- /dev/null +++ b/jsowell-ui/src/components/Crontab/week.vue @@ -0,0 +1,202 @@ + + + diff --git a/jsowell-ui/src/components/Crontab/year.vue b/jsowell-ui/src/components/Crontab/year.vue new file mode 100644 index 000000000..5487a6c7f --- /dev/null +++ b/jsowell-ui/src/components/Crontab/year.vue @@ -0,0 +1,131 @@ + + + diff --git a/jsowell-ui/src/components/DictData/index.js b/jsowell-ui/src/components/DictData/index.js new file mode 100644 index 000000000..7b85d4aaa --- /dev/null +++ b/jsowell-ui/src/components/DictData/index.js @@ -0,0 +1,49 @@ +import Vue from 'vue' +import store from '@/store' +import DataDict from '@/utils/dict' +import { getDicts as getDicts } from '@/api/system/dict/data' + +function searchDictByKey(dict, key) { + if (key == null && key == "") { + return null + } + try { + for (let i = 0; i < dict.length; i++) { + if (dict[i].key == key) { + return dict[i].value + } + } + } catch (e) { + return null + } +} + +function install() { + Vue.use(DataDict, { + metas: { + '*': { + labelField: 'dictLabel', + valueField: 'dictValue', + request(dictMeta) { + const storeDict = searchDictByKey(store.getters.dict, dictMeta.type) + if (storeDict) { + return new Promise(resolve => { resolve(storeDict) }) + } else { + return new Promise((resolve, reject) => { + getDicts(dictMeta.type).then(res => { + store.dispatch('dict/setDict', { key: dictMeta.type, value: res.data }) + resolve(res.data) + }).catch(error => { + reject(error) + }) + }) + } + }, + }, + }, + }) +} + +export default { + install, +} \ No newline at end of file diff --git a/jsowell-ui/src/components/DictTag/index.vue b/jsowell-ui/src/components/DictTag/index.vue new file mode 100644 index 000000000..794849bf4 --- /dev/null +++ b/jsowell-ui/src/components/DictTag/index.vue @@ -0,0 +1,58 @@ + + + + diff --git a/jsowell-ui/src/components/Editor/index.vue b/jsowell-ui/src/components/Editor/index.vue new file mode 100644 index 000000000..6bb5a18d3 --- /dev/null +++ b/jsowell-ui/src/components/Editor/index.vue @@ -0,0 +1,272 @@ + + + + + diff --git a/jsowell-ui/src/components/FileUpload/index.vue b/jsowell-ui/src/components/FileUpload/index.vue new file mode 100644 index 000000000..aa2296b93 --- /dev/null +++ b/jsowell-ui/src/components/FileUpload/index.vue @@ -0,0 +1,209 @@ + + + + + diff --git a/jsowell-ui/src/components/Hamburger/index.vue b/jsowell-ui/src/components/Hamburger/index.vue new file mode 100644 index 000000000..368b00215 --- /dev/null +++ b/jsowell-ui/src/components/Hamburger/index.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/jsowell-ui/src/components/HeaderSearch/index.vue b/jsowell-ui/src/components/HeaderSearch/index.vue new file mode 100644 index 000000000..c44eff56e --- /dev/null +++ b/jsowell-ui/src/components/HeaderSearch/index.vue @@ -0,0 +1,190 @@ + + + + + diff --git a/jsowell-ui/src/components/IconSelect/index.vue b/jsowell-ui/src/components/IconSelect/index.vue new file mode 100644 index 000000000..b0ec9fa1a --- /dev/null +++ b/jsowell-ui/src/components/IconSelect/index.vue @@ -0,0 +1,68 @@ + + + + + + diff --git a/jsowell-ui/src/components/IconSelect/requireIcons.js b/jsowell-ui/src/components/IconSelect/requireIcons.js new file mode 100644 index 000000000..99e5c54cc --- /dev/null +++ b/jsowell-ui/src/components/IconSelect/requireIcons.js @@ -0,0 +1,11 @@ + +const req = require.context('../../assets/icons/svg', false, /\.svg$/) +const requireAll = requireContext => requireContext.keys() + +const re = /\.\/(.*)\.svg/ + +const icons = requireAll(req).map(i => { + return i.match(re)[1] +}) + +export default icons diff --git a/jsowell-ui/src/components/ImagePreview/index.vue b/jsowell-ui/src/components/ImagePreview/index.vue new file mode 100644 index 000000000..743d8d51d --- /dev/null +++ b/jsowell-ui/src/components/ImagePreview/index.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/jsowell-ui/src/components/ImageUpload/index.vue b/jsowell-ui/src/components/ImageUpload/index.vue new file mode 100644 index 000000000..4068b6727 --- /dev/null +++ b/jsowell-ui/src/components/ImageUpload/index.vue @@ -0,0 +1,212 @@ + + + + + diff --git a/jsowell-ui/src/components/MapContainer/MapContainer.vue b/jsowell-ui/src/components/MapContainer/MapContainer.vue new file mode 100644 index 000000000..c5b155a46 --- /dev/null +++ b/jsowell-ui/src/components/MapContainer/MapContainer.vue @@ -0,0 +1,235 @@ + + + + + diff --git a/jsowell-ui/src/components/MapContainer/MapContainer11111.vue b/jsowell-ui/src/components/MapContainer/MapContainer11111.vue new file mode 100644 index 000000000..8f6c29149 --- /dev/null +++ b/jsowell-ui/src/components/MapContainer/MapContainer11111.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/jsowell-ui/src/components/Pagination/index.vue b/jsowell-ui/src/components/Pagination/index.vue new file mode 100644 index 000000000..f2696ff0c --- /dev/null +++ b/jsowell-ui/src/components/Pagination/index.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/jsowell-ui/src/components/PanThumb/index.vue b/jsowell-ui/src/components/PanThumb/index.vue new file mode 100644 index 000000000..1bcf41709 --- /dev/null +++ b/jsowell-ui/src/components/PanThumb/index.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/jsowell-ui/src/components/ParentView/index.vue b/jsowell-ui/src/components/ParentView/index.vue new file mode 100644 index 000000000..7bf614897 --- /dev/null +++ b/jsowell-ui/src/components/ParentView/index.vue @@ -0,0 +1,3 @@ + diff --git a/jsowell-ui/src/components/RightPanel/index.vue b/jsowell-ui/src/components/RightPanel/index.vue new file mode 100644 index 000000000..2d6122bc0 --- /dev/null +++ b/jsowell-ui/src/components/RightPanel/index.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/jsowell-ui/src/components/RightToolbar/index.vue b/jsowell-ui/src/components/RightToolbar/index.vue new file mode 100644 index 000000000..358f23cf2 --- /dev/null +++ b/jsowell-ui/src/components/RightToolbar/index.vue @@ -0,0 +1,104 @@ + + + diff --git a/jsowell-ui/src/components/Screenfull/index.vue b/jsowell-ui/src/components/Screenfull/index.vue new file mode 100644 index 000000000..d4e539c26 --- /dev/null +++ b/jsowell-ui/src/components/Screenfull/index.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/jsowell-ui/src/components/SizeSelect/index.vue b/jsowell-ui/src/components/SizeSelect/index.vue new file mode 100644 index 000000000..069b5de9b --- /dev/null +++ b/jsowell-ui/src/components/SizeSelect/index.vue @@ -0,0 +1,56 @@ + + + diff --git a/jsowell-ui/src/components/SvgIcon/index.vue b/jsowell-ui/src/components/SvgIcon/index.vue new file mode 100644 index 000000000..e4bf5ade1 --- /dev/null +++ b/jsowell-ui/src/components/SvgIcon/index.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/jsowell-ui/src/components/ThemePicker/index.vue b/jsowell-ui/src/components/ThemePicker/index.vue new file mode 100644 index 000000000..1714e1f39 --- /dev/null +++ b/jsowell-ui/src/components/ThemePicker/index.vue @@ -0,0 +1,173 @@ + + + + + diff --git a/jsowell-ui/src/components/TopNav/index.vue b/jsowell-ui/src/components/TopNav/index.vue new file mode 100644 index 000000000..0cc24dba8 --- /dev/null +++ b/jsowell-ui/src/components/TopNav/index.vue @@ -0,0 +1,181 @@ + + + + + diff --git a/jsowell-ui/src/components/iFrame/index.vue b/jsowell-ui/src/components/iFrame/index.vue new file mode 100644 index 000000000..426857fb1 --- /dev/null +++ b/jsowell-ui/src/components/iFrame/index.vue @@ -0,0 +1,36 @@ +