From 3aa180bbd21b7c0979af542492ff99f77e12207c Mon Sep 17 00:00:00 2001 From: lihansani Date: Mon, 13 Jul 2026 13:25:21 +0800 Subject: [PATCH] first commit --- .claude/settings.local.json | 8 + .gitattributes | 2 + .gitignore | 33 + .mvn/wrapper/maven-wrapper.properties | 19 + doc/协议描述.pdf | Bin 0 -> 55521 bytes ...环验证系统应用软件研制技术要求-20250918.docx | Bin 0 -> 37495 bytes mvnw | 259 +++ mvnw.cmd | 149 ++ pom.xml | 275 +++ src/main/java/com/cbsd/client/Test.java | 6 + .../branch/IBranchConditionInterface.java | 29 + .../cbsd/client/check/ICheckInterface.java | 30 + .../client/parse/IParseDataInterface.java | 23 + .../client/pretreatment/CalculateUtils.java | 4 + .../pretreatment/IPreTreatmentInterface.java | 30 + .../verification/IVerificationInterface.java | 20 + .../cbsd/client/verification/VerifyUtils.java | 55 + .../DynamicClassBootstrap.java | 59 + ...ersaltestsoftwareClientApiApplication.java | 21 + .../boardTask/PushHolder.java | 173 ++ .../boardTask/PushSession.java | 40 + .../config/BasePage.java | 22 + .../config/ConnectionFactory.java | 26 + .../config/GeneratorCode.java | 82 + .../config/IBaseMapper.java | 16 + .../config/InsertBatchSqlInjector.java | 22 + .../config/Log4j2Configuration.java | 62 + .../config/LogPoolManager.java | 65 + .../config/MyBatisPlusConfig.java | 41 + .../config/SwaggerConfig.java | 82 + .../controller/BackageFileController.java | 339 ++++ .../controller/BoardController.java | 102 ++ .../controller/BootLogController.java | 50 + .../controller/ChannelDataController.java | 83 + .../DataBoardChannelController.java | 18 + .../controller/DataBoardController.java | 161 ++ .../controller/DownloadController.java | 18 + .../controller/InstructController.java | 224 +++ .../controller/InvocationController.java | 93 + .../controller/JavaCompilerController.java | 100 ++ .../controller/LogController.java | 78 + .../controller/ParserController.java | 62 + .../controller/ProtocolController.java | 68 + .../controller/SchemeController.java | 59 + .../controller/ScriptController.java | 180 ++ .../controller/SensorEquipmentController.java | 47 + .../controller/ServerTerminalController.java | 76 + .../controller/SystemConfigController.java | 52 + .../dto/BatchChangeInstructParamDto.java | 40 + .../dto/BatchCopyBelowDTO.java | 16 + .../dto/BatchMoveBelowDTO.java | 17 + .../dto/BoardTemperatureDto.java | 17 + .../dto/BoardTemperatureServerDto.java | 17 + .../dto/BootLogDeleteDto.java | 31 + .../dto/BootLogPageDto.java | 29 + .../dto/ChannelDataDto.java | 18 + .../dto/ChannelDataHistoryDto.java | 22 + .../dto/ChannelDataQueryAll.java | 15 + .../dto/ChannelDataStatisticsDto.java | 18 + .../dto/ChannelDto.java | 16 + .../dto/ChannelPageDto.java | 28 + .../dto/CreateGroupDTO.java | 17 + .../dto/DataClearDto.java | 11 + .../dto/DeleteChannelDto.java | 18 + .../dto/DownloadDto.java | 18 + .../dto/ExportDataBoardDto.java | 46 + .../dto/FileInfo.java | 21 + .../dto/InsertDataBoardDto.java | 35 + .../dto/InstructCalcDto.java | 13 + .../dto/InstructDeleteDto.java | 13 + .../dto/InstructDeleteFileDto.java | 14 + .../dto/InstructGroupDto.java | 35 + .../dto/InstructItem.java | 164 ++ .../dto/InstructPageDto.java | 17 + .../dto/InstructXmlDTO.java | 21 + .../dto/InvocationPageDto.java | 17 + .../dto/InvocationSortDto.java | 11 + .../dto/PageBoard.java | 19 + .../dto/PageChannelDataDto.java | 30 + .../dto/PageLogDto.java | 13 + .../dto/PageServerInfoDto.java | 14 + .../dto/ProtocolImport.java | 295 ++++ .../dto/ProtocolNameListDto.java | 15 + .../dto/ProtocolPageDto.java | 16 + .../dto/ScriptContentDto.java | 14 + .../dto/ScriptDto.java | 13 + .../dto/ScriptFileContentDto.java | 13 + .../dto/SensorEquipmentDto.java | 32 + .../dto/SensorEquipmentPageDto.java | 15 + .../dto/ServerPageChannelDataDto.java | 27 + .../dto/SortDto.java | 16 + .../dto/SubmitAndCompile.java | 25 + .../entity/Board.java | 68 + .../entity/BootLog.java | 65 + .../entity/Channel.java | 65 + .../entity/ChannelData.java | 46 + .../entity/ChannelDataHistory.java | 61 + .../entity/ChannelDataHit.java | 41 + .../entity/ChannelDataTraffic.java | 36 + .../entity/DataBoard.java | 50 + .../entity/DataBoardChannel.java | 75 + .../entity/Download.java | 59 + .../entity/Instruct.java | 190 ++ .../entity/Invocation.java | 48 + .../entity/ParseData.java | 36 + .../entity/Parser.java | 61 + .../entity/ParserProtocol.java | 48 + .../entity/Protocol.java | 68 + .../entity/Scheme.java | 41 + .../entity/SensorEquipment.java | 39 + .../entity/ServerInfo.java | 76 + .../entity/SystemConfig.java | 26 + .../exception/BaseException.java | 26 + .../exception/CustomizeException.java | 28 + .../DbInsertOrUpdateErrorExcetpion.java | 10 + .../exception/GlobalExceptionHandler.java | 97 + .../mapper/BootLogMapper.java | 17 + .../mapper/ChannelDataHitMapper.java | 18 + .../mapper/ChannelDataMapper.java | 18 + .../mapper/ChannelDataTrafficMapper.java | 18 + .../mapper/DataBoardChannelMapper.java | 20 + .../mapper/DataBoardMapper.java | 16 + .../mapper/DownloadMapper.java | 16 + .../mapper/InstructMapper.java | 70 + .../mapper/InvocationMapper.java | 22 + .../mapper/ParserMapper.java | 18 + .../mapper/ParserProtocolMapper.java | 18 + .../mapper/ProtocolMapper.java | 16 + .../mapper/SchemeMapper.java | 16 + .../mapper/SensorEquipmentMapper.java | 7 + .../mapper/ServerMapper.java | 16 + .../mapper/SystemConfigMapper.java | 18 + .../nettyclient/config/NettyClientConfig.java | 33 + .../nettyclient/config/ServerConfig.java | 19 + .../connection/ConnectionManager.java | 332 ++++ .../connection/ServerConnection.java | 650 +++++++ .../nettyclient/entity/FrameDecoder.java | 56 + .../nettyclient/entity/FrameEncoder.java | 27 + .../nettyclient/entity/NoticeMessage.java | 12 + .../nettyclient/entity/ReceiveTcpContent.java | 93 + .../nettyclient/entity/SendTcpContent.java | 105 ++ .../parser/ParserManager.java | 161 ++ .../parser/ParserMessageQueueConsumer.java | 125 ++ .../parser/ParserMessageQueueManager.java | 86 + .../parser/ParserThread.java | 184 ++ .../parser/ProtocolParserHelper.java | 48 + .../parser/ProtocolThread.java | 529 ++++++ .../parser/enums/ProtocolEnum.java | 59 + .../parser/model/Filed.java | 302 ++++ .../parser/model/FiledParseData.java | 29 + .../parser/model/Filed_Back.java | 283 +++ .../parser/model/FrameHeader.java | 22 + .../parser/model/Meta.java | 82 + .../parser/model/OriginalData.java | 17 + .../parser/model/ParserMessage.java | 32 + .../parser/model/ProtocolXml.java | 53 + .../parser/model/VariableLengthDesc.java | 39 + .../script/AccumulatePreTreatment.java | 28 + .../script/CrcCheck.java | 31 + .../script/DigitalParser.java | 50 + .../script/ScriptManager.java | 501 ++++++ .../script/ScriptTemplate.java | 183 ++ .../script/TemperatureParser.java | 33 + .../script/TestBranchCondition.java | 26 + .../script/TestCheckScript.java | 23 + .../script/TestScript.java | 8 + .../script/VoltageParser.java | 46 + .../service/BoardService.java | 48 + .../service/ChannelDataHitService.java | 13 + .../service/ChannelDataService.java | 34 + .../service/ChannelDataTrafficService.java | 13 + .../service/DataBoardChannelService.java | 16 + .../service/DataBoardService.java | 39 + .../service/DownloadService.java | 16 + .../service/IBootLogService.java | 24 + .../service/InstructService.java | 72 + .../service/InvocationService.java | 34 + .../service/JavaCompileService.java | 297 ++++ .../service/ParserProtocolService.java | 13 + .../service/ParserService.java | 28 + .../service/ProtocolService.java | 29 + .../service/SchemeService.java | 22 + .../service/SensorEquipmentService.java | 21 + .../service/ServeService.java | 28 + .../service/SystemConfigService.java | 23 + .../service/impl/BoardServiceImpl.java | 273 +++ .../service/impl/BootLogServiceImpl.java | 80 + .../impl/ChannelDataHitServiceImpl.java | 22 + .../service/impl/ChannelDataServiceImpl.java | 491 ++++++ .../impl/ChannelDataTrafficServiceImpl.java | 22 + .../impl/DataBoardChannelServiceImpl.java | 20 + .../service/impl/DataBoardServiceImpl.java | 759 ++++++++ .../service/impl/DownloadServiceImpl.java | 20 + .../service/impl/InstructServiceImpl.java | 1557 +++++++++++++++++ .../service/impl/InvocationServiceImpl.java | 367 ++++ .../impl/ParserProtocolServiceImpl.java | 22 + .../service/impl/ParserServiceImpl.java | 176 ++ .../service/impl/ProtocolServiceImpl.java | 399 +++++ .../service/impl/SchemeServiceImpl.java | 83 + .../impl/SensorEquipmentServiceImpl.java | 98 ++ .../service/impl/ServeServiceImpl.java | 127 ++ .../service/impl/SystemConfigServiceImpl.java | 77 + .../timing/ChannelDataTiming.java | 29 + .../util/CalcUtils.java | 122 ++ .../util/ConverUtils.java | 79 + .../util/DataUtils.java | 274 +++ .../util/DateUtils.java | 51 + .../util/DynamicClassLoader.java | 13 + .../util/FileUtils.java | 37 + .../util/MediaTypeUtils.java | 56 + .../util/ModbusCRC16Utils.java | 89 + .../util/ProtocolToolUtils.java | 37 + .../util/Result.java | 105 ++ .../util/ResultCode.java | 263 +++ .../util/SplitUtils.java | 25 + .../util/UUIDUtils.java | 21 + .../util/XmlToObjectConverter.java | 40 + .../util/YamlUtils.java | 86 + .../vo/BoardListVo.java | 34 + .../vo/ChannelDataHistoryVo.java | 45 + .../vo/ChannelDataStatisticsVo.java | 21 + .../vo/ChannelDataVo.java | 21 + .../vo/ChannelListVo.java | 13 + .../vo/ExportDataBoardVo.java | 31 + .../vo/InstructScriptVo.java | 16 + .../vo/InvocationInstructVo.java | 17 + .../vo/ProtocolSaveVo.java | 11 + .../vo/ScriptTypeVo.java | 15 + .../vo/ScriptVo.java | 21 + .../vo/ServerListVo.java | 36 + .../DataPlayBackWebSocketConfig.java | 24 + .../DataPlayBackWebSocketHandler.java | 74 + .../websocket/DataWebNettyWebSocket.java | 54 + .../websocket/DataWebSocketConfig.java | 24 + .../websocket/DataWebSocketHandler.java | 71 + .../websocket/NoticeWebSocketConfig.java | 23 + .../websocket/NoticeWebSocketHandler.java | 74 + .../websocket/WebSocketController.java | 21 + .../command/CommandClientMessage.java | 126 ++ .../websocket/command/CommandRunMessage.java | 64 + .../command/CommandWebSocketConfig.java | 42 + .../command/CommandWebSocketHandler.java | 160 ++ .../command/InvocationRunThread.java | 633 +++++++ .../command/InvocationRunThreadBack.java | 631 +++++++ .../websocket/command/SchemeRunThread.java | 218 +++ src/main/resources/application-dev.yml | 75 + src/main/resources/application-pro.yml | 74 + src/main/resources/application.yml | 23 + .../db/migration/V1_0__init_client.sql | 236 +++ .../migration/V1_10__add_channel_data_raw.sql | 1 + .../db/migration/V1_1__sensor_equipment.sql | 11 + .../db/migration/V1_2__instruct_change.sql | 4 + .../migration/V1_3__instruct_data_split.sql | 2 + .../db/migration/V1_4__instruct_tail_time.sql | 1 + .../db/migration/V1_5__data_board_channel.sql | 3 + .../V1_6__channel_data_statistics.sql | 20 + .../resources/db/migration/V1_7__1_27.sql | 3 + .../resources/db/migration/V1_8__download.sql | 13 + .../resources/db/migration/V1_9__2_10.sql | 3 + src/main/resources/mapper/BootLogMapper.xml | 5 + .../resources/mapper/ChannelDataHitMapper.xml | 18 + .../resources/mapper/ChannelDataMapper.xml | 17 + .../mapper/ChannelDataTrafficMapper.xml | 17 + .../mapper/DataBoardChannelMapper.xml | 19 + src/main/resources/mapper/DataBoardMapper.xml | 18 + src/main/resources/mapper/DownloadMapper.xml | 22 + src/main/resources/mapper/InstructMapper.xml | 116 ++ .../resources/mapper/InvocationMapper.xml | 17 + src/main/resources/mapper/ParserMapper.xml | 19 + .../resources/mapper/ParserProtocolMapper.xml | 17 + src/main/resources/mapper/ProtocolMapper.xml | 20 + src/main/resources/mapper/SchemeMapper.xml | 18 + .../mapper/SensorEquipmentMapper.xml | 7 + src/main/resources/mapper/ServerMapper.xml | 5 + .../resources/mapper/SystemConfigMapper.xml | 15 + src/main/xml/protocol.xml | 163 ++ src/main/xml/protocolForInstruct.xml | 137 ++ src/main/xml/protocolForVoltage.xml | 812 +++++++++ src/main/xml/protocolWithNoHeader.xml | 62 + src/main/xml/protocol_digital.xml | 37 + src/main/xml/protocol_variable_length.xml | 147 ++ ...testsoftwareClientApiApplicationTests.java | 13 + .../controller/BoardControllerTest.java | 119 ++ .../service/impl/BoardServiceImplTest.java | 48 + update_channel_data_structure.sql | 18 + 285 files changed, 22351 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 doc/协议描述.pdf create mode 100644 doc/通用闭环验证系统应用软件研制技术要求-20250918.docx create mode 100644 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml create mode 100644 src/main/java/com/cbsd/client/Test.java create mode 100644 src/main/java/com/cbsd/client/branch/IBranchConditionInterface.java create mode 100644 src/main/java/com/cbsd/client/check/ICheckInterface.java create mode 100644 src/main/java/com/cbsd/client/parse/IParseDataInterface.java create mode 100644 src/main/java/com/cbsd/client/pretreatment/CalculateUtils.java create mode 100644 src/main/java/com/cbsd/client/pretreatment/IPreTreatmentInterface.java create mode 100644 src/main/java/com/cbsd/client/verification/IVerificationInterface.java create mode 100644 src/main/java/com/cbsd/client/verification/VerifyUtils.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/DynamicClassBootstrap.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/UniversaltestsoftwareClientApiApplication.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/boardTask/PushHolder.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/boardTask/PushSession.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/config/BasePage.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/config/ConnectionFactory.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/config/GeneratorCode.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/config/IBaseMapper.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/config/InsertBatchSqlInjector.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/config/Log4j2Configuration.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/config/LogPoolManager.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/config/MyBatisPlusConfig.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/config/SwaggerConfig.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/controller/BackageFileController.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/controller/BoardController.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/controller/BootLogController.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/controller/ChannelDataController.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/controller/DataBoardChannelController.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/controller/DataBoardController.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/controller/DownloadController.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/controller/InstructController.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/controller/InvocationController.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/controller/JavaCompilerController.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/controller/LogController.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/controller/ParserController.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/controller/ProtocolController.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/controller/SchemeController.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/controller/ScriptController.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/controller/SensorEquipmentController.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/controller/ServerTerminalController.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/controller/SystemConfigController.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/BatchChangeInstructParamDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/BatchCopyBelowDTO.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/BatchMoveBelowDTO.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/BoardTemperatureDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/BoardTemperatureServerDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/BootLogDeleteDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/BootLogPageDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/ChannelDataDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/ChannelDataHistoryDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/ChannelDataQueryAll.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/ChannelDataStatisticsDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/ChannelDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/ChannelPageDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/CreateGroupDTO.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/DataClearDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/DeleteChannelDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/DownloadDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/ExportDataBoardDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/FileInfo.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/InsertDataBoardDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructCalcDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructDeleteDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructDeleteFileDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructGroupDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructItem.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructPageDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructXmlDTO.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/InvocationPageDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/InvocationSortDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/PageBoard.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/PageChannelDataDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/PageLogDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/PageServerInfoDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/ProtocolImport.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/ProtocolNameListDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/ProtocolPageDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/ScriptContentDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/ScriptDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/ScriptFileContentDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/SensorEquipmentDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/SensorEquipmentPageDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/ServerPageChannelDataDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/SortDto.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/dto/SubmitAndCompile.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/entity/Board.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/entity/BootLog.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/entity/Channel.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/entity/ChannelData.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/entity/ChannelDataHistory.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/entity/ChannelDataHit.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/entity/ChannelDataTraffic.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/entity/DataBoard.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/entity/DataBoardChannel.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/entity/Download.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/entity/Instruct.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/entity/Invocation.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/entity/ParseData.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/entity/Parser.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/entity/ParserProtocol.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/entity/Protocol.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/entity/Scheme.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/entity/SensorEquipment.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/entity/ServerInfo.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/entity/SystemConfig.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/exception/BaseException.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/exception/CustomizeException.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/exception/DbInsertOrUpdateErrorExcetpion.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/mapper/BootLogMapper.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/mapper/ChannelDataHitMapper.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/mapper/ChannelDataMapper.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/mapper/ChannelDataTrafficMapper.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/mapper/DataBoardChannelMapper.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/mapper/DataBoardMapper.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/mapper/DownloadMapper.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/mapper/InstructMapper.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/mapper/InvocationMapper.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/mapper/ParserMapper.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/mapper/ParserProtocolMapper.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/mapper/ProtocolMapper.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/mapper/SchemeMapper.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/mapper/SensorEquipmentMapper.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/mapper/ServerMapper.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/mapper/SystemConfigMapper.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/config/NettyClientConfig.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/config/ServerConfig.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/connection/ConnectionManager.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/connection/ServerConnection.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/entity/FrameDecoder.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/entity/FrameEncoder.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/entity/NoticeMessage.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/entity/ReceiveTcpContent.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/entity/SendTcpContent.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/parser/ParserManager.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/parser/ParserMessageQueueConsumer.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/parser/ParserMessageQueueManager.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/parser/ParserThread.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/parser/ProtocolParserHelper.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/parser/ProtocolThread.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/parser/enums/ProtocolEnum.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/parser/model/Filed.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/parser/model/FiledParseData.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/parser/model/Filed_Back.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/parser/model/FrameHeader.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/parser/model/Meta.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/parser/model/OriginalData.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/parser/model/ParserMessage.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/parser/model/ProtocolXml.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/parser/model/VariableLengthDesc.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/script/AccumulatePreTreatment.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/script/CrcCheck.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/script/DigitalParser.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/script/ScriptManager.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/script/ScriptTemplate.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/script/TemperatureParser.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/script/TestBranchCondition.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/script/TestCheckScript.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/script/TestScript.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/script/VoltageParser.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/BoardService.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/ChannelDataHitService.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/ChannelDataService.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/ChannelDataTrafficService.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/DataBoardChannelService.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/DataBoardService.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/DownloadService.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/IBootLogService.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/InstructService.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/InvocationService.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/JavaCompileService.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/ParserProtocolService.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/ParserService.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/ProtocolService.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/SchemeService.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/SensorEquipmentService.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/ServeService.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/SystemConfigService.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/impl/BoardServiceImpl.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/impl/BootLogServiceImpl.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ChannelDataHitServiceImpl.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ChannelDataServiceImpl.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ChannelDataTrafficServiceImpl.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/impl/DataBoardChannelServiceImpl.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/impl/DataBoardServiceImpl.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/impl/DownloadServiceImpl.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/impl/InstructServiceImpl.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/impl/InvocationServiceImpl.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ParserProtocolServiceImpl.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ParserServiceImpl.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ProtocolServiceImpl.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/impl/SchemeServiceImpl.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/impl/SensorEquipmentServiceImpl.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ServeServiceImpl.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/service/impl/SystemConfigServiceImpl.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/timing/ChannelDataTiming.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/util/CalcUtils.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/util/ConverUtils.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/util/DataUtils.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/util/DateUtils.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/util/DynamicClassLoader.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/util/FileUtils.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/util/MediaTypeUtils.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/util/ModbusCRC16Utils.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/util/ProtocolToolUtils.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/util/Result.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/util/ResultCode.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/util/SplitUtils.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/util/UUIDUtils.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/util/XmlToObjectConverter.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/util/YamlUtils.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/vo/BoardListVo.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/vo/ChannelDataHistoryVo.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/vo/ChannelDataStatisticsVo.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/vo/ChannelDataVo.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/vo/ChannelListVo.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/vo/ExportDataBoardVo.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/vo/InstructScriptVo.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/vo/InvocationInstructVo.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/vo/ProtocolSaveVo.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/vo/ScriptTypeVo.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/vo/ScriptVo.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/vo/ServerListVo.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/websocket/DataPlayBackWebSocketConfig.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/websocket/DataPlayBackWebSocketHandler.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/websocket/DataWebNettyWebSocket.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/websocket/DataWebSocketConfig.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/websocket/DataWebSocketHandler.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/websocket/NoticeWebSocketConfig.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/websocket/NoticeWebSocketHandler.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/websocket/WebSocketController.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/CommandClientMessage.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/CommandRunMessage.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/CommandWebSocketConfig.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/CommandWebSocketHandler.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/InvocationRunThread.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/InvocationRunThreadBack.java create mode 100644 src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/SchemeRunThread.java create mode 100644 src/main/resources/application-dev.yml create mode 100644 src/main/resources/application-pro.yml create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/db/migration/V1_0__init_client.sql create mode 100644 src/main/resources/db/migration/V1_10__add_channel_data_raw.sql create mode 100644 src/main/resources/db/migration/V1_1__sensor_equipment.sql create mode 100644 src/main/resources/db/migration/V1_2__instruct_change.sql create mode 100644 src/main/resources/db/migration/V1_3__instruct_data_split.sql create mode 100644 src/main/resources/db/migration/V1_4__instruct_tail_time.sql create mode 100644 src/main/resources/db/migration/V1_5__data_board_channel.sql create mode 100644 src/main/resources/db/migration/V1_6__channel_data_statistics.sql create mode 100644 src/main/resources/db/migration/V1_7__1_27.sql create mode 100644 src/main/resources/db/migration/V1_8__download.sql create mode 100644 src/main/resources/db/migration/V1_9__2_10.sql create mode 100644 src/main/resources/mapper/BootLogMapper.xml create mode 100644 src/main/resources/mapper/ChannelDataHitMapper.xml create mode 100644 src/main/resources/mapper/ChannelDataMapper.xml create mode 100644 src/main/resources/mapper/ChannelDataTrafficMapper.xml create mode 100644 src/main/resources/mapper/DataBoardChannelMapper.xml create mode 100644 src/main/resources/mapper/DataBoardMapper.xml create mode 100644 src/main/resources/mapper/DownloadMapper.xml create mode 100644 src/main/resources/mapper/InstructMapper.xml create mode 100644 src/main/resources/mapper/InvocationMapper.xml create mode 100644 src/main/resources/mapper/ParserMapper.xml create mode 100644 src/main/resources/mapper/ParserProtocolMapper.xml create mode 100644 src/main/resources/mapper/ProtocolMapper.xml create mode 100644 src/main/resources/mapper/SchemeMapper.xml create mode 100644 src/main/resources/mapper/SensorEquipmentMapper.xml create mode 100644 src/main/resources/mapper/ServerMapper.xml create mode 100644 src/main/resources/mapper/SystemConfigMapper.xml create mode 100644 src/main/xml/protocol.xml create mode 100644 src/main/xml/protocolForInstruct.xml create mode 100644 src/main/xml/protocolForVoltage.xml create mode 100644 src/main/xml/protocolWithNoHeader.xml create mode 100644 src/main/xml/protocol_digital.xml create mode 100644 src/main/xml/protocol_variable_length.xml create mode 100644 src/test/java/com/cbsd/universaltestsoftware_client/UniversaltestsoftwareClientApiApplicationTests.java create mode 100644 src/test/java/com/cbsd/universaltestsoftware_client/controller/BoardControllerTest.java create mode 100644 src/test/java/com/cbsd/universaltestsoftware_client/service/impl/BoardServiceImplTest.java create mode 100644 update_channel_data_structure.sql diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..9541d5f --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(ls \"e:\\\\projects\\\\wxwx\\\\projects_job\\\\UniversalTestSoftware\\\\UDP_Projects\\\\Code\\\\Client\\\\Back_end\\\\\")", + "Read(//e/projects/wxwx/projects_job/UniversalTestSoftware/UDP_Projects/Code/Client/**)" + ] + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3b41682 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..667aaef --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +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/ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..12fbe1e --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/doc/协议描述.pdf b/doc/协议描述.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f46b6a98ba95e48dece8510cc08e7bfa5dcc2994 GIT binary patch literal 55521 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 G_WuG>1#gi6 literal 0 HcmV?d00001 diff --git a/doc/通用闭环验证系统应用软件研制技术要求-20250918.docx b/doc/通用闭环验证系统应用软件研制技术要求-20250918.docx new file mode 100644 index 0000000000000000000000000000000000000000..f9f5d965ffe4e7e6dc35e3b02e975a5074667ff2 GIT binary patch literal 37495 zcmeFXV~;M*69qVTY}>YN+qP}nwr$(SGrnWn)*aj4-#?pdc9Z=EyZxeHb-L1(I;pO6 zj-m_*C@K&*5F`)~5D^do3FxQeA0QxNa3CNQAV^>x5eIu$GkaG9RWC;~7d?7UJ6pnH zP+-bJAmIPb|Nra%!x?Bzosu77L==0K{1Q2)liqF?gk-x89z&*9;vZ9wG>0WWD|+dwv6+)b-Bc+v!y_b%J1H6=*N_{ukf3RC&(2|>0c zXk2peykgi4mB_5zt=i0G?77c7!KIl5O)#KT_OJk5YLa_TcwCYBr~mr-rt=AY&?vqQ z0tY_75E&+9<1yQxgA_^Dc*A5#(;`ha)UXv*(yh<+LPEMvhElTYTFM1)5sr<~pRuU^ z3%_xxcVR`eju0m%vf-~XhQWJf=YzA6looCW*T8v=w2b9*n@*&@u(a#xDyrQG^!3YE zzjJ3;o-pajyO8#!d}JsN$oUs&oP!51Ve&pQ5YMT6D4OgZgZTYS4@znAc2p)dr+~Q4 z4pdMpc;Wkt!oT%R+?di?rzfHX?WkW&pMDN&H+c|<$jXF%0DDrcLO#MpTB+bSn+`p) zw~w>0i3?$_1IO1L3=q)oF9?w0{{yI5YW`q9|5e|?e>jBs52yytX0|R2^#9xce}Mjf z7?uCGr`IOS$$&5+hTjJN5>5B=Y*7<0?&yo2$(ert5n=ue%0Wo3YX9A(jHX|1i@;~` zI-Otd~0J$#0Ec$>g%e5o?rcuYCy{(~t4MHx~|90Z?3JJ0^^vO1>!2f zk)$S&;yY|c<`1Y)AT=-)dMzH;U(jmk3g&*^)8mO0in;fUfZm0zr56)U?8}=NUEZ;u zD6Td1c-JR4cZqs^-q#F=YlT!7yCzS{MXZbSm{5lNZmjsc&ozbL&w$U2OAPK$?)v9& zl6!VZV&rbe-u|P*u6oa&Utu4g+7g;=oapbWj&71ojLUB1Ulj=h2cNw(njO4@mrK8e zS@(D&tgkA8%{8R>k^RqD#p5x>t6Yitqi_N1yoGtUHxp4s%iA+(v7-Z=2-bBK6r`;c z_Y+Q{dU)Z+2wb0B)BtN`WghDlCShT{P{W#dCFIKz)iM`lztqm7)Z-7z`!Oo2XC>}Y z`Q9nipX*dZbzftlCQ`i_X!FVkq+BNJAns6~b^)`K&i7`pQ?T;I1tKp_=K#&eZ#G-#&@ zXlZ=o+(faY$h~E&2d*?n@zS+QZM5rnB_a1t#6UB@o1vM>B&4f84;oZFLp#W~kN`so z+GVVVdtSV*Q|#t~gUG$22BkVOw(xzlf|z0{WvUV>?CR2*(xOGA`I~ED6j?Bb&(vt+ zueC|=hgR#Y_AgP}&1j__x9Y}P(!DFRcK$#HO_8Qa+`EfLC4u$BZahpzz4%9Q(G*{! zn4<4iA!z#nlHhMdZ0yf|!U4`3_aVXUP#XSI(W5qwa?~k*nE3ul-)zpAgFp|F?iqrZ zNJb&-eKx&Iv2Fg4#sMm}gofEIL67$z?A*M)|LT!3ePA8g0AL#45L*8{yny$B&|mg@ zN%`?efOpqKAd>jXT_}toG1pd7qvZU^{Nub4dC=eVOUq9UdEB%ffZcS+mL#&a*}K=C zp)dGz>ev2B?DP~-1j~Qs}|@b>T!$y8vmuBPOdv^w*?EIJm}4? z93T*#@6EqMd*co|Rm1@ZC)Icoh4-b5T}IGnNF7TUa7VE)iMxMMlCJH0Oy@D!Em&=fDS>jnizNHRQq_BJxz&cnZ7 z@EJ_iic7O(8N0Qur`^`~c=dm_bC)77(FU&pA7NfUD;;Jg%p&iFh`E#i=wr0?>wQFr z10W{uw`Z;CS>W3ZLP)c7woft?50a23&B~z}@Q<69-m;?p&Zl-~c=z+%+8l$5CvhIp ziq~UXsNi*_33-@f@|H5%?i3?jX-c(V$~Ox@Uxh~ujE0N`sqioTu63*y_!W*p<{DFu zW}U^Fmz>g?=T+n=lJ0;(V>a(y#GwRH7dsI%k0~nZzxQT@4KIG~EPM@`cvzG;1@pqi z0UlUPJsoYnzi%=L?A1^Q8^vBsG7owt@@joXk;6tnn@cBrlQ$t~=8Jt#W`CgTtSPus zwhZ1R{$`{Mnxg5MPj|t>4GGSbY_E2lz=d{ zW~=z|FoTPsq8GQxd=srRYzyA>^ICEX&vjEOh#dARsPQ8- z*{rYu9Z+qASp|b{%0Dmp2&$%fik$igvMf@pWHedscgKcIiEoM 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 literal 0 HcmV?d00001 diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..19529dd --- /dev/null +++ b/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..249bdf3 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..1b0fb0f --- /dev/null +++ b/pom.xml @@ -0,0 +1,275 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.4.5 + + + com.example + universaltestsoftware_client_api + 0.0.1-SNAPSHOT + universaltestsoftware_client_api + universaltestsoftware_client_api + + + + + + + + + + + + + + + 1.8 + 1.8 + 1.8 + + + + + + + com.fasterxml.jackson.core + jackson-annotations + 2.17.1 + + + + org.flywaydb + flyway-core + 5.2.1 + + + + com.opencsv + opencsv + 5.9 + + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + 2.17.1 + + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-properties + 2.17.1 + + + + commons-io + commons-io + 2.5 + + + com.alibaba + druid-spring-boot-starter + 1.2.16 + + + + + org.springframework.boot + spring-boot-starter-log4j2 + + + + commons-io + commons-io + 2.14.0 + + + + com.alibaba + easyexcel + 4.0.3 + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-logging + + + + + + + io.netty + netty-all + 4.1.58.Final + + + + org.springframework.boot + spring-boot-starter-websocket + + + org.springframework.boot + spring-boot-starter-test + test + + + mysql + mysql-connector-java + 5.1.49 + runtime + + + com.baomidou + mybatis-plus-boot-starter + 3.5.2 + + + + com.baomidou + mybatis-plus-generator + 3.5.3.1 + + + org.freemarker + freemarker + 2.3.32 + + + io.springfox + springfox-boot-starter + 3.0.0 + + + io.springfox + springfox-swagger2 + 3.0.0 + + + + io.springfox + springfox-swagger-ui + 3.0.0 + + + + com.alibaba + fastjson + 1.2.83 + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.apache.httpcomponents + httpclient + + + org.apache.httpcomponents + httpmime + + + + + cn.hutool + hutool-all + 5.8.20 + + + + com.sun.xml.bind + jaxb-core + 2.3.0 + + + javax.xml.bind + jaxb-api + 2.3.1 + + + com.sun.xml.bind + jaxb-impl + 2.3.1 + + + com.sun.istack + istack-commons-runtime + 3.0.11 + + + org.apache.commons + commons-lang3 + 3.12.0 + + + io.reactivex.rxjava3 + rxjava + 3.1.6 + + + + org.springframework.boot + spring-boot-loader + provided + + + + org.yeauty + netty-websocket-spring-boot-starter + 0.12.0 + + + + + + + + + + org.springframework.cloud + spring-cloud-dependencies + 2020.0.3 + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + + + + + + + diff --git a/src/main/java/com/cbsd/client/Test.java b/src/main/java/com/cbsd/client/Test.java new file mode 100644 index 0000000..10cc859 --- /dev/null +++ b/src/main/java/com/cbsd/client/Test.java @@ -0,0 +1,6 @@ +package com.cbsd.client; + +public interface Test { + public String test(); + +} diff --git a/src/main/java/com/cbsd/client/branch/IBranchConditionInterface.java b/src/main/java/com/cbsd/client/branch/IBranchConditionInterface.java new file mode 100644 index 0000000..055353f --- /dev/null +++ b/src/main/java/com/cbsd/client/branch/IBranchConditionInterface.java @@ -0,0 +1,29 @@ +package com.cbsd.client.branch; + +import java.util.Map; + +/** + * 分支条件脚本 + * 根据通道绑定的ICD的协议中的字段,对数据进行判断返回需要跳转的指令名称 + */ +public interface IBranchConditionInterface { + + /** + * 分支条件 + * @param originalCommand 原始指令数据 + * @param preTreatmentCommand 预处理指令数据,若无预处理,则为原始指令数据 + * @param repeatCount 重复次数 + * @param hitCount 命中次数 + * @param latestHitTime 最后命中时间 + * @param dataMap 数据集合,key为协议字段属性名(label),value为物理值 + * @return 需要跳转的指令名称,为空则为不跳转 + */ + String branchCondition(String originalCommand, String preTreatmentCommand, int repeatCount, int hitCount, String latestHitTime, Map dataMap); + + /** + * 描述 + * + * @return 描述内容 + */ + String comment(); +} diff --git a/src/main/java/com/cbsd/client/check/ICheckInterface.java b/src/main/java/com/cbsd/client/check/ICheckInterface.java new file mode 100644 index 0000000..68cd068 --- /dev/null +++ b/src/main/java/com/cbsd/client/check/ICheckInterface.java @@ -0,0 +1,30 @@ +package com.cbsd.client.check; + +import java.util.List; +import java.util.Map; + +/** + * 检查脚本接口 + * 根据通道绑定的ICD的协议中的字段,对数据进行检查并写入日志 + */ +public interface ICheckInterface { + + /** + * 检查 + * @param originalCommand 原始指令数据 + * @param preTreatmentCommand 预处理指令数据,若无预处理,则为原始指令数据 + * @param repeatCount 重复次数 + * @param hitCount 命中次数 + * @param latestHitTime 最后命中时间 + * @param dataMap 数据集合,key为协议字段属性名(label),value为物理值 + * @return 日志信息列表 + */ + List check(String originalCommand, String preTreatmentCommand, int repeatCount, int hitCount, String latestHitTime, Map dataMap); + + /** + * 描述 + * + * @return 描述内容 + */ + String comment(); +} diff --git a/src/main/java/com/cbsd/client/parse/IParseDataInterface.java b/src/main/java/com/cbsd/client/parse/IParseDataInterface.java new file mode 100644 index 0000000..c3f08cb --- /dev/null +++ b/src/main/java/com/cbsd/client/parse/IParseDataInterface.java @@ -0,0 +1,23 @@ +package com.cbsd.client.parse; + +public interface IParseDataInterface { + + /** + * 从数据解析物理值 + * + * @param sn 当前字段序号 + * @param sourceData 源数据 + * @param fieldData 当前字段数据 + * @param endian BIG:大端序 LITTLE:小端序 + * @param binStr 二进制字符串 + * @return 解析后的数据值 + */ + String parsePhysicalFromData(int sn, byte[] sourceData, byte[] fieldData, String endian, String binStr); + + /** + * 当前解析描述 + * + * @return 描述内容 + */ + String comment(); +} diff --git a/src/main/java/com/cbsd/client/pretreatment/CalculateUtils.java b/src/main/java/com/cbsd/client/pretreatment/CalculateUtils.java new file mode 100644 index 0000000..8dac80e --- /dev/null +++ b/src/main/java/com/cbsd/client/pretreatment/CalculateUtils.java @@ -0,0 +1,4 @@ +package com.cbsd.client.pretreatment; + +public class CalculateUtils { +} diff --git a/src/main/java/com/cbsd/client/pretreatment/IPreTreatmentInterface.java b/src/main/java/com/cbsd/client/pretreatment/IPreTreatmentInterface.java new file mode 100644 index 0000000..d9a74f5 --- /dev/null +++ b/src/main/java/com/cbsd/client/pretreatment/IPreTreatmentInterface.java @@ -0,0 +1,30 @@ +package com.cbsd.client.pretreatment; + +import java.util.Map; + +/** + * 预处理脚本接口 + * 指令数据、命中次数、命中时间、重复次数、 校验(内置方法)? + */ +public interface IPreTreatmentInterface { + + /** + * 预处理 + * + * @param command 指令内容 + * @param repeatCount 重复次数 + * @param hitCount 命中次数 + * @param latestHitTime 最后命中时间 + * @param dataMap 数据集合,key为协议字段属性名(label),value为物理值 + * @param calculateUtils 计算工具类 + * @return 结果 + */ + String pretreatment(String command, int repeatCount, int hitCount, String latestHitTime, Map dataMap, CalculateUtils calculateUtils); + + /** + * 描述 + * + * @return 描述内容 + */ + String comment(); +} diff --git a/src/main/java/com/cbsd/client/verification/IVerificationInterface.java b/src/main/java/com/cbsd/client/verification/IVerificationInterface.java new file mode 100644 index 0000000..bb8d401 --- /dev/null +++ b/src/main/java/com/cbsd/client/verification/IVerificationInterface.java @@ -0,0 +1,20 @@ +package com.cbsd.client.verification; + +public interface IVerificationInterface { + /** + * 校验 + * + * @param data 校验数据 + * @param verify 校验位 + * @param verifyUtils 校验工具类 + * @return 校验结果 + */ + boolean verification(byte[] data, byte[] verify, VerifyUtils verifyUtils); + + /** + * 当前解析描述 + * + * @return 描述内容 + */ + String comment(); +} diff --git a/src/main/java/com/cbsd/client/verification/VerifyUtils.java b/src/main/java/com/cbsd/client/verification/VerifyUtils.java new file mode 100644 index 0000000..c76e7d9 --- /dev/null +++ b/src/main/java/com/cbsd/client/verification/VerifyUtils.java @@ -0,0 +1,55 @@ +package com.cbsd.client.verification; + +public class VerifyUtils { + + + /** + * 验证数据的CRC16校验值 + * + * @param data 包含CRC校验值的数据(CRC在数据末尾,2字节) + * @return 校验结果,true表示校验通过,false表示校验失败 + */ + public boolean verifyCRC(byte[] data) { + if (data == null || data.length < 2) { + return false; + } + + // 提取数据部分(不包含CRC) + byte[] dataWithoutCRC = new byte[data.length - 2]; + System.arraycopy(data, 0, dataWithoutCRC, 0, data.length - 2); + + // 计算CRC + byte[] calculatedCRC = calculateCRC(dataWithoutCRC); + + // 比较计算得到的CRC与数据中包含的CRC + return calculatedCRC[0] == data[data.length - 2] && + calculatedCRC[1] == data[data.length - 1]; + } + + /** + * 计算Modbus CRC16校验值 + * + * @param data 待校验的数据 + * @return CRC16校验值,2字节数组 + */ + private byte[] calculateCRC(byte[] data) { + int crc = 0xFFFF; + for (byte b : data) { + crc ^= (b & 0xFF); + for (int i = 0; i < 8; i++) { + if ((crc & 0x0001) != 0) { + crc >>= 1; + crc ^= 0xA001; + } else { + crc >>= 1; + } + } + } + // 将CRC转换为字节数组(低字节在前,高字节在后) + byte[] result = new byte[2]; + result[0] = (byte) (crc & 0xFF); + result[1] = (byte) ((crc >> 8) & 0xFF); + + return result; + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/DynamicClassBootstrap.java b/src/main/java/com/cbsd/universaltestsoftware_client/DynamicClassBootstrap.java new file mode 100644 index 0000000..8f631b1 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/DynamicClassBootstrap.java @@ -0,0 +1,59 @@ +package com.cbsd.universaltestsoftware_client; + +import com.cbsd.universaltestsoftware_client.util.DynamicClassLoader; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class DynamicClassBootstrap { + + @Value("${cbsd.uploadfilesrootpath}") + private String rootPath; + + // 最终缓存:全限定类名 → Class,运行时直接拿 + public static final Map> CLAZZ_CACHE = new ConcurrentHashMap<>(); + +// @PostConstruct // Spring 容器初始化完成后立即执行 + public void loadAllCompiledClasses() throws Exception { + Path root = Paths.get(rootPath); + if (!Files.exists(root)) { + return; + } + + // 自定义类加载器 + DynamicClassLoader loader = new DynamicClassLoader(rootPath); + + // 遍历目录树 + Files.walk(root) + .filter(p -> p.toString().endsWith(".class")) + .forEach(p -> { + // 把文件路径 → 全限定类名 + String fullName = pathToFqn(root, p); + try { + Class clazz = loader.loadClass(fullName); + CLAZZ_CACHE.put(fullName, clazz); + } catch (ClassNotFoundException e) { + // 记录日志即可 + e.printStackTrace(); + } + }); + + System.out.println("已预加载 class 数量:" + CLAZZ_CACHE.size()); + } + + // 工具:把文件路径转换成 com.xxx.yyy.ClassName + private String pathToFqn(Path root, Path classFile) { + String relative = root.relativize(classFile).toString(); + return relative + .replace(File.separatorChar, '.') + .replace(".class", ""); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/UniversaltestsoftwareClientApiApplication.java b/src/main/java/com/cbsd/universaltestsoftware_client/UniversaltestsoftwareClientApiApplication.java new file mode 100644 index 0000000..da689e3 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/UniversaltestsoftwareClientApiApplication.java @@ -0,0 +1,21 @@ +package com.cbsd.universaltestsoftware_client; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; + +@SpringBootApplication +@MapperScan("com.cbsd.*.mapper") +@ComponentScan("com.cbsd.*") +@EnableAsync +@EnableScheduling // 开启定时任务功能 +public class UniversaltestsoftwareClientApiApplication { + + public static void main(String[] args) { + SpringApplication.run(UniversaltestsoftwareClientApiApplication.class, args); + } + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/boardTask/PushHolder.java b/src/main/java/com/cbsd/universaltestsoftware_client/boardTask/PushHolder.java new file mode 100644 index 0000000..34210b7 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/boardTask/PushHolder.java @@ -0,0 +1,173 @@ +package com.cbsd.universaltestsoftware_client.boardTask; + +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.cbsd.universaltestsoftware_client.dto.PageChannelDataDto; +import com.cbsd.universaltestsoftware_client.entity.ChannelData; +import com.cbsd.universaltestsoftware_client.entity.DataBoardChannel; +import com.cbsd.universaltestsoftware_client.mapper.ChannelDataMapper; +import com.cbsd.universaltestsoftware_client.mapper.DataBoardChannelMapper; +import com.cbsd.universaltestsoftware_client.parser.model.FiledParseData; +import com.cbsd.universaltestsoftware_client.util.DateUtils; +import com.cbsd.universaltestsoftware_client.vo.ChannelDataVo; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Component +public class PushHolder { + // 使用ConcurrentHashMap存储每个dataBoardId对应的PushSession,确保线程安全 + private final ConcurrentHashMap POOL = new ConcurrentHashMap<>(); + + @Resource + private DataBoardChannelMapper dataBoardChannelMapper; + + @Resource + private ChannelDataMapper channelDataMapper; + + // 根据dataBoardId获取对应的PushSession + public PushSession get(String id) { + return POOL.get(id); + } + + // 创建一个新的PushSession + public synchronized PushSession create(PageChannelDataDto cmd) { + // 如果已经存在相同的dataBoardId,先停止旧的PushSession + stop(cmd.getDataBoardId()); + PushSession s = new PushSession(); + s.boardId = cmd.getDataBoardId(); + s.pageSize = cmd.getPageSize(); + s.startTime = cmd.getStartTime(); + s.endTime = cmd.getEndTime(); + int want = cmd.getPageSize() * 6; // 初始拉取数据条数为pageSize的4倍 + cmd.setWant(want); + List first = getData(cmd); // 获取第一批数据 + s.queue.addAll(first.subList(0, Math.min(first.size(), want))); // 将第一批数据加入队列 + if (!first.isEmpty()) { + int idx = Math.min(first.size(), want) - 1; + if (idx >= 0) { + long timestamp = first.get(idx).getTimestamp(); + s.lastTime = DateUtils.timestampToDateString(timestamp, "yyyy-MM-dd HH:mm:ss.SSS"); // 更新最后时间 + } + } + s.worker = new Thread(() -> pullLoop(s), "pull-" + s.boardId); // 创建拉取数据的线程 + s.worker.setDaemon(true); + s.worker.start(); + POOL.put(s.boardId, s); // 将新的PushSession加入POOL + return s; + } + + // 停止指定dataBoardId的PushSession + public synchronized void stop(String boardId) { + PushSession s = POOL.remove(boardId); + if (s != null) s.shutdown(); // 调用shutdown方法停止线程 + } + + // 更新指定dataBoardId的分页参数 + public synchronized void update(PageChannelDataDto cmd) { + PushSession s = POOL.get(cmd.getDataBoardId()); + if (s == null) { + create(cmd); // 如果不存在,创建一个新的PushSession + return; + } + // 如果startTime或endTime发生变化,停止旧的PushSession并创建新的 + if (!cmd.getStartTime().equals(s.startTime) || !cmd.getEndTime().equals(s.endTime)) { + s.shutdown(); + create(cmd); + } else { + // 如果只有pageSize发生变化,直接更新pageSize + s.pageSize = cmd.getPageSize(); + } + } + + // 拉取数据的循环逻辑 + private void pullLoop(PushSession s) { + while (s.isRunning()) { + try { + int want = s.pageSize * 5; // 每次拉取数据条数为pageSize的3倍 + if (s.queue.size() < want) { + PageChannelDataDto cmd = new PageChannelDataDto(); + cmd.setWant(want); + cmd.setDataBoardId(s.boardId); + cmd.setStartTime(s.startTime); + cmd.setEndTime(s.endTime); + cmd.setLastTime(s.lastTime); + List chunk = getData(cmd); // 获取一批数据 + if (chunk.isEmpty()) { + break; // 如果没有数据,退出循环 + } + int gap = want - s.queue.size(); + s.queue.addAll(chunk.subList(0, Math.min(chunk.size(), gap))); // 将数据加入队列 + long timestamp = chunk.get(Math.min(chunk.size(), gap) - 1).getTimestamp(); + s.lastTime = DateUtils.timestampToDateString(timestamp, "yyyy-MM-dd HH:mm:ss.SSS"); // 更新最后时间 + } + TimeUnit.SECONDS.sleep(4); // 每5秒拉取一次数据 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + } + + // 根据PageChannelDataDto获取数据 + public List getData(PageChannelDataDto pageChannelDataDto) { + ArrayList channelDataVos = new ArrayList<>(); + try { + LambdaQueryWrapper dataBoardChannelLambdaQueryWrapper = new LambdaQueryWrapper<>(); + dataBoardChannelLambdaQueryWrapper.eq(DataBoardChannel::getDataBoardId, pageChannelDataDto.getDataBoardId()) + .groupBy(DataBoardChannel::getChannelId); + List dataBoardChannelList = dataBoardChannelMapper.selectList(dataBoardChannelLambdaQueryWrapper); + List channelIds = dataBoardChannelList.stream().map(DataBoardChannel::getChannelId).collect(Collectors.toList()); + if (CollectionUtils.isNotEmpty(channelIds)) { + LambdaQueryWrapper channelDataLambdaQueryWrapper = new LambdaQueryWrapper<>(); + channelDataLambdaQueryWrapper + .between(StringUtils.isNotBlank(pageChannelDataDto.getStartTime()) && StringUtils.isNotBlank(pageChannelDataDto.getEndTime()), ChannelData::getTime, pageChannelDataDto.getStartTime(), pageChannelDataDto.getEndTime()) + .in(ChannelData::getChannelId, channelIds) + .gt(StringUtils.isNotBlank(pageChannelDataDto.getLastTime()), ChannelData::getTime, pageChannelDataDto.getLastTime()) + .last("limit " + pageChannelDataDto.getWant()) + .orderByAsc(ChannelData::getTime); + List channelDataList = channelDataMapper.selectList(channelDataLambdaQueryWrapper); + + for (ChannelData channelData : channelDataList) { + ArrayList filedParseData = new ArrayList<>(); + ChannelDataVo parserMessage = new ChannelDataVo(); + parserMessage.setChannelId(channelData.getChannelId()); + + if (StringUtils.isNotBlank(channelData.getData())) { + try { + List filedParseDataList = JSON.parseArray(channelData.getData(), FiledParseData.class); + filedParseData.addAll(filedParseDataList); + } catch (Exception e) { + e.printStackTrace(); + } + } + parserMessage.setFiledParseDataList(filedParseData); + parserMessage.setTimestamp(convertToTimestamp(channelData.getTime())); + parserMessage.setDataBoardId(pageChannelDataDto.getDataBoardId()); + channelDataVos.add(parserMessage); + } + } + }catch (Exception e){ + e.printStackTrace(); + + } + return channelDataVos; + } + + // 将时间字符串转换为时间戳 + public static long convertToTimestamp(String dateTimeStr) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); + LocalDateTime localDateTime = LocalDateTime.parse(dateTimeStr, formatter); + return localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + } +} \ No newline at end of file diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/boardTask/PushSession.java b/src/main/java/com/cbsd/universaltestsoftware_client/boardTask/PushSession.java new file mode 100644 index 0000000..1c9147b --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/boardTask/PushSession.java @@ -0,0 +1,40 @@ +package com.cbsd.universaltestsoftware_client.boardTask; + +import com.cbsd.universaltestsoftware_client.vo.ChannelDataVo; +import lombok.Data; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; + +@Data +public class PushSession { + // 用于控制线程是否运行的原子布尔变量 + private final AtomicBoolean run = new AtomicBoolean(true); + // 每次拉取的数据条数 + volatile int pageSize; + // 上次拉取的最后时间(字符串格式) + volatile String lastTime; + // 数据拉取的开始时间 + volatile String startTime; + // 数据拉取的结束时间 + volatile String endTime; + // 存储数据的阻塞队列 + final LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); + // 数据拉取的线程 + Thread worker; + // 数据板ID + String boardId; + + // 停止线程的方法 + public void shutdown() { + run.set(false); // 设置线程停止标志 + if (worker != null) { + worker.interrupt(); // 中断线程 + } + } + + // 检查线程是否正在运行 + public boolean isRunning() { + return run.get(); + } +} \ No newline at end of file diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/config/BasePage.java b/src/main/java/com/cbsd/universaltestsoftware_client/config/BasePage.java new file mode 100644 index 0000000..198f664 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/config/BasePage.java @@ -0,0 +1,22 @@ +package com.cbsd.universaltestsoftware_client.config; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class BasePage extends Page { + + //关键字查询 + @ApiModelProperty("关键字查询") + private String keyword; + + @ApiModelProperty(name = "开始时间") + private String startTime; + + @ApiModelProperty(name = "结束时间") + private String endTime; + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/config/ConnectionFactory.java b/src/main/java/com/cbsd/universaltestsoftware_client/config/ConnectionFactory.java new file mode 100644 index 0000000..dfde542 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/config/ConnectionFactory.java @@ -0,0 +1,26 @@ +package com.cbsd.universaltestsoftware_client.config; + +import org.apache.logging.log4j.core.appender.db.jdbc.AbstractConnectionSource; +import org.springframework.util.Assert; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +public class ConnectionFactory extends AbstractConnectionSource +{ + private final DataSource dataSource; + + public ConnectionFactory(DataSource dataSource) + { + Assert.notNull(dataSource, "dataSource can not be null"); + this.dataSource = dataSource; + } + + @Override + public Connection getConnection() + throws SQLException + { + return dataSource.getConnection(); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/config/GeneratorCode.java b/src/main/java/com/cbsd/universaltestsoftware_client/config/GeneratorCode.java new file mode 100644 index 0000000..8df22fd --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/config/GeneratorCode.java @@ -0,0 +1,82 @@ +package com.cbsd.universaltestsoftware_client.config; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.generator.FastAutoGenerator; +import com.baomidou.mybatisplus.generator.config.DataSourceConfig; +import com.baomidou.mybatisplus.generator.config.OutputFile; +import com.baomidou.mybatisplus.generator.config.rules.DateType; +import com.baomidou.mybatisplus.generator.config.rules.DbColumnType; +import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine; + +import java.sql.Types; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * @ClassName GeneratorCode + * @Author FY + * @Version 1.0.0 + * @Description 代码生成器 + * @CreateTime 2023年04月22日 15:49:00 + */ +public class + +GeneratorCode { + /** + * 数据源配置 + */ + private static final DataSourceConfig.Builder DATA_SOURCE_CONFIG = new DataSourceConfig + .Builder("jdbc:mysql://192.168.100.40:3306/universaltestsoftware_client_api?serverTimezone=GMT%2B8&characterEncoding=utf8&useSSL=true&allowMultiQueries=true&serverTimezone=GMT%2B8", "root", "Fushoukeji1234"); + + + /** + * 代码生成器 + * + * @param args + */ + public static void main(String[] args) { + FastAutoGenerator.create(DATA_SOURCE_CONFIG) + .globalConfig(builder -> { + builder.author("admin") // 设置作者 + .enableSwagger() // 开启 swagger 模式 + .disableOpenDir() + .dateType(DateType.ONLY_DATE) + .outputDir("C:\\gs\\project\\universaltestsoftware_client_api\\src\\main\\java"); // 指定输出目录 + }) + .dataSourceConfig(builder -> builder.typeConvertHandler((globalConfig, typeRegistry, metaInfo) -> { + int typeCode = metaInfo.getJdbcType().TYPE_CODE; + if (typeCode == Types.SMALLINT) { + // 自定义类型转换 + return DbColumnType.INTEGER; + } + return typeRegistry.getColumnType(metaInfo); + + })) + .packageConfig(builder -> { + builder.entity("entity").parent("com.cbsd.universaltestsoftware_client") // 设置父包名 + .pathInfo(Collections.singletonMap(OutputFile.xml, "C:\\gs\\project\\universaltestsoftware_client_api\\src\\main\\resources\\mapper")); // 设置mapperXml生成路径 + }) + .strategyConfig(builder -> { + builder.addInclude(getTables("data_board,data_board_channel")) // 设置需要生成的表名 + .addTablePrefix(""); // 设置过滤表前缀 + builder.serviceBuilder().formatServiceFileName("%sService"); + builder.entityBuilder().logicDeleteColumnName("del").enableFileOverride().idType(IdType.ASSIGN_ID).enableTableFieldAnnotation().enableLombok().disableSerialVersionUID(); + builder.mapperBuilder().enableFileOverride().enableBaseResultMap().enableBaseColumnList(); + builder.controllerBuilder().enableRestStyle(); + }) + .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板 + .execute(); + + } + + /** + * 处理多表情况 + * + * @param tables 表名 + * @return List + */ + protected static List getTables(String tables) { + return "all".equals(tables) ? Collections.emptyList() : Arrays.asList(tables.split(",")); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/config/IBaseMapper.java b/src/main/java/com/cbsd/universaltestsoftware_client/config/IBaseMapper.java new file mode 100644 index 0000000..f886263 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/config/IBaseMapper.java @@ -0,0 +1,16 @@ +package com.cbsd.universaltestsoftware_client.config; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface IBaseMapper extends BaseMapper { + + /** + * 批量插入 + * @param batchList 批量数据 + * @return insert 数量 + */ + int insertBatchSomeColumn(@Param("list") List batchList); +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/config/InsertBatchSqlInjector.java b/src/main/java/com/cbsd/universaltestsoftware_client/config/InsertBatchSqlInjector.java new file mode 100644 index 0000000..d28b01e --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/config/InsertBatchSqlInjector.java @@ -0,0 +1,22 @@ +package com.cbsd.universaltestsoftware_client.config; + +import com.baomidou.mybatisplus.core.injector.AbstractMethod; +import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.extension.injector.methods.InsertBatchSomeColumn; +import org.apache.ibatis.session.Configuration; + +import java.util.List; + +/** + * 批量插入 SQL 注入器 + */ +public class InsertBatchSqlInjector extends DefaultSqlInjector { + + @Override + public List getMethodList(Class mapperClass, TableInfo tableInfo) { + List methodList = super.getMethodList(mapperClass, tableInfo); + methodList.add(new InsertBatchSomeColumn()); + return methodList; + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/config/Log4j2Configuration.java b/src/main/java/com/cbsd/universaltestsoftware_client/config/Log4j2Configuration.java new file mode 100644 index 0000000..de69858 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/config/Log4j2Configuration.java @@ -0,0 +1,62 @@ +package com.cbsd.universaltestsoftware_client.config; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.appender.db.ColumnMapping; +import org.apache.logging.log4j.core.appender.db.jdbc.ColumnConfig; +import org.apache.logging.log4j.core.appender.db.jdbc.JdbcAppender; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; + +@Component +public class Log4j2Configuration implements ApplicationListener +{ + private final DataSource dataSource; + + public Log4j2Configuration(DataSource dataSource) + { + this.dataSource = dataSource; + } + + @Override + public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) + { + final LoggerContext ctx = LoggerContext.getContext(false); + final ColumnConfig[] cc = + {ColumnConfig.newBuilder().setConfiguration(ctx.getConfiguration()).setName("event_id").setPattern("%X{id}").setUnicode(false).build(), + ColumnConfig.newBuilder().setConfiguration(ctx.getConfiguration()).setName("event_date").setEventTimestamp(true).setUnicode(false).build(), + ColumnConfig.newBuilder().setConfiguration(ctx.getConfiguration()).setName("thread").setPattern("%t %x").setUnicode(false).build(), + ColumnConfig.newBuilder().setConfiguration(ctx.getConfiguration()).setName("log_class").setPattern("%C").setUnicode(false).build(), + ColumnConfig.newBuilder().setConfiguration(ctx.getConfiguration()).setName("method").setPattern("%M").setUnicode(false).build(), + ColumnConfig.newBuilder().setConfiguration(ctx.getConfiguration()).setName("message").setPattern("%m").setUnicode(false).build(), + ColumnConfig.newBuilder().setConfiguration(ctx.getConfiguration()).setName("exception").setPattern("%ex{full}").setUnicode(false).build(), + ColumnConfig.newBuilder().setConfiguration(ctx.getConfiguration()).setName("level").setPattern("%level").setUnicode(false).build(), + ColumnConfig.newBuilder() + .setConfiguration(ctx.getConfiguration()) + .setName("time") + .setPattern("%d{yyyy-MM-dd HH:mm:ss.SSS}") + .setUnicode(false) + .build()}; + + // 配置appender + final Appender appender = JdbcAppender.newBuilder() + .setName("databaseAppender") + .setIgnoreExceptions(false) + .setConnectionSource(new ConnectionFactory(dataSource)) + .setTableName("boot_log") + .setColumnConfigs(cc) + .setColumnMappings(new ColumnMapping[0]) + .build(); + appender.start(); + + ctx.getConfiguration().addAppender(appender); + + // 指定哪些logger输出的日志保存在mysql中 + ctx.getConfiguration().getLoggerConfig("com.cbsd").addAppender(appender, Level.INFO, null); + ctx.updateLoggers(); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/config/LogPoolManager.java b/src/main/java/com/cbsd/universaltestsoftware_client/config/LogPoolManager.java new file mode 100644 index 0000000..758c7ca --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/config/LogPoolManager.java @@ -0,0 +1,65 @@ +package com.cbsd.universaltestsoftware_client.config; + +import com.alibaba.druid.pool.DruidDataSource; +import com.baomidou.mybatisplus.core.toolkit.Assert; + +import com.cbsd.universaltestsoftware_client.util.YamlUtils; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; + +import javax.sql.DataSource; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Properties; + +public final class LogPoolManager { + private static DataSource dataSource; + private static String configLocation; + + private LogPoolManager() { + super(); + } + + public static void setConfigLocation(String configLocation) { + LogPoolManager.configLocation = configLocation; + } + + public static synchronized void init() { + if (dataSource != null) { + return; // 避免重复初始化 + } + + try { + if (StringUtils.isNotBlank(configLocation)) { + File file = new File(configLocation); + String text = FileUtils.readFileToString(file, StandardCharsets.UTF_8); + Properties props = YamlUtils.yamlToProperties(text); + + // 直接实例化DruidDataSource,而非通过DataSourceBuilder + DruidDataSource druidDataSource = new DruidDataSource(); + druidDataSource.setUrl(props.getProperty("spring.datasource.url")); + druidDataSource.setUsername(props.getProperty("spring.datasource.username")); + druidDataSource.setPassword(props.getProperty("spring.datasource.password")); + + // 关键:禁用SQL解析功能(解决ResultSetColumnMetaData错误) + druidDataSource.setPoolPreparedStatements(false); + druidDataSource.setFilters("stat"); // 只保留统计功能,移除wall等可能引起解析的过滤器 + druidDataSource.setProxyFilters(null); // 清空代理过滤器 + + dataSource = druidDataSource; + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static Connection getConnection() throws SQLException { + if (dataSource == null) { + init(); + } + Assert.notNull(dataSource, "dataSource can not be null"); + return dataSource.getConnection(); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/config/MyBatisPlusConfig.java b/src/main/java/com/cbsd/universaltestsoftware_client/config/MyBatisPlusConfig.java new file mode 100644 index 0000000..510e747 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/config/MyBatisPlusConfig.java @@ -0,0 +1,41 @@ +package com.cbsd.universaltestsoftware_client.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; +import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MyBatisPlusConfig { + + /** + * 分页插件配置 + * + * @return + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + + // 分页插件 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + //乐观锁插件 + interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); + + return interceptor; + } + + /** + * 自定义批量插入 SQL 注入器 + */ + @Bean + public InsertBatchSqlInjector insertBatchSqlInjector() { + return new InsertBatchSqlInjector(); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/config/SwaggerConfig.java b/src/main/java/com/cbsd/universaltestsoftware_client/config/SwaggerConfig.java new file mode 100644 index 0000000..f287391 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/config/SwaggerConfig.java @@ -0,0 +1,82 @@ +package com.cbsd.universaltestsoftware_client.config; + +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.oas.annotations.EnableOpenApi; +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.Arrays; +import java.util.Collections; +import java.util.List; + +@Configuration //表明当前类是配置类 +@EnableOpenApi //表示开启生成接口文档功能(只有开启了OpenApi,才可以实现生成接口文档的功能) +public class SwaggerConfig { + + + @Bean + public Docket docket() { + return new Docket(DocumentationType.OAS_30) + .apiInfo(apiInfo()) + .select() + .apis(RequestHandlerSelectors.basePackage("com.cbsd.universaltestsoftware_client.controller")) + .paths(PathSelectors.any()) + .build() + .securitySchemes(securitySchemes()) // 添加安全方案 + .securityContexts(Collections.singletonList(securityContext())); // 添加上下文 + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("Swagger3 API文档") + .description("RESTFul 风格接口") + .version("1.0") + .build(); + } + + private List securitySchemes() { + List apiKeyList = new ArrayList<>(); + // 添加sessionId认证 + apiKeyList.add(new ApiKey("sessionId", "sessionId", "header")); + // 添加tenantId请求头支持 + // apiKeyList.add(new ApiKey("tenantId", "tenantId", "header")); + return apiKeyList; + } + + /** + * 设置swagger认证的安全上下文 + */ + private SecurityContext securityContext() { + return SecurityContext.builder() + .securityReferences(defaultAuth()) + .build(); + } + + /** + * 默认的安全引用,包含sessionId和tenantId + */ + private List defaultAuth() { + AuthorizationScope[] authorizationScopes = new AuthorizationScope[]{ + new AuthorizationScope("web", "All scope is trusted!") + }; + + return Arrays.asList( + new SecurityReference("sessionId", authorizationScopes) + // new SecurityReference("tenantId", authorizationScopes) + ); + } + + + +} + + + + diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/controller/BackageFileController.java b/src/main/java/com/cbsd/universaltestsoftware_client/controller/BackageFileController.java new file mode 100644 index 0000000..46d0df6 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/controller/BackageFileController.java @@ -0,0 +1,339 @@ +package com.cbsd.universaltestsoftware_client.controller; + +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.cbsd.universaltestsoftware_client.dto.FileInfo; +import com.cbsd.universaltestsoftware_client.util.*; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiOperation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.math.BigInteger; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +@Api(tags = "文件上传下载相关操作的接口文档") +@RestController() +@RequestMapping("/api/common/backstage/file") +public class BackageFileController { + // 得到一个用来记录日志的对象,这样在打印信息时能够标记打印的是哪个类的信息 + private static final Log logger = LogFactory.getLog(BackageFileController.class); + + @Value("${cbsd.uploadfilesrootpath}") + private String uploadfilesrootpath; + + /** + * 单文件上传 + */ + @ApiOperation(value = "单个文件上传", notes = "") + @PostMapping("/fileupload") + public Result oneFileUpload(MultipartFile file) { + if (file == null) { + return Result.error(ResultCode.PARAMETER_ERROR, "file文件参数未传"); + } + String fileName = file.getOriginalFilename(); + if (StringUtils.isBlank(fileName)) { + return Result.error("文件上传失败,请检查文件名称"); + } + try { + String suffix = fileName.substring(fileName.lastIndexOf(".")); + String fileId = UUIDUtils.getUUID(); + String parentDic = DateUtils.dateToString("yyyy/MM/dd"); + String filePath = parentDic + "/" + fileId + suffix; + //创建文件夹 + File parentFile = new File(uploadfilesrootpath, parentDic); + if (!parentFile.exists()) { + parentFile.mkdirs(); + } + Path savePath = Paths.get(uploadfilesrootpath, filePath); + Files.write(savePath, file.getBytes()); + + FileInfo fileInfo = new FileInfo(); + fileInfo.setNewFileName(fileId + suffix); + fileInfo.setOriginalFilename(fileName); + fileInfo.setRelativePath(parentDic); + fileInfo.setNginxFileReadPath(filePath); + fileInfo.setFileSize(String.valueOf(file.getSize())); + return Result.success(fileInfo); + } catch (Exception e) { + e.printStackTrace(); + } + return Result.error("文件上传失败"); + } + + + @ApiOperation(value = "多个文件上传", notes = "上传文件参数名称统一files," + + "如果中途有个别文件上传没有成功,将不会返回错误,只会返回对应正确上传文件信息列表," + + "前端请注意查看对比数量") + @PostMapping("/multifile") //ApiResultResponse> + public Result> multiFileUpload(List files) { + if (files == null || files.isEmpty()) { + return Result.error("文件不能为空"); + } + List fileInfoList = new ArrayList(); + for (MultipartFile file : files) { + Result result = oneFileUpload(file); + if (result.getCode() == ResultCode.OK && result.getData() != null) { + fileInfoList.add(result.getData()); + } + } + return Result.success(fileInfoList); + } + + /** + * 执行下载 + */ + @ApiOperation(value = "文件下载") + @GetMapping("download") //ApiResultResponse + public ResponseEntity download(@RequestParam String filePath, @RequestParam(required = false) String fileName) throws Exception { + + if (StringUtils.isBlank(filePath)) { + return null; + } + String downloadPath = uploadfilesrootpath + filePath; + File downloadFile = new File(downloadPath); + + HttpHeaders headers = new HttpHeaders(); + //下载 + headers.add("Content-Disposition", "attachment; filename=" + downloadFile.getName()); + //二进制流 + String mediaType = "application/octet-stream"; + return ResponseEntity.ok() + .headers(headers) + .contentLength(downloadFile.length()) + .contentType(MediaType.parseMediaType(mediaType)) + .body(new FileSystemResource(downloadFile)); + } + + + @ApiOperation(value = "文件下载或预览", httpMethod = "GET", notes = "文件下载或预览") + @ApiImplicitParams({ + @ApiImplicitParam(name = "filePath", value = "文件路劲", dataType = "String", dataTypeClass = String.class, paramType = "query"), + @ApiImplicitParam(name = "downloadOrRead", value = "下载或预览 :download/read", dataType = "String", dataTypeClass = String.class, paramType = "query") + }) + @RequestMapping("/fileDownLoad") + public ResponseEntity fileDownLoad( + @RequestParam String filePath, + @RequestParam(required = false) String fileName, + @RequestParam(required = false) String downloadOrRead) throws UnsupportedEncodingException { + + if (StringUtils.isBlank(filePath)) { + return ResponseEntity.badRequest().build(); + } + + String downloadPath = uploadfilesrootpath + filePath; + File downloadFile = new File(downloadPath); + + if (!downloadFile.exists()) { + return ResponseEntity.notFound().build(); + } + + String originalFileName = downloadFile.getName(); + String actualFileName = StringUtils.isNotBlank(fileName) ? fileName : originalFileName; + + // 如果用户传了文件名,但没有后缀,就补上原始后缀 + if (StringUtils.isNotBlank(fileName) && !fileName.contains(".")) { + String suffix = originalFileName.substring(originalFileName.lastIndexOf(".")); + actualFileName = fileName + suffix; + } + + // URL 编码文件名(防止中文乱码) + actualFileName = URLEncoder.encode(actualFileName, StandardCharsets.UTF_8.toString()) + .replaceAll("\\+", "%20"); + + HttpHeaders headers = new HttpHeaders(); + String mediaType; + + if ("read".equals(downloadOrRead)) { + // 预览 + headers.add(HttpHeaders.CONTENT_DISPOSITION, "inline; filename*=UTF-8''" + actualFileName); + String suffix = originalFileName.substring(originalFileName.lastIndexOf(".")); + mediaType = MediaTypeUtils.getMediaType(suffix); // 你需要实现这个工具类 + } else { + // 下载 + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + actualFileName); + mediaType = "application/octet-stream"; + } + + return ResponseEntity.ok() + .headers(headers) + .contentLength(downloadFile.length()) + .contentType(MediaType.parseMediaType(mediaType)) + .body(new FileSystemResource(downloadFile)); + + } + + @GetMapping("/getVideo") + public void getVideo(HttpServletResponse response, String filePath, HttpServletRequest request) throws FileNotFoundException { + +// InputStream objectContent = OssUtils.ossGetInputStream( url) ; +// InputStream objectContent = new FileInputStream("/Users/chenlin/Downloads/123.mp4"); + String url = uploadfilesrootpath + filePath; + System.err.println(url); + try { + File tempFile = new File(url); + + RandomAccessFile randomFile = new RandomAccessFile(tempFile, "r");//只读模式 + long contentLength = randomFile.length(); + String range = request.getHeader("Range"); + int start = 0, end = 0; + if (range != null && range.startsWith("bytes=")) { + String[] values = range.split("=")[1].split("-"); + start = Integer.parseInt(values[0]); + if (values.length > 1) { + end = Integer.parseInt(values[1]); + } + } + int requestSize = 0; + if (end != 0 && end > start) { + requestSize = end - start + 1; + } else { + requestSize = Integer.MAX_VALUE; + } + + byte[] buffer = new byte[2048]; + response.setContentType("video/mp4"); + response.setHeader("Accept-Ranges", "bytes"); + String fileNameString = filePath.substring(filePath.lastIndexOf("/") + 1); + response.setHeader("ETag", fileNameString); + response.setHeader("Last-Modified", new Date().toString()); + //第一次请求只返回content length来让客户端请求多次实际数据 + if (range == null) { + + response.setHeader("Content-length", contentLength + ""); + } else { + //以后的多次以断点续传的方式来返回视频数据 + response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);//206 + long requestStart = 0, requestEnd = 0; + String[] ranges = range.split("="); + if (ranges.length > 1) { + String[] rangeDatas = ranges[1].split("-"); + requestStart = Integer.parseInt(rangeDatas[0]); + if (rangeDatas.length > 1) { + requestEnd = Integer.parseInt(rangeDatas[1]); + } + } + long length = 0; + if (requestEnd > 0) { + length = requestEnd - requestStart + 1; + response.setHeader("Content-length", "" + length); + response.setHeader("Content-Range", "bytes " + requestStart + "-" + requestEnd + "/" + contentLength); + } else { + length = contentLength - requestStart; + response.setHeader("Content-length", "" + length); + response.setHeader("Content-Range", "bytes " + requestStart + "-" + (contentLength - 1) + "/" + contentLength); + } + } + ServletOutputStream out = response.getOutputStream(); + int needSize = requestSize; + randomFile.seek(start); + while (needSize > 0) { + + int len = randomFile.read(buffer); + if (needSize < buffer.length) { + out.write(buffer, 0, needSize); + } else { + out.write(buffer, 0, len); + if (len < buffer.length) { + break; + } + } + needSize -= buffer.length; + } + randomFile.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + /** + * 下载保存时中文文件名的字符编码转换方法 + */ + public String toUTF8String(String str) { + StringBuffer sb = new StringBuffer(); + int len = str.length(); + for (int i = 0; i < len; i++) { + // 取出字符中的每个字符 + char c = str.charAt(i); + // Unicode码值为0~255时,不做处理 + if (c >= 0 && c <= 255) { + sb.append(c); + } else { // 转换 UTF-8 编码 + byte b[]; + try { + b = Character.toString(c).getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + b = null; + } + // 转换为%HH的字符串形式 + for (int j = 0; j < b.length; j++) { + int k = b[j]; + if (k < 0) { + k &= 255; + } + sb.append("%" + Integer.toHexString(k).toUpperCase()); + } + } + } + return sb.toString(); + } + + /** + * 获取上传文件的md5 + * + * @param file + * @return + * @throws IOException + */ + public String getMd5(MultipartFile file) { + try { + //获取文件的byte信息 + byte[] uploadBytes = file.getBytes(); + // 拿到一个MD5转换器 + MessageDigest md5 = MessageDigest.getInstance("MD5"); + byte[] digest = md5.digest(uploadBytes); + //转换为16进制 + return new BigInteger(1, digest).toString(16); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + + /** + * 验证文件信息是否是 相关路径下的文件数据 + * + * @param relativeUrl 放开的相关路径 + * @param filePathUrl 下载文件路径 + * @return + */ + public static boolean isPathSafe(String relativeUrl, String filePathUrl) { + Path basePath = Paths.get(relativeUrl); + Path pathToCheck = Paths.get(filePathUrl); + return basePath.resolve(pathToCheck.normalize()).normalize().startsWith(basePath); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/controller/BoardController.java b/src/main/java/com/cbsd/universaltestsoftware_client/controller/BoardController.java new file mode 100644 index 0000000..d722810 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/controller/BoardController.java @@ -0,0 +1,102 @@ +package com.cbsd.universaltestsoftware_client.controller; + +import com.cbsd.universaltestsoftware_client.dto.BoardTemperatureDto; +import com.cbsd.universaltestsoftware_client.dto.ChannelPageDto; +import com.cbsd.universaltestsoftware_client.dto.PageBoard; +import com.cbsd.universaltestsoftware_client.entity.Board; +import com.cbsd.universaltestsoftware_client.entity.Channel; +import com.cbsd.universaltestsoftware_client.service.BoardService; +import com.cbsd.universaltestsoftware_client.util.Result; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.v3.oas.annotations.Parameter; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.constraints.NotNull; + +/** + *

+ * 板卡 前端控制器 + *

+ * + * @author admin + * @since 2025-08-26 + */ +@RestController +@Api(tags = "板卡端接口") +@RequestMapping("/api/board") +public class BoardController { + @Resource + private BoardService boardService; + + @ApiOperation("获取板卡列表") + @PostMapping("/pageBoardList") + public String pageList(@RequestBody @Validated PageBoard pageBoard) { + return boardService.pageList(pageBoard); + } + + @ApiOperation("添加修改板卡") + @PostMapping("/saveOrUpdateBoard") + public String saveOrUpdateBoard(@RequestBody @Validated Board board) { + + return boardService.saveOrUpdateBoard(board); + } + + @ApiOperation("删除板卡") + @GetMapping("/deleteBoardById") + public String deleteById(@RequestParam String serverId, @RequestParam String boardId) { + return boardService.deleteById(serverId, boardId); + } + + @ApiOperation("获取板卡类型") + @GetMapping("/getBoardType") + public String getBoardType(@RequestParam String serverId) { + return boardService.getBoardType(serverId); + } + + @ApiOperation("新增或者修改通道") + @PostMapping("/saveOrUpdateChannel") + public String saveOrUpdateChannel(@RequestBody @Validated Channel channel) { + return boardService.saveOrUpdateChannel(channel); + } + + @ApiOperation("获取通道列表") + @PostMapping("/pageChannelList") + public Result pageChannelList(@RequestBody @Validated ChannelPageDto channelPageDto) { + return boardService.pageChannelList(channelPageDto); + } + + @ApiOperation("获取所有通道类型") + @GetMapping("/getChannelTypeAll") + public String getChannelTypeAll(@RequestParam String serverId, @Parameter(description = "boardTypeId") @NotNull(message = "板卡类型id不可以为空") String boardTypeId) { + return boardService.getChannelTypeAll(serverId, boardTypeId); + } + + + @ApiOperation("删除通道") + @GetMapping("/deleteChannelById") + public String deleteChannelById(@RequestParam String serverId, @RequestParam String channelId) { + return boardService.deleteChannelById(serverId, channelId); + } + + @ApiOperation("根据版卡id获取通道的路径") + @GetMapping("/getChannelPath") + public String getChannelPath(@RequestParam String serverId, @RequestParam String boardId) { + return boardService.getChannelPath(serverId, boardId); + } + + @ApiOperation("刷新绑卡信息") + @GetMapping("/refressBoardInformation") + public String refressBoardInformation(@RequestParam String serverId) { + return boardService.refressBoardInformation(serverId); + } + + @ApiOperation("获取板卡温度") + @PostMapping("/getBoardTemperature") + public String getBoardTemperature(@RequestBody BoardTemperatureDto dto) { + return boardService.getBoardTemperature(dto); + } + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/controller/BootLogController.java b/src/main/java/com/cbsd/universaltestsoftware_client/controller/BootLogController.java new file mode 100644 index 0000000..f0d545b --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/controller/BootLogController.java @@ -0,0 +1,50 @@ +package com.cbsd.universaltestsoftware_client.controller; + + + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.cbsd.universaltestsoftware_client.dto.BootLogDeleteDto; +import com.cbsd.universaltestsoftware_client.dto.BootLogPageDto; +import com.cbsd.universaltestsoftware_client.entity.BootLog; +import com.cbsd.universaltestsoftware_client.service.IBootLogService; +import com.cbsd.universaltestsoftware_client.util.Result; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.validation.annotation.Validated; +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.annotation.Resource; + +/** + *

+ * 前端控制器 + *

+ * + * @author wt + * @since 2025-09-03 + */ +@RestController +@Api(tags = "日志记录") +@RequestMapping("/api/boot-log") +public class BootLogController { + + @Resource + private IBootLogService bootLogService; + + + @ApiOperation(value = "日志记录-查询分页") + @PostMapping("/bootLogPage") + public Result> bootLogPage(@RequestBody @Validated BootLogPageDto bootLogPageDto) { + return bootLogService.bootLogPage(bootLogPageDto); + } + + + @ApiOperation(value = "日志记录-删除分页") + @PostMapping("/bootLogDelete") + public Result bootLogDelete(@RequestBody @Validated BootLogDeleteDto bootLogDeleteDto) { + return bootLogService.bootLogDelete(bootLogDeleteDto); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/controller/ChannelDataController.java b/src/main/java/com/cbsd/universaltestsoftware_client/controller/ChannelDataController.java new file mode 100644 index 0000000..5790f49 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/controller/ChannelDataController.java @@ -0,0 +1,83 @@ +package com.cbsd.universaltestsoftware_client.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.cbsd.universaltestsoftware_client.dto.ChannelDataDto; +import com.cbsd.universaltestsoftware_client.dto.ChannelDataHistoryDto; +import com.cbsd.universaltestsoftware_client.dto.ChannelDataStatisticsDto; +import com.cbsd.universaltestsoftware_client.dto.PageChannelDataDto; +import com.cbsd.universaltestsoftware_client.entity.ChannelDataHistory; +import com.cbsd.universaltestsoftware_client.parser.model.FiledParseData; +import com.cbsd.universaltestsoftware_client.service.ChannelDataService; +import com.cbsd.universaltestsoftware_client.util.Result; +import com.cbsd.universaltestsoftware_client.vo.ChannelDataStatisticsVo; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import javax.validation.constraints.NotBlank; +import java.util.List; + +@RestController +@Api(tags = "数据回访") +@RequestMapping("/api/channelData") +public class ChannelDataController { + + @Resource + private ChannelDataService channelDataService; + + @PostConstruct + public void init(){ + channelDataService.clearChannelData(); + } + + + @ApiOperation("开始数据拉取") + @PostMapping("/start") + public Result start(@RequestBody @Validated PageChannelDataDto pageChannelDataDto) throws Exception { + // 调用服务层的start方法,开始数据拉取 + channelDataService.start(pageChannelDataDto); + return Result.success(); + } + + @ApiOperation("停止数据拉取") + @GetMapping("/stop") + public Result stop(@NotBlank @RequestParam String dataBoardId) { + // 调用服务层的stop方法,停止指定dataBoardId的数据拉取 + channelDataService.stop(dataBoardId); + return Result.success(); + } + + @ApiOperation("更新分页参数") + @PostMapping("/updateData") + public Result updateData(@RequestBody @Validated PageChannelDataDto pageChannelDataDto) { + // 调用服务层的update方法,更新分页参数 + channelDataService.updateData(pageChannelDataDto); + return Result.success(); + } + + @ApiOperation(value = "通道历史数据-查询分页") + @PostMapping("/channelDataQuery") + public String channelDataQuery(@RequestBody @Validated ChannelDataHistoryDto channelDataHistoryDto) { + return channelDataService.channelDataQuery(channelDataHistoryDto); + } + + @ApiOperation(value = "通道历史数据-删除") + @PostMapping("/channelDataDelete") + public String channelDataDelete(@RequestBody @Validated ChannelDataHistoryDto channelDataHistoryDto) { + return channelDataService.channelDataDelete(channelDataHistoryDto); + } + + @ApiOperation(value = "通道数据统计") + @PostMapping("/getChannelDataStatistics") + public Result> getChannelDataStatistics(@RequestBody ChannelDataStatisticsDto dto) { + return channelDataService.getChannelDataStatistics(dto); + } + @ApiOperation(value = "清除通道数据统计") + @PostMapping("/clearChannelDataStatistics") + public Result clearChannelDataStatistics(@RequestBody ChannelDataStatisticsDto dto) { + return channelDataService.clearChannelDataStatistics(dto); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/controller/DataBoardChannelController.java b/src/main/java/com/cbsd/universaltestsoftware_client/controller/DataBoardChannelController.java new file mode 100644 index 0000000..5c3889b --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/controller/DataBoardChannelController.java @@ -0,0 +1,18 @@ +package com.cbsd.universaltestsoftware_client.controller; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 前端控制器 + *

+ * + * @author admin + * @since 2025-09-10 + */ +@RestController +@RequestMapping("/dataBoardChannel") +public class DataBoardChannelController { + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/controller/DataBoardController.java b/src/main/java/com/cbsd/universaltestsoftware_client/controller/DataBoardController.java new file mode 100644 index 0000000..e8e7731 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/controller/DataBoardController.java @@ -0,0 +1,161 @@ +package com.cbsd.universaltestsoftware_client.controller; + + +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.cbsd.universaltestsoftware_client.dto.DataClearDto; +import com.cbsd.universaltestsoftware_client.dto.DownloadDto; +import com.cbsd.universaltestsoftware_client.dto.ExportDataBoardDto; +import com.cbsd.universaltestsoftware_client.dto.InsertDataBoardDto; +import com.cbsd.universaltestsoftware_client.entity.DataBoard; +import com.cbsd.universaltestsoftware_client.entity.DataBoardChannel; +import com.cbsd.universaltestsoftware_client.entity.Download; +import com.cbsd.universaltestsoftware_client.service.DataBoardService; +import com.cbsd.universaltestsoftware_client.service.SystemConfigService; +import com.cbsd.universaltestsoftware_client.util.MediaTypeUtils; +import com.cbsd.universaltestsoftware_client.util.Result; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.FileSystemResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + *

+ * 数据看板 前端控制器 + *

+ * + * @author admin + * @since 2025-09-10 + */ +@RestController +@Api(tags = "数据看板") +@RequestMapping("/api/dataBoard") +public class DataBoardController { + + @Resource + private DataBoardService dataBoardService; + + @Value("${cbsd.downloadUrl}") + private String downloadDir; + + @ApiOperation("新增修改数据看板") + @PostMapping("/saveOrUpdateDataBoard") + public Result saveOrUpdateDataBoard(@RequestBody @Validated InsertDataBoardDto insertDataBoardDto) throws Exception { + dataBoardService.saveOrUpdateDataBoard(insertDataBoardDto); + return Result.success(); + } + + @ApiOperation("删除数据看板") + @GetMapping("/removeDataBoardById") + public Result removeDataBoardById(@RequestParam String dataBoardId) throws Exception { + dataBoardService.removeDataBoardById(dataBoardId); + return Result.success(); + } + + @ApiOperation("查询数据看板") + @GetMapping("/listDataBoard") + public Result> listDataBoard(@RequestParam Integer dataType) throws Exception { + return Result.success(dataBoardService.listDataBoard(dataType)); + } + + @ApiOperation("通过id修改颜色或者左轴") + @PostMapping("/updateDataBoardById") + public Result updateDataBoardById(@RequestBody DataBoardChannel dataBoardChannel) throws Exception { + dataBoardService.updateDataBoardById(dataBoardChannel); + return Result.success(); + } + + @ApiOperation("导出数据回访") + @PostMapping("/exportDataBoard") + public Result exportDataBoard(@RequestBody @Validated ExportDataBoardDto exportDataBoardDto, HttpServletResponse response) throws Exception { + return dataBoardService.exportDataBoard(exportDataBoardDto, response); + + } + + @ApiOperation("清除") + @PostMapping("/clear") + public Result clear(@RequestBody @Validated ExportDataBoardDto exportDataBoardDto) throws Exception { + dataBoardService.clear(exportDataBoardDto); + return Result.success(); + } + + @ApiOperation("导出历史查看") + @PostMapping("/exportHistory") + public Result> exportHistory(@RequestBody @Validated DownloadDto downloadDto) { + return dataBoardService.exportHistory(downloadDto); + + } + + @ApiOperation(value = "文件下载或预览", httpMethod = "GET", notes = "文件下载或预览") + @ApiImplicitParams({ + @ApiImplicitParam(name = "filePath", value = "文件路劲", dataType = "String", dataTypeClass = String.class, paramType = "query"), + @ApiImplicitParam(name = "downloadOrRead", value = "下载或预览 :download/read", dataType = "String", dataTypeClass = String.class, paramType = "query") + }) + @RequestMapping("/fileDownLoad") + public ResponseEntity fileDownLoad( + @RequestParam String filePath, + @RequestParam(required = false) String fileName, + @RequestParam(required = false) String downloadOrRead) throws UnsupportedEncodingException { + + if (StringUtils.isBlank(filePath)) { + return ResponseEntity.badRequest().build(); + } + + String downloadPath = downloadDir + "/"+filePath; + File downloadFile = new File(downloadPath); + + if (!downloadFile.exists()) { + return ResponseEntity.notFound().build(); + } + + String originalFileName = downloadFile.getName(); + String actualFileName = StringUtils.isNotBlank(fileName) ? fileName : originalFileName; + + // 如果用户传了文件名,但没有后缀,就补上原始后缀 + if (StringUtils.isNotBlank(fileName) && !fileName.contains(".")) { + String suffix = originalFileName.substring(originalFileName.lastIndexOf(".")); + actualFileName = fileName + suffix; + } + + // URL 编码文件名(防止中文乱码) + actualFileName = URLEncoder.encode(actualFileName, StandardCharsets.UTF_8.toString()) + .replaceAll("\\+", "%20"); + + HttpHeaders headers = new HttpHeaders(); + String mediaType; + + if ("read".equals(downloadOrRead)) { + // 预览 + headers.add(HttpHeaders.CONTENT_DISPOSITION, "inline; filename*=UTF-8''" + actualFileName); + String suffix = originalFileName.substring(originalFileName.lastIndexOf(".")); + mediaType = MediaTypeUtils.getMediaType(suffix); // 你需要实现这个工具类 + } else { + // 下载 + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + actualFileName); + mediaType = "application/octet-stream"; + } + + return ResponseEntity.ok() + .headers(headers) + .contentLength(downloadFile.length()) + .contentType(MediaType.parseMediaType(mediaType)) + .body(new FileSystemResource(downloadFile)); + + } + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/controller/DownloadController.java b/src/main/java/com/cbsd/universaltestsoftware_client/controller/DownloadController.java new file mode 100644 index 0000000..3261d95 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/controller/DownloadController.java @@ -0,0 +1,18 @@ +package com.cbsd.universaltestsoftware_client.controller; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 前端控制器 + *

+ * + * @author admin + * @since 2026-02-03 + */ +@RestController +@RequestMapping("/download") +public class DownloadController { + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/controller/InstructController.java b/src/main/java/com/cbsd/universaltestsoftware_client/controller/InstructController.java new file mode 100644 index 0000000..104d852 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/controller/InstructController.java @@ -0,0 +1,224 @@ +package com.cbsd.universaltestsoftware_client.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.cbsd.universaltestsoftware_client.dto.*; +import com.cbsd.universaltestsoftware_client.entity.Instruct; +import com.cbsd.universaltestsoftware_client.parser.model.Filed; +import com.cbsd.universaltestsoftware_client.service.InstructService; +import com.cbsd.universaltestsoftware_client.util.CalcUtils; +import com.cbsd.universaltestsoftware_client.util.DataUtils; +import com.cbsd.universaltestsoftware_client.util.ModbusCRC16Utils; +import com.cbsd.universaltestsoftware_client.util.Result; +import com.cbsd.universaltestsoftware_client.vo.ServerListVo; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.apache.commons.lang3.StringUtils; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.security.NoSuchAlgorithmException; +import java.util.List; + +/** + *

+ * 前端控制器 + *

+ * + * @author admin + * @since 2025-09-01 + */ +@RestController +@Api(tags = "指令") +@RequestMapping("/api/instruct") +public class InstructController { + + @Resource + private InstructService instructService; + + @ApiOperation("获取指令列表") + @PostMapping("/pageInstructList") + public Result> pageList(@RequestBody @Validated InstructPageDto instructPageDto) { + return Result.success(instructService.pageList(instructPageDto)); + } + + @ApiOperation("修改指令") + @PostMapping("/saveOrUpdateInstruct") + public Result saveOrUpdateInstruct(@RequestBody @Validated Instruct instruct) { + return instructService.saveOrUpdateInstruct(instruct); + } + + @ApiOperation("删除指令") + @PostMapping("/deleteInstructById") + public Result deleteInstructById(@RequestBody InstructDeleteDto dto) { + return instructService.deleteInstructById(dto); + } + + @ApiOperation("执行指令") + @GetMapping("/execute") + public Result execute(@RequestParam String instructId) { +// return instructService.execute(instructId, null); + return null; + } + + @ApiOperation("获取通道列表") + @GetMapping("/getChannelList") + public Result> getChannelList(String keyword) { + return instructService.getChannelList(keyword); + } + + @ApiOperation("获取板载驱动程序注册函数") + @GetMapping("/getChannelFunctionName") + public String getChannelFunctionName(@RequestParam String channelId, @RequestParam String serverId) { + return instructService.getChannelFunctionName(channelId, serverId); + } + + @ApiOperation("获取板载驱动额外数据") + @GetMapping("/getBoardDriverExtraData") + public String getBoardDriverExtraData(@RequestParam String channelId, @RequestParam String serverId) { + return instructService.getBoardDriverExtraData(channelId, serverId); + } + + + @ApiOperation("新增") + @PostMapping("/saveOrUpdateInstructGroup") + public Result saveOrUpdateInstructGroup(@RequestBody @Validated InstructGroupDto instructGroupDto) { + return instructService.saveOrUpdateInstructGroup(instructGroupDto); + } + + @ApiOperation("上移") + @GetMapping("/moveUp") + public Result moveUp(@RequestParam String instructId) { + return instructService.moveUp(instructId); + + } + + @ApiOperation("下移") + @GetMapping("/moveDown") + public Result moveDown(@RequestParam String instructId) { + return instructService.moveDown(instructId); + } + + @ApiOperation("根据指令id获取协议参数") + @GetMapping("/getInstructProtocolParam") + public Result> getInstructProtocolParam(@RequestParam String instructId) { + return instructService.getInstructProtocolParam(instructId); + } + + /** + * 至少 2 个常规指令 → 新建组并归组 + */ + @ApiOperation("新建指令组:至少 2 个常规指令 → 新建组并归组") + @PostMapping("/createGroupFromList") + public Result createGroupFromList(@Valid @RequestBody CreateGroupDTO dto) { + instructService.createGroupFromList(dto); + return Result.success(); + } + + @ApiOperation("批量修改指令参数") + @PostMapping("/batchChangeParam") + public Result batchChangeParam(@RequestBody BatchChangeInstructParamDto dto) { + return instructService.batchChangeParam(dto); + } + + + @ApiOperation("批量复制") + @PostMapping("/batchCopyBelow") + public Result batchCopyBelow(@RequestBody @Validated BatchCopyBelowDTO dto) { + instructService.batchCopyBelow(dto); + return Result.success(); + } + + @ApiOperation("批量移动") + @PostMapping("/batchMoveBelow") + public Result batchMoveBelow(@RequestBody @Validated BatchMoveBelowDTO dto) { + instructService.batchMoveBelow(dto); + return Result.success(); + } + + @ApiOperation("上传指令文件") + @PostMapping("/uploadCommandFile") + public Result uploadCommandFile(@RequestParam String instructId, MultipartFile file) { + return instructService.uploadCommandFile(instructId, file); + } + + @ApiOperation("删除指令脚本文件") + @PostMapping("/deleteInstructFile") + public Result deleteInstructFile(@RequestBody InstructDeleteFileDto dto) { + return instructService.deleteInstructFile(dto); + } + + @ApiOperation("获取指令校验位") + @PostMapping("/instructCalc") + public Result instructCalc(@RequestBody InstructCalcDto dto) { + if (dto == null) { + return Result.error("参数不能为空"); + } + if (StringUtils.isBlank(dto.getData())) { + return Result.error("数据不能为空"); + } + if (dto.getCalcMethod() == null) { + return Result.error("校验方法不能为空"); + } + + String calcResult; + if (dto.getCalcMethod() == 1) { + // crc16 + calcResult = ModbusCRC16Utils.calculateCRCToHexString( + DataUtils.HexToByteArr(dto.getData())); + } else if (dto.getCalcMethod() == 2) { + // crc32 + long result = CalcUtils.crc32Checksum( + DataUtils.HexToByteArr(dto.getData())); + calcResult = Long.toHexString(result).toUpperCase(); + } else if (dto.getCalcMethod() == 3) { + // 奇偶校验 + byte result = CalcUtils.parityCheck( + DataUtils.HexToByteArr(dto.getData())); + calcResult = Integer.toHexString(result).toUpperCase(); + } else if (dto.getCalcMethod() == 4) { + // 校验和 + int result = CalcUtils.simpleChecksum( + DataUtils.HexToByteArr(dto.getData())); + calcResult = Integer.toHexString(result).toUpperCase(); + } else if (dto.getCalcMethod() == 5) { + // MD5 + try { + calcResult = CalcUtils.md5Checksum( + DataUtils.HexToByteArr(dto.getData())).toUpperCase(); + } catch (NoSuchAlgorithmException e) { + return Result.error("计算失败"); + } + } else if (dto.getCalcMethod() == 6) { + // SHA-256 + try { + calcResult = CalcUtils.sha256Checksum( + DataUtils.HexToByteArr(dto.getData())).toUpperCase(); + } catch (NoSuchAlgorithmException e) { + return Result.error("计算失败"); + } + } else if (dto.getCalcMethod() == 7) { + // 单字节异或校验 + try { + calcResult = CalcUtils.byteXOR(dto.getData()).toUpperCase(); + } catch (Exception e) { + return Result.error("计算失败"); + } + } else { + return Result.error("不支持的校验方式"); + } + + // ====== 根据 byteCount 截取或补齐 ====== + calcResult = CalcUtils.fitToByteCount(calcResult, dto.getByteCount()); + return Result.success(calcResult); + } + + @ApiOperation("获取指令数据注释") + @GetMapping("/getInstructDataRemark") + public String getInstructDataRemark(@RequestParam String instructId) { + return instructService.getInstructDataRemark(instructId); + } + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/controller/InvocationController.java b/src/main/java/com/cbsd/universaltestsoftware_client/controller/InvocationController.java new file mode 100644 index 0000000..b6e92fe --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/controller/InvocationController.java @@ -0,0 +1,93 @@ +package com.cbsd.universaltestsoftware_client.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.cbsd.universaltestsoftware_client.dto.InvocationPageDto; +import com.cbsd.universaltestsoftware_client.dto.InvocationSortDto; +import com.cbsd.universaltestsoftware_client.entity.Invocation; +import com.cbsd.universaltestsoftware_client.service.InvocationService; +import com.cbsd.universaltestsoftware_client.util.DateUtils; +import com.cbsd.universaltestsoftware_client.util.Result; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + *

+ * 用列 前端控制器 + *

+ * + * @author admin + * @since 2025-09-01 + */ +@RestController +@Api(tags = "用列") +@RequestMapping("/api/invocation") +public class InvocationController { + + @Resource + private InvocationService invocationService; + + @ApiOperation("获取用列列表") + @PostMapping("/pageList") + public Result> pageList(@RequestBody @Validated InvocationPageDto invocationPageDto) { + return Result.success(invocationService.pageList(invocationPageDto)); + } + + @ApiOperation("添加或者修改用列") + @PostMapping("/saveOrUpdate") + public Result saveOrUpdate(@RequestBody @Validated Invocation invocation) { + if (StringUtils.isNotBlank(invocation.getInvocationId())) { + invocation.setUpdateTime(DateUtils.dateToString()); + } else { + long schemeInvocationCount = invocationService.count(new LambdaQueryWrapper().eq(Invocation::getSchemeId, invocation.getSchemeId())); + invocation.setSort((int) (schemeInvocationCount + 1)); + invocation.setCreateTime(DateUtils.dateToString()); + } + try { + return invocationService.saveOrUpdate(invocation) ? Result.success("保存成功") : Result.error("保存失败"); + } catch (Exception e) { + if (e instanceof DuplicateKeyException) { + return Result.error("同一方案内用例名字重复"); + } + return Result.error("保存失败"); + } + } + + @ApiOperation("删除用列") + @GetMapping("/deleteById") + public Result deleteById(@RequestParam String invocationId) { + return invocationService.deleteById(invocationId); + } + + @ApiOperation("排序") + @PostMapping("/sort") + public Result sort(@RequestBody InvocationSortDto dto){ + return invocationService.sort(dto); + } + + @ApiOperation("用列导出") + @GetMapping("/exportInvocation") + public Result exportInvocation(@RequestParam String invocationId, HttpServletResponse response)throws IOException { + invocationService.exportInvocation(invocationId, response); + return Result.success(); + } + + /** + * 导入 XML 文件 + */ + @PostMapping("/importXml") + public Result importXml(@RequestParam("file") MultipartFile file,@RequestParam("schemeId")String schemeId) throws Exception { + invocationService.importXml(file,schemeId); + return Result.success(); + } + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/controller/JavaCompilerController.java b/src/main/java/com/cbsd/universaltestsoftware_client/controller/JavaCompilerController.java new file mode 100644 index 0000000..1114d94 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/controller/JavaCompilerController.java @@ -0,0 +1,100 @@ +package com.cbsd.universaltestsoftware_client.controller; + + + +import com.cbsd.client.Test; +import com.cbsd.universaltestsoftware_client.DynamicClassBootstrap; +import com.cbsd.universaltestsoftware_client.dto.SubmitAndCompile; +import com.cbsd.universaltestsoftware_client.util.Result; +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import com.cbsd.universaltestsoftware_client.service.JavaCompileService; + +@RestController +@RequestMapping("/api/javaCompiler") +public class JavaCompilerController { + + private final JavaCompileService javaCompileService; + + @Autowired + public JavaCompilerController(JavaCompileService javaCompileService) { + this.javaCompileService = javaCompileService; + } + + /** + * 上传Java文件并编译 + */ + @ApiModelProperty("上传Java文件并编译") + @PostMapping("/uploadCompile") + public Result uploadAndCompile(@RequestParam("file") MultipartFile file) { + // 验证文件 + if (file.isEmpty()) { + return Result.error("请上传有效的Java文件"); + + } + + String fileName = file.getOriginalFilename(); + if (fileName == null || !fileName.endsWith(".java")) { + return Result.error("请上传扩展名为.java的文件"); + + } + + try { + Result stringResult = javaCompileService.compileJavaFile(file); + return stringResult; + } catch (Exception e) { + return Result.error("编译过程出错"+e.getMessage()); + + } + } + + /** + * 提交Java代码字符串并编译 + */ + @ApiOperation("提交Java代码字符串并编译") + @PostMapping("/submitCompile") + public Result submitAndCompile(@RequestBody @Validated SubmitAndCompile submitAndCompile) { + if (submitAndCompile.getClassName() == null || submitAndCompile.getClassName().trim().isEmpty()) { + return Result.error("类名不能为空"); + + } + + if (submitAndCompile.getCode() == null || submitAndCompile.getCode().trim().isEmpty()) { + return Result.error("Java代码不能为空"); + } + + try { + Result stringResult = javaCompileService.compileJavaCode(submitAndCompile.getClassName(), submitAndCompile.getCode()); + return stringResult; + } catch (Exception e) { + return Result.error("编译过程出错"+e.getMessage()); + } + } + @ApiOperation("获取所有已编译的类") + @GetMapping("/getClasses") + public Result getAllCompiledClasses() { + return Result.success(DynamicClassBootstrap.CLAZZ_CACHE); + + } + @ApiOperation("测试执行其中一个clss结果") + @GetMapping("/testExecuteClass") + public Result testExecuteClass(@RequestParam("className") String className) { + Class aClass = DynamicClassBootstrap.CLAZZ_CACHE.get(className); + if (aClass != null) { + try { + Test o = (Test) aClass.newInstance(); + return Result.success(o.test()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + return Result.success(); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/controller/LogController.java b/src/main/java/com/cbsd/universaltestsoftware_client/controller/LogController.java new file mode 100644 index 0000000..201410d --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/controller/LogController.java @@ -0,0 +1,78 @@ +package com.cbsd.universaltestsoftware_client.controller; + +import cn.hutool.http.HttpRequest; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.cbsd.universaltestsoftware_client.dto.BootLogDeleteDto; +import com.cbsd.universaltestsoftware_client.dto.PageLogDto; +import com.cbsd.universaltestsoftware_client.entity.ServerInfo; +import com.cbsd.universaltestsoftware_client.mapper.ServerMapper; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +@RestController +@Api(tags = "服务端日志") +@RequestMapping("/api/log") +public class LogController { + + @Resource + private ServerMapper serverMapper; + @ApiOperation("通过服务端id获取日志") + @PostMapping("/getLogList") + public String getLogList(@RequestBody @Validated PageLogDto pageLogDto) { + + ServerInfo serverInfo = serverMapper.selectById(pageLogDto.getServerId()); + if (serverInfo==null){ + throw new RuntimeException("服务端不存在"); + } + try { + String url = "http://"+serverInfo.getIpAddress()+":"+serverInfo.getHttpPort()+"/universaltestsoftware_server_api/outer/client/bootLogPage"; + String result = HttpRequest.post(url) + .header("Content-Type", "application/json") + .body(JSONUtil.toJsonStr(pageLogDto)) + .execute() + .body(); + + if (StringUtils.isBlank(result)){ + throw new RuntimeException("连接失败"); + } + + return result; + }catch (Exception e){ + e.printStackTrace(); + throw new RuntimeException("连接失败"); + } + } + + @ApiOperation("通过服务端id删除日志") + @PostMapping("/deleteLog") + private String deleteLog(@RequestBody BootLogDeleteDto bootLogDeleteDto) { + ServerInfo serverInfo = serverMapper.selectById(bootLogDeleteDto.getServerId()); + if (serverInfo==null){ + throw new RuntimeException("服务端不存在"); + } + try { + String url = "http://"+serverInfo.getIpAddress()+":"+serverInfo.getHttpPort()+"/universaltestsoftware_server_api/outer/client/bootLogDelete"; + String result = HttpRequest.post(url) + .header("Content-Type", "application/json") + .body(JSONUtil.toJsonStr(bootLogDeleteDto)) + .execute() + .body(); + + if (StringUtils.isBlank(result)){ + throw new RuntimeException("连接失败"); + } + + return result; + }catch (Exception e){ + e.printStackTrace(); + throw new RuntimeException("连接失败"); + } + } + + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/controller/ParserController.java b/src/main/java/com/cbsd/universaltestsoftware_client/controller/ParserController.java new file mode 100644 index 0000000..d8efb5b --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/controller/ParserController.java @@ -0,0 +1,62 @@ +package com.cbsd.universaltestsoftware_client.controller; + +import com.cbsd.universaltestsoftware_client.entity.Parser; +import com.cbsd.universaltestsoftware_client.entity.ParserProtocol; +import com.cbsd.universaltestsoftware_client.nettyclient.entity.ReceiveTcpContent; +import com.cbsd.universaltestsoftware_client.service.ParserService; +import com.cbsd.universaltestsoftware_client.util.Result; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +@RestController +@Api(tags = "解析器管理") +@RequestMapping("/api/parser") +public class ParserController { + + @Resource + private ParserService parserService; + + + @ApiOperation("获取解析器列表") + @GetMapping("/getParserList") + public Result> getParserList(@RequestParam String channelId) { + return parserService.getParserList(channelId); + } + + @ApiOperation("修改或者新增解析器") + @PostMapping("/saveOrUpdateParser") + public Result saveOrUpdateParser(@RequestBody Parser parser) { + return parserService.saveOrUpdateParser(parser); + } + + @ApiOperation("删除解析器") + @GetMapping("/deleteParserById") + public Result deleteParserById(@RequestParam String parserId) { + return parserService.deleteParserById(parserId); + } + + @ApiOperation("解析器添加或者修改协议") + @PostMapping("/addProtocol") + public Result addProtocol(@RequestBody @Validated List parserProtocol) { + return parserService.addProtocol(parserProtocol); + } + @ApiOperation("解析器协议列表") + @GetMapping("/getProtocolList") + public Result> getProtocolList(@RequestParam String parserId) { + return parserService.getProtocolList(parserId); + } + + @ApiOperation("删除解析器协议") + @GetMapping("/deleteParserProtocolById") + public Result deleteParserProtocolById(@RequestParam String parserProtocolId) { + return parserService.deleteParserProtocolById(parserProtocolId); + } + + + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/controller/ProtocolController.java b/src/main/java/com/cbsd/universaltestsoftware_client/controller/ProtocolController.java new file mode 100644 index 0000000..5b6cced --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/controller/ProtocolController.java @@ -0,0 +1,68 @@ +package com.cbsd.universaltestsoftware_client.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.cbsd.universaltestsoftware_client.dto.ProtocolPageDto; +import com.cbsd.universaltestsoftware_client.entity.Protocol; +import com.cbsd.universaltestsoftware_client.service.ProtocolService; +import com.cbsd.universaltestsoftware_client.util.Result; +import com.cbsd.universaltestsoftware_client.vo.ProtocolSaveVo; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; + +/** + *

+ * 协议 前端控制器 + *

+ * + * @author admin + * @since 2025-09-02 + */ +@RestController +@Api(tags = "协议管理") +@RequestMapping("/api/protocol") +public class ProtocolController { + @Resource + private ProtocolService protocolService; + + @ApiOperation("获取协议列表") + @PostMapping("/pageListProtocol") + public Result> pageListProtocol(@RequestBody ProtocolPageDto dto) { + return protocolService.pageListProtocol(dto); + } + + @ApiOperation("添加或者修改协议") + @PostMapping("/saveOrUpdateProtocol") + public Result saveOrUpdateProtocol(@RequestBody Protocol protocol) { + + return protocolService.saveOrUpdateProtocol(protocol); + } + + @ApiOperation("删除协议") + @GetMapping("/deleteProtocolById") + public Result deleteProtocolById(@RequestParam String protocolId) { + return protocolService.deleteProtocolById(protocolId); + } + + @ApiOperation("协议导入") + @PostMapping("/import") + public Result importProtocol(MultipartFile file) { + if (file == null) { + return Result.error("文件不能为空"); + } + if (file.isEmpty()) { + return Result.error("文件无效或文件内容为空"); + } + if (StringUtils.isBlank(file.getOriginalFilename())) { + return Result.error("文件名无效"); + } + if (!(file.getOriginalFilename().endsWith(".xlsx") || file.getOriginalFilename().endsWith(".xls"))) { + return Result.error("文件格式错误"); + } + return protocolService.importProtocol(file); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/controller/SchemeController.java b/src/main/java/com/cbsd/universaltestsoftware_client/controller/SchemeController.java new file mode 100644 index 0000000..61ddc81 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/controller/SchemeController.java @@ -0,0 +1,59 @@ +package com.cbsd.universaltestsoftware_client.controller; + +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.cbsd.universaltestsoftware_client.config.BasePage; +import com.cbsd.universaltestsoftware_client.entity.Scheme; +import com.cbsd.universaltestsoftware_client.service.SchemeService; +import com.cbsd.universaltestsoftware_client.util.DateUtils; +import com.cbsd.universaltestsoftware_client.util.Result; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +/** + *

+ * 方案 前端控制器 + *

+ * + * @author admin + * @since 2025-09-01 + */ +@RestController +@Api(tags = "方案") +@RequestMapping("/api/scheme") +public class SchemeController { + + @Resource + private SchemeService schemeService; + + + @PostMapping("/pageList") + @ApiOperation("获取方案列表") + public Result> pageList(@RequestBody BasePage basePage) { + return schemeService.pageList(basePage); + } + + @ApiOperation("添加或者修改方案") + @PostMapping("/saveOrUpdate") + public Result saveOrUpdate(@RequestBody @Validated Scheme scheme) { + if (StringUtils.isNotBlank(scheme.getSchemeId())) { + scheme.setUpdateTime(DateUtils.dateToString()); + } else { + scheme.setCreateTime(DateUtils.dateToString()); + } + return schemeService.saveOrUpdate(scheme) ? Result.success("成功") : Result.error("失败"); + + } + + @GetMapping("/deleteById") + @ApiOperation("删除方案") + public Result deleteById(@RequestParam String schemeId) { + return schemeService.deleteScheme(schemeId); + } + + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/controller/ScriptController.java b/src/main/java/com/cbsd/universaltestsoftware_client/controller/ScriptController.java new file mode 100644 index 0000000..02d8da9 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/controller/ScriptController.java @@ -0,0 +1,180 @@ +package com.cbsd.universaltestsoftware_client.controller; + +import com.cbsd.universaltestsoftware_client.dto.ScriptContentDto; +import com.cbsd.universaltestsoftware_client.dto.ScriptDto; +import com.cbsd.universaltestsoftware_client.dto.ScriptFileContentDto; +import com.cbsd.universaltestsoftware_client.script.ScriptManager; +import com.cbsd.universaltestsoftware_client.service.InstructService; +import com.cbsd.universaltestsoftware_client.util.Result; +import com.cbsd.universaltestsoftware_client.vo.InstructScriptVo; +import com.cbsd.universaltestsoftware_client.vo.ScriptTypeVo; +import com.cbsd.universaltestsoftware_client.vo.ScriptVo; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; + +@RestController +@Api(tags = "脚本管理") +@RequestMapping("/api/script") +public class ScriptController { + + @Resource + private ScriptManager scriptManager; + + @Resource + private InstructService instructService; + + @ApiOperation("刷新脚本") + @GetMapping("/recompileAllScript") + public Result recompileAllScript() { + scriptManager.recompileAllScript(); + return Result.success(); + } + + @ApiOperation("获取脚本类型列表") + @GetMapping("/getScriptTypeList") + public Result> getScriptTypeList() { + return Result.success(scriptManager.getScriptTypeList()); + } + + @ApiOperation("根据脚本类型获取脚本列表") + @GetMapping("/getScriptList") + public Result> getScriptList(@RequestParam String scriptType) { + //判断scriptType是否是ScriptManager.ScriptType + if (StringUtils.isBlank(scriptType)) { + return Result.error("脚本类型不能为空"); + } + if (!ScriptManager.ScriptType.getValues().contains(scriptType)) { + return Result.error("类型错误"); + } + return Result.success(scriptManager.getScriptList(scriptType)); + } + + @ApiOperation("编译脚本") + @PostMapping("/compileScript") + public Result compileScript(@RequestBody ScriptDto dto) { + if (dto == null) { + return Result.error("参数错误"); + } + if (StringUtils.isBlank(dto.getScriptType())) { + return Result.error("脚本类型不能为空"); + } + if (!ScriptManager.ScriptType.getValues().contains(dto.getScriptType())) { + return Result.error("类型错误"); + } + if (StringUtils.isBlank(dto.getScriptFileName())) { + return Result.error("脚本文件名称不能为空"); + } + return scriptManager.compileScript(dto.getScriptType(), dto.getScriptFileName()); + } + + @ApiOperation("删除脚本") + @PostMapping("/deleteScript") + public Result deleteScript(@RequestBody ScriptDto dto) { + if (dto == null) { + return Result.error("参数错误"); + } + if (StringUtils.isBlank(dto.getScriptType())) { + return Result.error("脚本类型不能为空"); + } + if (!ScriptManager.ScriptType.getValues().contains(dto.getScriptType())) { + return Result.error("类型错误"); + } + if (StringUtils.isBlank(dto.getScriptFileName())) { + return Result.error("脚本文件名称不能为空"); + } + scriptManager.deleteScript(dto.getScriptType(), dto.getScriptFileName(), dto.getScriptName()); + return Result.success(); + } + + @ApiOperation("上传脚本") + @PostMapping("/uploadScript") + public Result uploadScript(@RequestParam String instructId, @RequestParam String scriptType, MultipartFile file) { + InstructScriptVo vo = null; + try { + vo = scriptManager.uploadScript(scriptType, file); + instructService.uploadScript(instructId, vo.getScriptType(), vo.getScriptFile()); + vo.setInstructId(instructId); + return Result.success(vo); + } catch (Exception e) { + if (vo != null) { + scriptManager.deleteScriptFile(vo.getScriptType(), vo.getScriptFile()); + } + return Result.error(e.getMessage()); + } + } + + @ApiOperation("保存脚本内容") + @PostMapping("/saveScriptContent") + public Result saveScriptContent(@RequestBody ScriptContentDto dto) { + InstructScriptVo vo = null; + try { + vo = scriptManager.saveScriptContent(dto.getInstructId(), dto.getScriptType(), dto.getScriptContent()); + instructService.uploadScriptContent(dto.getInstructId(), vo.getScriptType(), vo.getScriptName(), dto.getScriptContent()); + vo.setInstructId(dto.getInstructId()); + return Result.success(vo); + } catch (Exception e) { + if (vo != null) { + scriptManager.deleteScriptFile(vo.getScriptType(), vo.getScriptFile()); + } + return Result.error(e.getMessage()); + } + } + + @ApiOperation("获取脚本文件内容") + @GetMapping("/getScriptFileContent") + public Result getScriptFileContent(@RequestParam String filePath) { + if (StringUtils.isBlank(filePath)) { + return Result.error("文件地址不能为空"); + } + if (!new File(filePath).exists()) { + return Result.error("文件不存在"); + } + try { + String content = new String(Files.readAllBytes(Paths.get(filePath)), StandardCharsets.UTF_8); + return Result.success(content); + } catch (IOException e) { + e.printStackTrace(); + } + return Result.error("文件读取失败"); + } + + @ApiOperation("保存脚本文件内容") + @PostMapping("/saveScriptFileContent") + public Result saveScriptFileContent(@RequestBody ScriptFileContentDto dto) { + if (StringUtils.isBlank(dto.getScriptType())) { + return Result.error("脚本类型不能为空"); + } + if (!ScriptManager.ScriptType.getValues().contains(dto.getScriptType())) { + return Result.error("类型错误"); + } + if (StringUtils.isBlank(dto.getContent())) { + return Result.error("脚本内容不能为空"); + } + try { + if (StringUtils.isBlank(dto.getFilePath())) { + //新增 + scriptManager.createScriptFile(dto.getScriptType(), dto.getContent()); + } else { + //修改 + File file = new File(dto.getFilePath()); + Files.write(Paths.get(file.getPath()), dto.getContent().getBytes(StandardCharsets.UTF_8)); + scriptManager.compileScriptFile(dto.getScriptType(), file); + } + return Result.success(); + }catch (Exception e) { + return Result.error(e.getMessage()); + } + + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/controller/SensorEquipmentController.java b/src/main/java/com/cbsd/universaltestsoftware_client/controller/SensorEquipmentController.java new file mode 100644 index 0000000..c6bb312 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/controller/SensorEquipmentController.java @@ -0,0 +1,47 @@ +package com.cbsd.universaltestsoftware_client.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.cbsd.universaltestsoftware_client.dto.SensorEquipmentDto; +import com.cbsd.universaltestsoftware_client.dto.SensorEquipmentPageDto; +import com.cbsd.universaltestsoftware_client.entity.SensorEquipment; +import com.cbsd.universaltestsoftware_client.service.SensorEquipmentService; +import com.cbsd.universaltestsoftware_client.util.Result; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +@RestController +@Api(tags = "传感设备") +@RequestMapping("/api/sensorEquipment") +public class SensorEquipmentController { + + @Resource + private SensorEquipmentService sensorEquipmentService; + + @ApiOperation("分页列表") + @PostMapping("/pageList") + public Result> pageList(@RequestBody SensorEquipmentPageDto dto) { + return sensorEquipmentService.pageList(dto); + } + + @ApiOperation("获取所有列表") + @GetMapping("/getAllList") + public Result> getAllList(@RequestParam(required = false) String channelId) { + return sensorEquipmentService.getAllList(channelId); + } + + @ApiOperation("新增或修改") + @PostMapping("/addOrUpdate") + public Result addOrUpdate(@RequestBody SensorEquipmentDto dto){ + return sensorEquipmentService.addOrUpdate(dto); + } + + @ApiOperation("删除") + @GetMapping("/delete") + public Result delete(@RequestParam String equipmentId) { + return sensorEquipmentService.delete(equipmentId); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/controller/ServerTerminalController.java b/src/main/java/com/cbsd/universaltestsoftware_client/controller/ServerTerminalController.java new file mode 100644 index 0000000..29e0cf8 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/controller/ServerTerminalController.java @@ -0,0 +1,76 @@ +package com.cbsd.universaltestsoftware_client.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.cbsd.universaltestsoftware_client.dto.PageServerInfoDto; +import com.cbsd.universaltestsoftware_client.entity.ServerInfo; +import com.cbsd.universaltestsoftware_client.service.ServeService; +import com.cbsd.universaltestsoftware_client.util.DateUtils; +import com.cbsd.universaltestsoftware_client.util.Result; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +/** + *

+ * 服务端 前端控制器 + *

+ * + * @author admin + * @since 2025-08-26 + */ +@RestController +@Api(tags = "服务端接口") +@RequestMapping("/api/serverTerminal") +public class ServerTerminalController { + @Resource + private ServeService serveService; + + @ApiOperation("获取服务端列表") + @PostMapping("/pageList") + public Result> pageList(@RequestBody PageServerInfoDto basePage) { + return serveService.pageList(basePage); + } + + @ApiOperation("添加或者修改服务端") + @PostMapping("/saveOrUpdate") + public Result saveOrUpdate(@RequestBody @Validated ServerInfo serverInfo) { + if (serverInfo.getServerId() == null) { + serverInfo.setCreateTime(DateUtils.dateToString()); + } else { + serverInfo.setUpdateTime(DateUtils.dateToString()); + } + return serveService.saveOrUpdate(serverInfo) ? Result.success("成功") : Result.error("失败"); + } + + @ApiOperation("删除服务端") + @GetMapping("/deleteById") + public Result deleteById(@RequestParam String serverId) { + return serveService.deleteById(serverId); + } + + /** + * 断开并移除指定服务端连接 + */ + @ApiOperation("断开指定服务端连接") + @GetMapping("/disconnectServer") + public Result disconnectServer(@RequestParam String serverId) { + serveService.disconnectServer(serverId); + return Result.success("已移除服务端[" + serverId + "]的连接"); + } + + + /** + * 连接指定服务端 + */ + @ApiOperation("连接指定服务端") + @GetMapping("/connectServer") + public Result connectServer(@RequestParam String serverId) { + //查询服务端信息 + return serveService.connectOrDisconnect(serverId); + } + + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/controller/SystemConfigController.java b/src/main/java/com/cbsd/universaltestsoftware_client/controller/SystemConfigController.java new file mode 100644 index 0000000..f0788a0 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/controller/SystemConfigController.java @@ -0,0 +1,52 @@ +package com.cbsd.universaltestsoftware_client.controller; + +import com.cbsd.universaltestsoftware_client.dto.DataClearDto; +import com.cbsd.universaltestsoftware_client.service.SystemConfigService; +import com.cbsd.universaltestsoftware_client.util.Result; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +@RestController +@Api(tags = "系统配置") +@RequestMapping("/api/config") +public class SystemConfigController { + + @Resource + private SystemConfigService systemConfigService; + + @ApiOperation("获取显示列列表") + @GetMapping("/getShowColumnList") + public Result> getShowColumnList(){ + return systemConfigService.getShowColumnList(); + } + + @ApiOperation("设置显示列列表") + @PostMapping("/setShowColumnList") + public Result setShowColumnList(@RequestBody List columnList){ + return systemConfigService.setShowColumnList(columnList); + } + + @ApiOperation("获取数据清理上限") + @GetMapping("/getDataClearLimit") + public Result getDataClearLimit() { + return Result.success(systemConfigService.getSystemConfig(2)); + } + + @ApiOperation("设置数据清理上限") + @PostMapping("/setDataClearLimit") + public Result setDataClearLimit(@RequestBody DataClearDto dto) { + if (dto == null || dto.getLimit() == null) { + return Result.error("数据上限不能为空"); + } + try { + systemConfigService.setSystemConfig(2, String.valueOf(dto.getLimit())); + } catch (Exception e) { + return Result.error("设置失败"); + } + return Result.success(); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/BatchChangeInstructParamDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/BatchChangeInstructParamDto.java new file mode 100644 index 0000000..ae8e3b9 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/BatchChangeInstructParamDto.java @@ -0,0 +1,40 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.List; + +@Data +public class BatchChangeInstructParamDto { + /** + * 用例id + */ + @ApiModelProperty(value = "用例id") + private String invocationId; + /** + * 指令id列表 + */ + @ApiModelProperty(value = "指令id列表") + private List instructIdList; + /** + * 状态 1.启用 0.禁用 + */ + @ApiModelProperty(value = "状态 1.启用 0.禁用") + private Integer status; + /** + * 断点 1.是 0.否 + */ + @ApiModelProperty(value = "断点 1.是 0.否") + private Integer isDebug; + /** + * 前置等待时间 + */ + @ApiModelProperty(value = "前置等待时间") + private Integer leadTime; + /** + * 后置等待时间 + */ + @ApiModelProperty(value = "后置等待时间") + private Integer tailTime; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/BatchCopyBelowDTO.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/BatchCopyBelowDTO.java new file mode 100644 index 0000000..b08d779 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/BatchCopyBelowDTO.java @@ -0,0 +1,16 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import java.util.List; + +@Data +public class BatchCopyBelowDTO { + + @NotBlank(message = "invocationId不能为空") + private String invocationId; + @NotEmpty(message = "instructIds不能为空") + private List<@NotBlank String> instructIds; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/BatchMoveBelowDTO.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/BatchMoveBelowDTO.java new file mode 100644 index 0000000..eedcc83 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/BatchMoveBelowDTO.java @@ -0,0 +1,17 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.List; +@Data +public class BatchMoveBelowDTO { + + @NotBlank(message = "invocationId不能为空") + private String invocationId; + + @NotNull(message = "移动的id,和sort的list") + private List instructs; + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/BoardTemperatureDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/BoardTemperatureDto.java new file mode 100644 index 0000000..e8bb0ef --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/BoardTemperatureDto.java @@ -0,0 +1,17 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +public class BoardTemperatureDto { + + @ApiModelProperty("服务端id") + private String serverId; + + @ApiModelProperty( "板卡id") + private String boardId; + + @ApiModelProperty( "板卡索引") + private String boardIndex; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/BoardTemperatureServerDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/BoardTemperatureServerDto.java new file mode 100644 index 0000000..2e97d8c --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/BoardTemperatureServerDto.java @@ -0,0 +1,17 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +public class BoardTemperatureServerDto { + + @ApiModelProperty("服务端id") + private String serverId; + + @ApiModelProperty( "板卡id") + private String boardId; + + @ApiModelProperty( "板卡索引") + private Integer boardIndex; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/BootLogDeleteDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/BootLogDeleteDto.java new file mode 100644 index 0000000..34e8b1f --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/BootLogDeleteDto.java @@ -0,0 +1,31 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import java.util.List; + +@Data +public class BootLogDeleteDto { + + @ApiModelProperty("服务端id") + private String serverId; + + @ApiModelProperty("主键ID,自增长") + private Long id; + + @ApiModelProperty("批量id") + private List listId; + + @ApiModelProperty("日志级别(如:DEBUG, INFO, WARN, ERROR)") + private String level; + + @ApiModelProperty(value = "开始时间") + private String startTime; + /** + * 结束时间 + */ + @ApiModelProperty(value = "结束时间") + private String endTime; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/BootLogPageDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/BootLogPageDto.java new file mode 100644 index 0000000..146fb19 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/BootLogPageDto.java @@ -0,0 +1,29 @@ +package com.cbsd.universaltestsoftware_client.dto; + + +import com.cbsd.universaltestsoftware_client.config.BasePage; +import com.cbsd.universaltestsoftware_client.entity.BootLog; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class BootLogPageDto extends BasePage { + + @ApiModelProperty("日志级别(如:DEBUG, INFO, WARN, ERROR)") + private String level; + + + + @ApiModelProperty(value = "开始时间") + private String startTime; + /** + * 结束时间 + */ + @ApiModelProperty(value = "结束时间") + private String endTime; + + @ApiModelProperty("日志类型 1.软件日志 2.系统日志") + private Integer logType; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/ChannelDataDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ChannelDataDto.java new file mode 100644 index 0000000..f15c0a0 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ChannelDataDto.java @@ -0,0 +1,18 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import com.cbsd.universaltestsoftware_client.config.BasePage; +import com.cbsd.universaltestsoftware_client.entity.ChannelData; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.List; + +@Data +public class ChannelDataDto extends BasePage { + @ApiModelProperty("通道idList") + private List channelIdList; + + + + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/ChannelDataHistoryDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ChannelDataHistoryDto.java new file mode 100644 index 0000000..75b5466 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ChannelDataHistoryDto.java @@ -0,0 +1,22 @@ +package com.cbsd.universaltestsoftware_client.dto; + + +import com.cbsd.universaltestsoftware_client.config.BasePage; +import com.cbsd.universaltestsoftware_client.entity.ChannelDataHistory; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotEmpty; + +@EqualsAndHashCode(callSuper = true) +@Data +public class ChannelDataHistoryDto extends BasePage { + + @ApiModelProperty("通道id") + private String channelId; + + @ApiModelProperty("服务端id") + @NotEmpty(message = "服务端id不能为空") + private String serverId; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/ChannelDataQueryAll.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ChannelDataQueryAll.java new file mode 100644 index 0000000..f27681b --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ChannelDataQueryAll.java @@ -0,0 +1,15 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class ChannelDataQueryAll { + + private List channelIds; + + private String startTime; + + private String endTime; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/ChannelDataStatisticsDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ChannelDataStatisticsDto.java new file mode 100644 index 0000000..3a040a4 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ChannelDataStatisticsDto.java @@ -0,0 +1,18 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +public class ChannelDataStatisticsDto { + /** + * 数据看板id + */ + @ApiModelProperty("数据看板id") + private String dataBoardId; + /** + * 通道id + */ + @ApiModelProperty("通道id") + private String channelId; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/ChannelDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ChannelDto.java new file mode 100644 index 0000000..a7b1caf --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ChannelDto.java @@ -0,0 +1,16 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +public class ChannelDto { + + @ApiModelProperty("通道id") + private String channelId; + + + @ApiModelProperty("通道名字") + private String channelName; + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/ChannelPageDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ChannelPageDto.java new file mode 100644 index 0000000..8300e17 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ChannelPageDto.java @@ -0,0 +1,28 @@ +package com.cbsd.universaltestsoftware_client.dto; + + +import com.cbsd.universaltestsoftware_client.config.BasePage; +import com.cbsd.universaltestsoftware_client.entity.Channel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; + +@EqualsAndHashCode(callSuper = true) +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChannelPageDto extends BasePage { + + @ApiModelProperty("板卡id") + @NotBlank(message = "板卡id不可以为空") + private String boardId; + + @ApiModelProperty("服务id") + @NotBlank(message = "服务id不可以为空") + private String serverId; + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/CreateGroupDTO.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/CreateGroupDTO.java new file mode 100644 index 0000000..de92762 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/CreateGroupDTO.java @@ -0,0 +1,17 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.util.List; + +@Data +public class CreateGroupDTO { + + @NotBlank + private String invocationId; + + @Size(min = 2, message = "至少选择 2 个节点") + private List<@NotBlank String> instructIds; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/DataClearDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/DataClearDto.java new file mode 100644 index 0000000..a6ffc47 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/DataClearDto.java @@ -0,0 +1,11 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +public class DataClearDto { + + @ApiModelProperty("数据上限") + private Integer limit; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/DeleteChannelDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/DeleteChannelDto.java new file mode 100644 index 0000000..4944fab --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/DeleteChannelDto.java @@ -0,0 +1,18 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.List; + +@Data +public class DeleteChannelDto { + + private List channelIds; + + @ApiModelProperty("开始时间") + private String startTime; + + @ApiModelProperty("结束时间") + private String endTime; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/DownloadDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/DownloadDto.java new file mode 100644 index 0000000..2ed841b --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/DownloadDto.java @@ -0,0 +1,18 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.cbsd.universaltestsoftware_client.config.BasePage; +import com.cbsd.universaltestsoftware_client.entity.Download; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +public class DownloadDto extends BasePage { + + @ApiModelProperty("1:生成中,2:已完成") + @TableField("state") + private Integer state; + + @ApiModelProperty("类型 1:解析数据 ,2:原数据") + private Integer type; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/ExportDataBoardDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ExportDataBoardDto.java new file mode 100644 index 0000000..de2f68e --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ExportDataBoardDto.java @@ -0,0 +1,46 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiOperation; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +@Data +public class ExportDataBoardDto { + + @ApiModelProperty("开始时间") + @NotEmpty(message = "开始时间不能为空") + private String startTime; + + @ApiModelProperty("结束时间") + @NotEmpty(message = "结束时间不能为空") + private String endTime; + + @ApiModelProperty("看板id") + @NotEmpty(message = "看板id不能为空") + private String dataBoardId; + + + @ApiModelProperty("参数") + private List param; + + @ApiModelProperty("导出类型1:解析,2原数据") +// @NotNull(message = "导出类型不能为空") + private Integer exportType; + + /** + * 解析数据类型 1.原码值 2.解析值 3.两者 + */ + @ApiModelProperty("解析数据类型 1.原码值 2.解析值 3.两者") + private Integer parseDataType; + + + @ApiModelProperty("协议ids") + private List protocolIds; + + + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/FileInfo.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/FileInfo.java new file mode 100644 index 0000000..1555dcc --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/FileInfo.java @@ -0,0 +1,21 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel(description= "文件信息") +public class FileInfo { + @ApiModelProperty(value = "文件存储相对路径") + private String relativePath; + @ApiModelProperty(value = "文件原始名称") + private String originalFilename; + @ApiModelProperty(value = "网络文件读取路径") + private String nginxFileReadPath; + + @ApiModelProperty(value = "文件尺寸大小,单位kb") + private String fileSize; + @ApiModelProperty(value = "文件新名称") + private String newFileName; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/InsertDataBoardDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/InsertDataBoardDto.java new file mode 100644 index 0000000..afd337c --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/InsertDataBoardDto.java @@ -0,0 +1,35 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.List; + +@Data +public class InsertDataBoardDto { + + @ApiModelProperty("数据看板id") + private String dataBoardId; + + @ApiModelProperty("看板类型,0:表格,1:曲线") + @TableField("data_board_type") + private Integer dataBoardType; + + @ApiModelProperty("看板名字") + @TableField("data_board_name") + private String dataBoardName; + + @ApiModelProperty("0:遥测,1:数据回放") + @NotNull(message = "数据类型不能为空") + private Integer dataType; + + @ApiModelProperty("通道lsit") + private List channelList; + + + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructCalcDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructCalcDto.java new file mode 100644 index 0000000..75953d3 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructCalcDto.java @@ -0,0 +1,13 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import lombok.Data; + +@Data +public class InstructCalcDto { + + private String data; + + private Integer calcMethod; + + private Integer byteCount; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructDeleteDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructDeleteDto.java new file mode 100644 index 0000000..a5ea05a --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructDeleteDto.java @@ -0,0 +1,13 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class InstructDeleteDto { + + private String invocationId; + + private List instructIds; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructDeleteFileDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructDeleteFileDto.java new file mode 100644 index 0000000..a1abf79 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructDeleteFileDto.java @@ -0,0 +1,14 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import lombok.Data; + +@Data +public class InstructDeleteFileDto { + + private String instructId; + + /** + * 删除文件类型 1.预处理 2.分支条件 3.检查 4.指令数据 5.协议 + */ + private Integer deleteFileType; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructGroupDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructGroupDto.java new file mode 100644 index 0000000..db9c658 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructGroupDto.java @@ -0,0 +1,35 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import com.baomidou.mybatisplus.annotation.TableField; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@Data +public class InstructGroupDto { + + @ApiModelProperty("指令id") + private String instructId; + + @ApiModelProperty("指令名字") + private String instructName; + + @ApiModelProperty("指令类型 1.常规指令 2.指令组") + private Integer instructType; + + @ApiModelProperty("用列id") + @NotBlank(message = "用列id不能为空") + private String invocationId; + + @ApiModelProperty("父级编码") + private String parentEncoding; + + @ApiModelProperty("排序") +// @NotNull(message = "排序不能为空") + private Integer sort; + + @ApiModelProperty("需要新增在他下方id") + private String inferiorId; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructItem.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructItem.java new file mode 100644 index 0000000..7ac4159 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructItem.java @@ -0,0 +1,164 @@ +package com.cbsd.universaltestsoftware_client.dto; + + +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.*; + +@Data +@NoArgsConstructor +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(propOrder = { + "instructId", "instructType", "encoding", "parentEncoding", "parentName", + "instructName", "serverId", "ipAddress", "tcpPort", "instructText", + "channelId", "channelName", "invocationId", "invocationName","operationType", "extraData", + "boardTypeName", "boardIndex", "protocolId", "protocolName", "protocolContent", + "sort", "status", "leadTime", "tailTime", "hitCount", "lastHitTime", + "instructTag", "preProcessScript", "preProcessScriptName", "preProcessScriptParam", + "preProcessScriptFile", "preProcessScriptComment", "repeatCount", + "branchConditionScript", "branchConditionScriptName", "branchConditionScriptFile", + "branchConditionScriptParam", "branchConditionScriptComment", + "checkScript", "checkScriptName", "checkScriptFile", "checkScriptComment", + "instructDataFile", "jumpTarget", "breakPoint" +}) +public class InstructItem { + + @XmlElement(name = "InstructId") + private String instructId; + + @XmlElement(name = "InstructType") + private Integer instructType; + + @XmlElement(name = "Encoding") + private String encoding; + + @XmlElement(name = "ParentEncoding") + private String parentEncoding; + + @XmlElement(name = "ParentName") + private String parentName; + + @XmlElement(name = "InstructName") + private String instructName; + + @XmlElement(name = "ServerId") + private String serverId; + + @XmlElement(name = "IpAddress") + private String ipAddress; + + @XmlElement(name = "TcpPort") + private Integer tcpPort; + + @XmlElement(name = "InstructText") + private String instructText; + + @XmlElement(name = "ChannelId") + private String channelId; + + @XmlElement(name = "ChannelName") + private String channelName; + + @XmlElement(name = "InvocationId") + private String invocationId; + + @XmlElement(name = "InvocationName") + private String invocationName; + + @XmlElement(name = "OperationType") + private Integer operationType; + + @XmlElement(name = "ExtraData") + private String extraData; + + @XmlElement(name = "BoardTypeName") + private String boardTypeName; + + @XmlElement(name = "BoardIndex") + private String boardIndex; + + @XmlElement(name = "ProtocolId") + private String protocolId; + + @XmlElement(name = "ProtocolName") + private String protocolName; + + @XmlElement(name = "ProtocolContent") + private String protocolContent; + + @XmlElement(name = "Sort") + private Integer sort; + + @XmlElement(name = "Status") + private Integer status; + + @XmlElement(name = "LeadTime") + private Integer leadTime; + + @XmlElement(name = "TailTime") + private Integer tailTime; + + @XmlElement(name = "HitCount") + private Integer hitCount; + + @XmlElement(name = "LastHitTime") + private String lastHitTime; + + @XmlElement(name = "InstructTag") + private String instructTag; + + @XmlElement(name = "PreProcessScript") + private String preProcessScript; + + @XmlElement(name = "PreProcessScriptName") + private String preProcessScriptName; + + @XmlElement(name = "PreProcessScriptParam") + private String preProcessScriptParam; + + @XmlElement(name = "PreProcessScriptFile") + private String preProcessScriptFile; + + @XmlElement(name = "PreProcessScriptComment") + private String preProcessScriptComment; + + @XmlElement(name = "RepeatCount") + private Integer repeatCount; + + @XmlElement(name = "BranchConditionScript") + private String branchConditionScript; + + @XmlElement(name = "BranchConditionScriptName") + private String branchConditionScriptName; + + @XmlElement(name = "BranchConditionScriptFile") + private String branchConditionScriptFile; + + @XmlElement(name = "BranchConditionScriptParam") + private String branchConditionScriptParam; + + @XmlElement(name = "BranchConditionScriptComment") + private String branchConditionScriptComment; + + @XmlElement(name = "CheckScript") + private String checkScript; + + @XmlElement(name = "CheckScriptName") + private String checkScriptName; + + @XmlElement(name = "CheckScriptFile") + private String checkScriptFile; + + @XmlElement(name = "CheckScriptComment") + private String checkScriptComment; + + @XmlElement(name = "InstructDataFile") + private String instructDataFile; + + @XmlElement(name = "JumpTarget") + private String jumpTarget; + + @XmlElement(name = "BreakPoint") + private Integer breakPoint; +} \ No newline at end of file diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructPageDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructPageDto.java new file mode 100644 index 0000000..24d7f7e --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructPageDto.java @@ -0,0 +1,17 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import com.cbsd.universaltestsoftware_client.config.BasePage; +import com.cbsd.universaltestsoftware_client.entity.Instruct; +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiOperation; +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +@Data +public class InstructPageDto extends BasePage { + + @ApiModelProperty("用列id") + @NotBlank(message = "用列id不能为空") + private String invocationId; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructXmlDTO.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructXmlDTO.java new file mode 100644 index 0000000..9c2eba2 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/InstructXmlDTO.java @@ -0,0 +1,21 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.*; +import java.util.List; + +@Data +@NoArgsConstructor +@XmlRootElement(name = "Instructs") +@XmlAccessorType(XmlAccessType.FIELD) +public class InstructXmlDTO { + + @XmlElement(name = "Instruct") + private List instructs; + + public InstructXmlDTO(List instructs) { + this.instructs = instructs; + } +} \ No newline at end of file diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/InvocationPageDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/InvocationPageDto.java new file mode 100644 index 0000000..a686960 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/InvocationPageDto.java @@ -0,0 +1,17 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import com.cbsd.universaltestsoftware_client.config.BasePage; +import com.cbsd.universaltestsoftware_client.entity.Invocation; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +@Data +public class InvocationPageDto extends BasePage { + + @ApiModelProperty("方案id") + @NotBlank(message = "方案id不能为空") + private String schemeId; + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/InvocationSortDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/InvocationSortDto.java new file mode 100644 index 0000000..d16eb65 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/InvocationSortDto.java @@ -0,0 +1,11 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import lombok.Data; + +@Data +public class InvocationSortDto { + + private String schemeId; + private String invocationId; + private Integer sort; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/PageBoard.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/PageBoard.java new file mode 100644 index 0000000..ae58ca5 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/PageBoard.java @@ -0,0 +1,19 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import com.cbsd.universaltestsoftware_client.config.BasePage; +import com.cbsd.universaltestsoftware_client.entity.Board; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PageBoard extends BasePage { + @ApiModelProperty("服务端id") + @NotEmpty(message = "服务端id不能为空") + private String serverId; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/PageChannelDataDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/PageChannelDataDto.java new file mode 100644 index 0000000..a97e632 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/PageChannelDataDto.java @@ -0,0 +1,30 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Data +public class PageChannelDataDto { + + @ApiModelProperty("开始时间") + private String startTime; + + @ApiModelProperty("结束时间") + private String endTime; + + @ApiModelProperty("条数") + @NotNull(message = "条数不能为空") + private Integer pageSize; + + @ApiModelProperty("数据看板id") + @NotEmpty(message = "数据看板id不能为空") + private String dataBoardId; + + private Integer want; + + private String lastTime; + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/PageLogDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/PageLogDto.java new file mode 100644 index 0000000..291e708 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/PageLogDto.java @@ -0,0 +1,13 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import com.cbsd.universaltestsoftware_client.config.BasePage; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +@Data +public class PageLogDto extends BasePage { + @ApiModelProperty("服务端id") + @NotEmpty(message = "服务端id不能为空") + private String serverId; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/PageServerInfoDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/PageServerInfoDto.java new file mode 100644 index 0000000..38f01a7 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/PageServerInfoDto.java @@ -0,0 +1,14 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import com.cbsd.universaltestsoftware_client.config.BasePage; +import com.cbsd.universaltestsoftware_client.entity.ServerInfo; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +public class PageServerInfoDto extends BasePage { + + @ApiModelProperty("连接状态0:不连接,1要连接") + private Integer connectStatus; + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/ProtocolImport.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ProtocolImport.java new file mode 100644 index 0000000..bd45fa4 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ProtocolImport.java @@ -0,0 +1,295 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import com.alibaba.excel.annotation.ExcelProperty; + +public class ProtocolImport { + + public static class BasicInfo { + @ExcelProperty("readName") + private String readName= ""; + @ExcelProperty("frameHeadValue") + private String frameHeadValue= ""; + @ExcelProperty("frameTailValue") + private String frameTailValue= ""; + @ExcelProperty("checkStartSn") + private String checkStartSn= ""; + @ExcelProperty("checkEndSn") + private String checkEndSn= ""; + @ExcelProperty("verifyScript") + private String verifyScript= ""; + + public String getReadName() { + return readName; + } + + public void setReadName(String readName) { + this.readName = readName; + } + + public String getFrameHeadValue() { + return frameHeadValue; + } + + public void setFrameHeadValue(String frameHeadValue) { + this.frameHeadValue = frameHeadValue; + } + + public String getFrameTailValue() { + return frameTailValue; + } + + public void setFrameTailValue(String frameTailValue) { + this.frameTailValue = frameTailValue; + } + + public String getCheckStartSn() { + return checkStartSn; + } + + public void setCheckStartSn(String checkStartSn) { + this.checkStartSn = checkStartSn; + } + + public String getCheckEndSn() { + return checkEndSn; + } + + public void setCheckEndSn(String checkEndSn) { + this.checkEndSn = checkEndSn; + } + + public String getVerifyScript() { + return verifyScript; + } + + public void setVerifyScript(String verifyScript) { + this.verifyScript = verifyScript; + } + } + + public static class Filed { + @ExcelProperty("sn") + private String sn = ""; + @ExcelProperty("label") + private String label = ""; + @ExcelProperty("readName") + private String readName = ""; + @ExcelProperty("phyType") + private String phyType = ""; + @ExcelProperty("timeFormat") + private String timeFormat = ""; + @ExcelProperty("bitslen") + private String bitslen = ""; + @ExcelProperty("bitmask") + private String bitmask = ""; + @ExcelProperty("phyInnerScript") + private String phyInnerScript = ""; + @ExcelProperty("phyScript") + private String phyScript = ""; + @ExcelProperty("endian") + private String endian = ""; + @ExcelProperty("type") + private String type = ""; + @ExcelProperty("frame") + private String frame = ""; + @ExcelProperty("calcScript") + private String calcScript = ""; + @ExcelProperty("phyUnit") + private String phyUnit = ""; + @ExcelProperty("binStr") + private String binStr = ""; + @ExcelProperty("hex") + private String hex = ""; + @ExcelProperty("phy") + private String phy = ""; + @ExcelProperty("avg") + private String avg = ""; + @ExcelProperty("min") + private String min = ""; + @ExcelProperty("max") + private String max = ""; + + public String getSn() { + return sn; + } + + public void setSn(String sn) { + this.sn = sn; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public String getReadName() { + return readName; + } + + public void setReadName(String readName) { + this.readName = readName; + } + + public String getPhyType() { + return phyType; + } + + public void setPhyType(String phyType) { + this.phyType = phyType; + } + + public String getTimeFormat() { + return timeFormat; + } + + public void setTimeFormat(String timeFormat) { + this.timeFormat = timeFormat; + } + + public String getBitslen() { + return bitslen; + } + + public void setBitslen(String bitslen) { + this.bitslen = bitslen; + } + + public String getBitmask() { + return bitmask; + } + + public void setBitmask(String bitmask) { + this.bitmask = bitmask; + } + + public String getPhyInnerScript() { + return phyInnerScript; + } + + public void setPhyInnerScript(String phyInnerScript) { + this.phyInnerScript = phyInnerScript; + } + + public String getPhyScript() { + return phyScript; + } + + public void setPhyScript(String phyScript) { + this.phyScript = phyScript; + } + + public String getEndian() { + return endian; + } + + public void setEndian(String endian) { + this.endian = endian; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getFrame() { + return frame; + } + + public void setFrame(String frame) { + this.frame = frame; + } + + public String getCalcScript() { + return calcScript; + } + + public void setCalcScript(String calcScript) { + this.calcScript = calcScript; + } + + public String getPhyUnit() { + return phyUnit; + } + + public void setPhyUnit(String phyUnit) { + this.phyUnit = phyUnit; + } + + public String getBinStr() { + return binStr; + } + + public void setBinStr(String binStr) { + this.binStr = binStr; + } + + public String getHex() { + return hex; + } + + public void setHex(String hex) { + this.hex = hex; + } + + public String getPhy() { + return phy; + } + + public void setPhy(String phy) { + this.phy = phy; + } + + public String getAvg() { + return avg; + } + + public void setAvg(String avg) { + this.avg = avg; + } + + public String getMin() { + return min; + } + + public void setMin(String min) { + this.min = min; + } + + public String getMax() { + return max; + } + + public void setMax(String max) { + this.max = max; + } + } + + public static class VariableLength { + @ExcelProperty("value") + private String value= ""; + @ExcelProperty("length") + private String length= ""; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getLength() { + return length; + } + + public void setLength(String length) { + this.length = length; + } + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/ProtocolNameListDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ProtocolNameListDto.java new file mode 100644 index 0000000..3525f5b --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ProtocolNameListDto.java @@ -0,0 +1,15 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.cbsd.universaltestsoftware_client.entity.DataBoardChannel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.List; +@Data +public class ProtocolNameListDto extends DataBoardChannel { + + @ApiModelProperty("协议参数list") + @TableField(exist = false) + private List parameterList; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/ProtocolPageDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ProtocolPageDto.java new file mode 100644 index 0000000..34ca82c --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ProtocolPageDto.java @@ -0,0 +1,16 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import com.cbsd.universaltestsoftware_client.config.BasePage; +import com.cbsd.universaltestsoftware_client.entity.Protocol; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class ProtocolPageDto extends BasePage { + + @ApiModelProperty("协议类型 1.解析 2.指令") + private Integer protocolType; + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/ScriptContentDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ScriptContentDto.java new file mode 100644 index 0000000..7a12a5f --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ScriptContentDto.java @@ -0,0 +1,14 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import lombok.Data; + +@Data +public class ScriptContentDto { + + private String instructId; + + private String scriptType; + + private String scriptContent; + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/ScriptDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ScriptDto.java new file mode 100644 index 0000000..1f4679e --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ScriptDto.java @@ -0,0 +1,13 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import lombok.Data; + +@Data +public class ScriptDto { + + private String scriptType; + + private String scriptFileName; + + private String scriptName; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/ScriptFileContentDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ScriptFileContentDto.java new file mode 100644 index 0000000..80a2bcb --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ScriptFileContentDto.java @@ -0,0 +1,13 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import lombok.Data; + +@Data +public class ScriptFileContentDto { + + private String scriptType; + + private String filePath; + + private String content; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/SensorEquipmentDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/SensorEquipmentDto.java new file mode 100644 index 0000000..3f70193 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/SensorEquipmentDto.java @@ -0,0 +1,32 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import lombok.Data; + +@Data +public class SensorEquipmentDto { + + /** + * 设备id + */ + private String equipmentId; + /** + * 设备名称 + */ + private String equipmentName; + /** + * 设备描述 + */ + private String equipmentDesc; + /** + * 设备参数 + */ + private String equipmentParam; + /** + * 通道id + */ + private String channelId; + /** + * 通道id + */ + private String channelAddress; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/SensorEquipmentPageDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/SensorEquipmentPageDto.java new file mode 100644 index 0000000..74a702e --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/SensorEquipmentPageDto.java @@ -0,0 +1,15 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.cbsd.universaltestsoftware_client.entity.SensorEquipment; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class SensorEquipmentPageDto extends Page { + + private String equipmentName; + + private String channelId; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/ServerPageChannelDataDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ServerPageChannelDataDto.java new file mode 100644 index 0000000..4881f35 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/ServerPageChannelDataDto.java @@ -0,0 +1,27 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; +@Data +public class ServerPageChannelDataDto { + + @ApiModelProperty("开始时间") + private Long startTime; + + @ApiModelProperty("结束时间") + private Long endTime; + + @ApiModelProperty("条数") + @NotNull(message = "条数不能为空") + private Integer pageSize; + + @ApiModelProperty("数据看板id") + @NotEmpty(message = "数据看板id不能为空") + private String dataBoardId; + + private List channelIds; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/SortDto.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/SortDto.java new file mode 100644 index 0000000..ad4c8f5 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/SortDto.java @@ -0,0 +1,16 @@ +package com.cbsd.universaltestsoftware_client.dto; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@Data +public class SortDto { + + @NotBlank(message = "instructId不能为空") + private String instructId; + + @NotNull(message = "sort不能为空") + private Integer sort; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/dto/SubmitAndCompile.java b/src/main/java/com/cbsd/universaltestsoftware_client/dto/SubmitAndCompile.java new file mode 100644 index 0000000..7532fdc --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/dto/SubmitAndCompile.java @@ -0,0 +1,25 @@ +package com.cbsd.universaltestsoftware_client.dto; + + + +public class SubmitAndCompile { + + private String className; + private String code; + + public String getClassName() { + return className; + } + + public void setClassName(String className) { + this.className = className; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/entity/Board.java b/src/main/java/com/cbsd/universaltestsoftware_client/entity/Board.java new file mode 100644 index 0000000..a2068be --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/entity/Board.java @@ -0,0 +1,68 @@ +package com.cbsd.universaltestsoftware_client.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + *

+ * 板卡 + *

+ * + * @author admin + * @since 2025-08-26 + */ +@Getter +@Setter +/*@TableName("board")*/ +@ApiModel(value = "Board对象", description = "板卡") +public class Board { + + @ApiModelProperty("板卡id") + private String boardId; + + @ApiModelProperty("板卡类型主键Id") + @NotBlank(message = "板卡类型不能为空") + private String boardTypeId; + @NotBlank(message = "服务id不能为空") + private String serverId; + + @ApiModelProperty("板卡名字") + @NotBlank(message = "板卡名字不能为空") + private String boardName; + + @ApiModelProperty("板卡编号") + @NotBlank(message = "板卡编号不能为空") + private String boardIndex; + + @ApiModelProperty("状态,0不在线,1在线") + private Integer onlineState; + + @ApiModelProperty("通道数量") + private Integer channelNum; + + @ApiModelProperty("创建时间") + private String createTime; + + @ApiModelProperty("修改时间") + private String updateTime; + + @ApiModelProperty("是否删除0否1是") + private String isDelete; + + @ApiModelProperty("板卡类型名称 用户看") + @TableField(exist = false) + private String boardTypeName; + + + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/entity/BootLog.java b/src/main/java/com/cbsd/universaltestsoftware_client/entity/BootLog.java new file mode 100644 index 0000000..7f68322 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/entity/BootLog.java @@ -0,0 +1,65 @@ +package com.cbsd.universaltestsoftware_client.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * 系统启动日志表,记录应用程序运行过程中的日志信息 + *

+ * + * @author wt + * @since 2025-09-03 + */ +@Getter +@Setter +@TableName("boot_log") +@ApiModel(value = "BootLog对象", description = "系统启动日志表,记录应用程序运行过程中的日志信息") +public class BootLog implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("主键ID,自增长") + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @ApiModelProperty("事件ID,用于唯一标识一个日志事件") + private String eventId; + + @ApiModelProperty("事件发生时间") + @JsonFormat(shape = JsonFormat.Shape.STRING,pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime eventDate; + + @ApiModelProperty("产生日志的线程名称") + private String thread; + + @ApiModelProperty("产生日志的类名") + private String logClass; + + @ApiModelProperty("产生日志的方法/函数名") + private String method; + + @ApiModelProperty("日志消息内容") + private String message; + + @ApiModelProperty("异常堆栈信息") + private String exception; + + @ApiModelProperty("日志级别(如:DEBUG, INFO, WARN, ERROR)") + private String level; + + @ApiModelProperty("日志记录时间") + @JsonFormat(shape = JsonFormat.Shape.STRING,pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime time; + + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/entity/Channel.java b/src/main/java/com/cbsd/universaltestsoftware_client/entity/Channel.java new file mode 100644 index 0000000..b34625e --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/entity/Channel.java @@ -0,0 +1,65 @@ +package com.cbsd.universaltestsoftware_client.entity; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +@Data +public class Channel { + @ApiModelProperty("通道id") + @TableId(value = "channel_id", type = IdType.ASSIGN_UUID) + private String channelId; + + @ApiModelProperty("通道类型id") + private String channelTypeId; + + @ApiModelProperty("服务id") + @NotBlank(message = "服务id不能为空") + private String serverId; + + @ApiModelProperty("通道路径") + private String channelPath; + + @ApiModelProperty("板卡id") + private String boardId; + + @ApiModelProperty("解析数量") + private int parsingQuantity; + + @ApiModelProperty("字节流量") + private int byteCount; + + @ApiModelProperty("通道名称") + private String channelName; + + @ApiModelProperty("创建时间") + @TableField(fill = FieldFill.INSERT) + private String createTime; + + @ApiModelProperty("更新时间") + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateTime; + + @ApiModelProperty("是否删除0否1是") + private String isDelete; + + @ApiModelProperty("读写通道类型") + private String rwChannelType; + + @ApiModelProperty("是否允许修改通道类型 ") + private Integer isUpType; + + @ApiModelProperty("通道类型 程序使用") + @TableField(exist = false) + private String channelType; + + @ApiModelProperty("通道类型 用户看") + @TableField(exist = false) + private String channelTypeName; + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/entity/ChannelData.java b/src/main/java/com/cbsd/universaltestsoftware_client/entity/ChannelData.java new file mode 100644 index 0000000..b168cad --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/entity/ChannelData.java @@ -0,0 +1,46 @@ +package com.cbsd.universaltestsoftware_client.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 解析的数据 + * @TableName channel_data + */ +@TableName(value ="channel_data") +@Data +public class ChannelData { + /** + * 解析数据id + */ + @TableId + private String dataId; + + /** + * 通道id + */ + private String channelId; + + /** + * 数据内容 + */ + private String data; + + /** + * 协议id + */ + private String protocolId; + + /** + * 协议内容 + */ + private String protocolContent; + + /** + * 数据时间 + */ + private String time; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/entity/ChannelDataHistory.java b/src/main/java/com/cbsd/universaltestsoftware_client/entity/ChannelDataHistory.java new file mode 100644 index 0000000..bdc4c78 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/entity/ChannelDataHistory.java @@ -0,0 +1,61 @@ +package com.cbsd.universaltestsoftware_client.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; + +/** + *

+ * 通道历史数据 + *

+ * + * @author wt + * @since 2025-09-28 + */ +@Getter +@Setter +@TableName("channel_data_history") +@ApiModel(value = "ChannelDataHistory对象", description = "通道历史数据") +public class ChannelDataHistory implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("通道历史数据id") + @TableId(value = "channel_data_history_id", type = IdType.ASSIGN_UUID) + private String channelDataHistoryId; + + @ApiModelProperty("通道id") + private String channelId; + + @ApiModelProperty("创建时间") + private String createTime; + + + @ApiModelProperty("是否删除0否1是") + private String isDelete; + + @ApiModelProperty("数据信息") + private byte[] content; + + @ApiModelProperty("数据字符串") + @TableField(exist = false) + private String contentString; + + @ApiModelProperty("时间戳") + private Long timestamp; + + /** + * 额外数据 + */ + @ApiModelProperty("额外数据") + private String extraData; + + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/entity/ChannelDataHit.java b/src/main/java/com/cbsd/universaltestsoftware_client/entity/ChannelDataHit.java new file mode 100644 index 0000000..1425f60 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/entity/ChannelDataHit.java @@ -0,0 +1,41 @@ +package com.cbsd.universaltestsoftware_client.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 通道协议命中次数 + * @TableName channel_data_hit + */ +@TableName(value ="channel_data_hit") +@Data +public class ChannelDataHit { + /** + * 命中id + */ + @TableId + private String hitId; + + /** + * 通道id + */ + private String channelId; + + /** + * 协议id + */ + private String protocolId; + + /** + * 命中次数 + */ + private Integer hitCount; + + /** + * 命中更新时间 + */ + private String updateTime; +} \ No newline at end of file diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/entity/ChannelDataTraffic.java b/src/main/java/com/cbsd/universaltestsoftware_client/entity/ChannelDataTraffic.java new file mode 100644 index 0000000..26cf18e --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/entity/ChannelDataTraffic.java @@ -0,0 +1,36 @@ +package com.cbsd.universaltestsoftware_client.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 通道流量统计 + * @TableName channel_data_traffic + */ +@TableName(value ="channel_data_traffic") +@Data +public class ChannelDataTraffic { + /** + * 数据流量id + */ + @TableId + private String dataTrafficId; + + /** + * 通道id + */ + private String channelId; + + /** + * 字节数 + */ + private Integer byteCount; + + /** + * 更新时间 + */ + private String updateTime; +} \ No newline at end of file diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/entity/DataBoard.java b/src/main/java/com/cbsd/universaltestsoftware_client/entity/DataBoard.java new file mode 100644 index 0000000..097f50c --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/entity/DataBoard.java @@ -0,0 +1,50 @@ +package com.cbsd.universaltestsoftware_client.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + *

+ * 数据看板 + *

+ * + * @author admin + * @since 2025-09-10 + */ +@Getter +@Setter +@TableName("data_board") +@ApiModel(value = "DataBoard对象", description = "数据看板") +public class DataBoard { + + @ApiModelProperty("数据看板id") + @TableId(value = "data_board_id", type = IdType.ASSIGN_UUID) + private String dataBoardId; + + @ApiModelProperty("看板类型,0:表格,1:曲线") + @TableField("data_board_type") + private Integer dataBoardType; + + @ApiModelProperty("看板名字") + @TableField("data_board_name") + private String dataBoardName; + + @ApiModelProperty("创建时间") + @TableField("create_time") + private String createTime; + + @ApiModelProperty("0:遥测,1:数据回放") + private Integer dataType; + + @ApiModelProperty("通道list") + @TableField(exist = false) + private List dataBoardChannelList; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/entity/DataBoardChannel.java b/src/main/java/com/cbsd/universaltestsoftware_client/entity/DataBoardChannel.java new file mode 100644 index 0000000..e414b38 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/entity/DataBoardChannel.java @@ -0,0 +1,75 @@ +package com.cbsd.universaltestsoftware_client.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.cbsd.universaltestsoftware_client.dto.ProtocolNameListDto; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + *

+ * + *

+ * + * @author admin + * @since 2025-09-10 + */ +@Getter +@Setter +@TableName("data_board_channel") +@ApiModel(value = "DataBoardChannel对象", description = "") +public class DataBoardChannel { + + @ApiModelProperty("数据看板通道id") + @TableId(value = "data_board_channel_id", type = IdType.ASSIGN_UUID) + private String dataBoardChannelId; + + @ApiModelProperty("数据看板id") + @TableField("data_board_id") + private String dataBoardId; + + @ApiModelProperty("通道Id") + @TableField("channel_id") + private String channelId; + + @ApiModelProperty("通道名字") + @TableField("channel_name") + private String channelName; + + @ApiModelProperty("参数名字") + @TableField("parameter") + private String parameter; + + @ApiModelProperty("协议id") + private String protocolId; + + @ApiModelProperty("协议名字") + private String protocolName; + + @ApiModelProperty("索引") + private Integer sn; + + @ApiModelProperty("创建时间") + @TableField("create_time") + private String createTime; + +// @ApiModelProperty("协议参数list") +// @TableField(exist = false) +// private List parameterList; + + @ApiModelProperty("协议list") + @TableField(exist = false) + private List protocolNameList; + + @ApiModelProperty("是否左轴") + private Integer isLeftYaxis; + + @ApiModelProperty("颜色") + private String legendColor; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/entity/Download.java b/src/main/java/com/cbsd/universaltestsoftware_client/entity/Download.java new file mode 100644 index 0000000..f1e8468 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/entity/Download.java @@ -0,0 +1,59 @@ +package com.cbsd.universaltestsoftware_client.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.Setter; + +/** + *

+ * + *

+ * + * @author admin + * @since 2026-02-03 + */ +@Getter +@Setter +@TableName("download") +@ApiModel(value = "Download对象", description = "") +public class Download { + + @ApiModelProperty("下载id") + @TableId(value = "download_id", type = IdType.ASSIGN_UUID) + private String downloadId; + + @ApiModelProperty("开始时间") + @TableField("start_time") + private String startTime; + + @ApiModelProperty("结束时间") + @TableField("end_time") + private String endTime; + + @ApiModelProperty("创建时间") + @TableField("create_time") + private String createTime; + + @ApiModelProperty("通道名字") + @TableField("channel_name") + private String channelName; + + @ApiModelProperty("协议名字") + @TableField("protocol_name") + private String protocolName; + + @ApiModelProperty("1:生成中,2:已完成") + @TableField("state") + private Integer state; + + @ApiModelProperty("下载地址") + @TableField("download_url") + private String downloadUrl; + @ApiModelProperty("类型 1:解析数据 ,2:原数据") + private Integer type; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/entity/Instruct.java b/src/main/java/com/cbsd/universaltestsoftware_client/entity/Instruct.java new file mode 100644 index 0000000..2a66322 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/entity/Instruct.java @@ -0,0 +1,190 @@ +package com.cbsd.universaltestsoftware_client.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotNull; +import java.util.ArrayList; +import java.util.List; + +/** + *

+ * + *

+ * + * @author admin + * @since 2025-09-01 + */ +@Getter +@Setter +@TableName("instruct") +@ApiModel(value = "Instruct对象", description = "") +public class Instruct { + + @ApiModelProperty("指令id") + @TableId(value = "instruct_id", type = IdType.ASSIGN_UUID) + private String instructId; + + + @ApiModelProperty("指令类型 1.常规指令 2.指令组") + @TableField("instruct_type") + private Integer instructType; + + @ApiModelProperty("指令名字") + @TableField("instruct_name") + private String instructName; + + @ApiModelProperty("用列id") + @TableField("invocation_id") + private String invocationId; + + @ApiModelProperty("服务id") + @TableField("server_id") + private String serverId; + + @ApiModelProperty("ip地址") + private String ipAddress; + + @ApiModelProperty("服务端tcp端口") + private Integer tcpPort; + + @ApiModelProperty("指令数据") + @TableField("instruct_text") + private String instructText; + + @ApiModelProperty("指令数据列表") + private String instructTextData; + + @ApiModelProperty("指令寄存器方法名称") + @TableField("instruct_function_name") + private String instructFunctionName; + + @ApiModelProperty("通道id") + @TableField("channel_id") + private String channelId; + + @ApiModelProperty("协议id") + private String protocolId; + + @ApiModelProperty("指令类型 0.数据 1.寄存器") + private Integer operationType; + + @ApiModelProperty("额外数据") + private String extraData; + + @ApiModelProperty("板卡编号") + private String boardIndex; + + @ApiModelProperty("板卡类型名称") + private String boardTypeName; + + @ApiModelProperty("状态0:禁用,1:启用") + private Integer status; + + @ApiModelProperty("父级编码") + private String parentEncoding; + + @ApiModelProperty("父级名字") + private String parentName; + + @ApiModelProperty("排序") + @NotNull(message = "排序不能为空") + private Integer sort; + + @ApiModelProperty("通道名字") + private String channelName; + @ApiModelProperty("前置时间") + private Integer leadTime; + + @ApiModelProperty("后置时间") + private Integer tailTime; + + @ApiModelProperty("命中次数") + private Integer hitCount; + + @ApiModelProperty("最后命中时间") + private String lastHitTime; + + @ApiModelProperty("指令标识") + private String instructTag; + + @ApiModelProperty("预处理脚本") + private String preProcessScript; + + @ApiModelProperty("预处理脚本名称") + private String preProcessScriptName; + + @ApiModelProperty("预处理脚本文件") + private String preProcessScriptFile; + + @ApiModelProperty("预处理脚本参数") + private String preProcessScriptParam; + + @ApiModelProperty("预处理脚本注释") + private String preProcessScriptComment; + + @ApiModelProperty("重复次数") + private Integer repeatCount; + + @ApiModelProperty("分支条件脚本") + private String branchConditionScript; + + @ApiModelProperty("分支条件脚本名称") + private String branchConditionScriptName; + + @ApiModelProperty("分支条件脚本文件") + private String branchConditionScriptFile; + + @ApiModelProperty("分支条件脚本参数") + private String branchConditionScriptParam; + + @ApiModelProperty("分支条件脚本注释") + private String branchConditionScriptComment; + + @ApiModelProperty("检查脚本") + private String checkScript; + + @ApiModelProperty("检查脚本名称") + private String checkScriptName; + + @ApiModelProperty("检查脚本文件") + private String checkScriptFile; + + @ApiModelProperty("检查脚本注释") + private String checkScriptComment; + + @ApiModelProperty("指令数据文件") + private String instructDataFile; + + @ApiModelProperty("跳转目标") + private String jumpTarget; + + @ApiModelProperty("断点 1.是 0.否") + private Integer breakPoint; + + @ApiModelProperty("编码") + private String encoding; + + @TableField(exist = false) + private List children=new ArrayList<>(); + + @ApiModelProperty("协议名称") + private String protocolName; + + @ApiModelProperty("数据拆分长度,单位:字节") + private Integer dataSplittingLength; + + /** + * 是否需要运行 + */ + @TableField(exist = false) + private boolean needRun; + + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/entity/Invocation.java b/src/main/java/com/cbsd/universaltestsoftware_client/entity/Invocation.java new file mode 100644 index 0000000..a2aad49 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/entity/Invocation.java @@ -0,0 +1,48 @@ +package com.cbsd.universaltestsoftware_client.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.Setter; + +/** + *

+ * 用例 + *

+ * + * @author admin + * @since 2025-09-01 + */ +@Getter +@Setter +@TableName("invocation") +@ApiModel(value = "Invocation对象", description = "用例") +public class Invocation { + + @ApiModelProperty("调用用例id") + @TableId(value = "invocation_id", type = IdType.ASSIGN_UUID) + private String invocationId; + + @ApiModelProperty("用例名字") + @TableField("invocation_name") + private String invocationName; + + @ApiModelProperty("用例序号") + private Integer sort; + + @ApiModelProperty("方案id") + @TableField("scheme_id") + private String schemeId; + + @ApiModelProperty("创建时间") + @TableField("create_time") + private String createTime; + + @ApiModelProperty("修改时间") + @TableField("update_time") + private String updateTime; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/entity/ParseData.java b/src/main/java/com/cbsd/universaltestsoftware_client/entity/ParseData.java new file mode 100644 index 0000000..b8af574 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/entity/ParseData.java @@ -0,0 +1,36 @@ +package com.cbsd.universaltestsoftware_client.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 解析的数据 + * @TableName parse_data + */ +@TableName(value ="parse_data") +@Data +public class ParseData { + /** + * 解析数据id + */ + @TableId + private String dataId; + + /** + * 通道id + */ + private String channelId; + + /** + * 数据内容 + */ + private String data; + + /** + * 数据时间 + */ + private String time; +} \ No newline at end of file diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/entity/Parser.java b/src/main/java/com/cbsd/universaltestsoftware_client/entity/Parser.java new file mode 100644 index 0000000..8bf02ac --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/entity/Parser.java @@ -0,0 +1,61 @@ +package com.cbsd.universaltestsoftware_client.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.List; + +/** + * 解析器 + * @TableName parser + */ +@TableName(value ="parser") +@Data +public class Parser { + /** + * 解析器id + */ + @TableId(value = "parser_id", type = IdType.ASSIGN_UUID) + private String parserId; + + /** + * 解析器名称 + */ + @ApiModelProperty("解析器名称") + private String parserName; + + /** + * 解析器顺序 + */ + @ApiModelProperty("解析器顺序") + private Integer parserSort; + + /** + * 通道id + */ + @ApiModelProperty("通道id") + private String channelId; + + /** + * 是否删除 1.是 0.否 + */ + @ApiModelProperty("是否删除 1.是 0.否") + private Integer isDelete; + + /** + * 创建时间 + */ + @ApiModelProperty("创建时间") + private String createTime; + + /** + * 协议个数 + */ + @TableField(exist = false) + @ApiModelProperty("协议个数") + private Long protocolCount; +} \ No newline at end of file diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/entity/ParserProtocol.java b/src/main/java/com/cbsd/universaltestsoftware_client/entity/ParserProtocol.java new file mode 100644 index 0000000..afe308b --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/entity/ParserProtocol.java @@ -0,0 +1,48 @@ +package com.cbsd.universaltestsoftware_client.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +/** + * 解析器协议 + * @TableName parser_protocol + */ +@TableName(value ="parser_protocol") +@Data +public class ParserProtocol { + /** + * 解析器协议id + */ + @TableId(value = "parser_protocol_id",type = IdType.ASSIGN_UUID) + private String parserProtocolId; + + /** + * 解析器id + */ + @ApiModelProperty("解析器id") + @NotBlank(message = "解析器id不能为空") + private String parserId; + + /** + * 协议id + */ + @ApiModelProperty("协议id") + @NotBlank(message = "协议id不能为空") + private String protocolId; + + @TableField(exist = false) + @ApiModelProperty("协议名字") + private String protocolName; + + /** + * 创建时间 + */ + @ApiModelProperty("创建时间") + private String createTime; +} \ No newline at end of file diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/entity/Protocol.java b/src/main/java/com/cbsd/universaltestsoftware_client/entity/Protocol.java new file mode 100644 index 0000000..252cc6b --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/entity/Protocol.java @@ -0,0 +1,68 @@ +package com.cbsd.universaltestsoftware_client.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.cbsd.universaltestsoftware_client.parser.model.Filed; +import com.cbsd.universaltestsoftware_client.parser.model.ProtocolXml; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.Setter; + +import java.util.Map; + +/** + *

+ * 协议 + *

+ * + * @author admin + * @since 2025-09-02 + */ +@Getter +@Setter +@TableName("protocol") +@ApiModel(value = "Protocol对象", description = "协议") +public class Protocol { + + @ApiModelProperty("协议id") + @TableId(value = "protocol_id", type = IdType.ASSIGN_ID) + private String protocolId; + + @ApiModelProperty("协议名字") + @TableField("protocol_name") + private String protocolName; + + @ApiModelProperty("协议内容") + @TableField("protocol_content") + private String protocolContent; + + @ApiModelProperty("协议类型 1.解析 2.指令") + @TableField("protocol_type") + private Integer protocolType; + + @ApiModelProperty("协议标签") + @TableField("protocol_tag") + private String protocolTag; + + @ApiModelProperty("创建时间") + @TableField("create_time") + private String createTime; + + @ApiModelProperty("修改时间") + @TableField("update_time") + private String updateTime; + + /** + * 协议xml + */ + @TableField(exist = false) + private ProtocolXml protocolXml; + /** + * 协议字段类型map + */ + @TableField(exist = false) + private Map filedMap; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/entity/Scheme.java b/src/main/java/com/cbsd/universaltestsoftware_client/entity/Scheme.java new file mode 100644 index 0000000..5fddcba --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/entity/Scheme.java @@ -0,0 +1,41 @@ +package com.cbsd.universaltestsoftware_client.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.Setter; + +/** + *

+ * 方案 + *

+ * + * @author admin + * @since 2025-09-01 + */ +@Getter +@Setter +@TableName("scheme") +@ApiModel(value = "Scheme对象", description = "方案") +public class Scheme { + + @ApiModelProperty("方案id") + @TableId(value = "scheme_id", type = IdType.ASSIGN_UUID) + private String schemeId; + + @ApiModelProperty("方案名字") + @TableField("scheme_name") + private String schemeName; + + @ApiModelProperty("创建时间") + @TableField("create_time") + private String createTime; + + @ApiModelProperty("修改时间") + @TableField("update_time") + private String updateTime; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/entity/SensorEquipment.java b/src/main/java/com/cbsd/universaltestsoftware_client/entity/SensorEquipment.java new file mode 100644 index 0000000..ebc8d5c --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/entity/SensorEquipment.java @@ -0,0 +1,39 @@ +package com.cbsd.universaltestsoftware_client.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +@TableName(value ="sensor_equipment") +@Data +public class SensorEquipment { + /** + * 设备id + */ + @TableId + private String equipmentId; + /** + * 设备名称 + */ + private String equipmentName; + /** + * 设备描述 + */ + private String equipmentDesc; + /** + * 设备参数 + */ + private String equipmentParam; + /** + * 通道id + */ + private String channelId; + /** + * 通道id + */ + private String channelAddress; + /** + * 创建时间 + */ + private String createTime; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/entity/ServerInfo.java b/src/main/java/com/cbsd/universaltestsoftware_client/entity/ServerInfo.java new file mode 100644 index 0000000..a6ee92a --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/entity/ServerInfo.java @@ -0,0 +1,76 @@ +package com.cbsd.universaltestsoftware_client.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + *

+ * 服务端 + *

+ * + * @author admin + * @since 2025-08-26 + */ +@Getter +@Setter +@TableName("server") +@ApiModel(value = "Server对象", description = "服务端") +public class ServerInfo { + + @ApiModelProperty("服务端id") + @TableId(value = "server_id", type = IdType.ASSIGN_UUID) + private String serverId; + + @ApiModelProperty("服务端名字") + @TableField("server_name") + @NotBlank(message = "服务端名字不能为空") + private String serverName; + + @ApiModelProperty("ip地址") + @TableField("ip_address") + @NotBlank(message = "ip地址不能为空") + private String ipAddress; + + @ApiModelProperty("服务端tcp端口") + + private Integer tcpPort; + + @ApiModelProperty("服务端http端口") + @NotNull(message = "服务端http端口不能为空") + private Integer httpPort; + + @ApiModelProperty("创建时间") + @TableField("create_time") + private String createTime; + + @ApiModelProperty("修改时间") + @TableField("update_time") + private String updateTime; + + @ApiModelProperty("是否删除0否1是") + @TableField("is_delete") + private String isDelete; + @ApiModelProperty("在线状态0:不在线,1在线") + @TableField(exist = false) + private Integer status; + + @ApiModelProperty("连接状态0:不连接,1要连接") + private Integer connectStatus; + + @ApiModelProperty("重连初始延迟") + private int reconnectInitialDelay = 1000; + @ApiModelProperty("重连最大延迟") + private int reconnectMaxDelay = 10000; + @ApiModelProperty("重连延迟乘数") + private double reconnectMultiplier = 1.5; + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/entity/SystemConfig.java b/src/main/java/com/cbsd/universaltestsoftware_client/entity/SystemConfig.java new file mode 100644 index 0000000..58f838f --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/entity/SystemConfig.java @@ -0,0 +1,26 @@ +package com.cbsd.universaltestsoftware_client.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 系统配置(仅类型和内容) + * @TableName system_config + */ +@TableName(value ="system_config") +@Data +public class SystemConfig { + /** + * 类型: 1.指令显示列 + */ + @TableId + private Integer type; + + /** + * 内容 + */ + private String content; +} \ No newline at end of file diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/exception/BaseException.java b/src/main/java/com/cbsd/universaltestsoftware_client/exception/BaseException.java new file mode 100644 index 0000000..b519c33 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/exception/BaseException.java @@ -0,0 +1,26 @@ +package com.cbsd.universaltestsoftware_client.exception; + +import com.cbsd.universaltestsoftware_client.util.ResultCode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Data +@EqualsAndHashCode(callSuper=false) +@ToString +public class BaseException extends RuntimeException { + + private static final long serialVersionUID = 2895464961740660192L; + + private int code= ResultCode.SYSTEM_ERROR; + + public BaseException(String message) { + super(message); + } + + public BaseException(int code, String message) { + super(message); + this.code = code; + } + +} \ No newline at end of file diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/exception/CustomizeException.java b/src/main/java/com/cbsd/universaltestsoftware_client/exception/CustomizeException.java new file mode 100644 index 0000000..3a95981 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/exception/CustomizeException.java @@ -0,0 +1,28 @@ +package com.cbsd.universaltestsoftware_client.exception; + + +import lombok.Data; +import lombok.EqualsAndHashCode; + + +@Data +@EqualsAndHashCode(callSuper=false) +public class CustomizeException extends RuntimeException{ + + /** + * + */ + private static final long serialVersionUID = 1L; + private String code; + + public CustomizeException(String message) { + super(message); + } + + public CustomizeException(String code, String message) { + super(message); + this.code = code; + } + + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/exception/DbInsertOrUpdateErrorExcetpion.java b/src/main/java/com/cbsd/universaltestsoftware_client/exception/DbInsertOrUpdateErrorExcetpion.java new file mode 100644 index 0000000..371d439 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/exception/DbInsertOrUpdateErrorExcetpion.java @@ -0,0 +1,10 @@ +package com.cbsd.universaltestsoftware_client.exception; + +/** + * 插入或者修改数据库错误异常 + */ +public class DbInsertOrUpdateErrorExcetpion extends Exception{ + public DbInsertOrUpdateErrorExcetpion(String msg){ + super(msg); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/exception/GlobalExceptionHandler.java b/src/main/java/com/cbsd/universaltestsoftware_client/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..0f98739 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/exception/GlobalExceptionHandler.java @@ -0,0 +1,97 @@ +package com.cbsd.universaltestsoftware_client.exception; + + +import com.cbsd.universaltestsoftware_client.util.Result; +import com.cbsd.universaltestsoftware_client.util.ResultCode; +import com.fasterxml.jackson.core.JsonProcessingException; +import lombok.extern.log4j.Log4j2; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingResult; +import org.springframework.validation.ObjectError; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.validation.ConstraintViolationException; + +import java.util.List; + +@Log4j2 +@ControllerAdvice +public class GlobalExceptionHandler { + + @ResponseBody + @ExceptionHandler({Exception.class}) + public Result handleException(HttpServletRequest request, HttpServletResponse response, Exception e) { + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/json; charset=utf-8"); + log.info("程序发生全局异常", e); + int code = ResultCode.SYSTEM_ERROR; + String message = ""; + if (e instanceof JsonProcessingException) { + message = "对象转json出错"; + } else if (e instanceof ConstraintViolationException) { + ConstraintViolationException constraintViolationException = (ConstraintViolationException) e; + code = ResultCode.SYSTEM_ERROR; + //获得第一个点的位置 + String str = constraintViolationException.getMessage(); + int index=str.indexOf(":"); + if (index!=-1) { + System.out.println(index); + //根据第二个点的位置,截取 字符串。得到结果 result + str=str.substring(index+1); + + } + message = str.trim(); + } else if (e instanceof BindException) { + BindException bindException = (BindException) e; + BindingResult bindingResult = bindException.getBindingResult(); + List objectErrorList = bindingResult.getAllErrors(); + if (objectErrorList != null && objectErrorList.size() > 0) { + for (ObjectError objectError : objectErrorList) { + message = objectError.getDefaultMessage(); + } + } + code = ResultCode.SYSTEM_ERROR; + } else if (e instanceof MethodArgumentNotValidException) { + MethodArgumentNotValidException me = (MethodArgumentNotValidException) e; + message = me.getBindingResult().getAllErrors().get(0).getDefaultMessage(); + code = ResultCode.SYSTEM_ERROR; + } else if (e instanceof DuplicateKeyException) { +// DuplicateKeyException ex = (DuplicateKeyException) e; +// String msg = ex.getMessage(); +// msg.indexOf("entry") +// Duplicate entry '时尚' for key 'excel_name' + message = "不允许插入重复数据"; + } else if (e instanceof BaseException) { + BaseException be = (BaseException) e; + code = be.getCode(); + message = be.getMessage(); + } else if (e instanceof IllegalArgumentException) { + message = "参数不合法"; + } else if (e instanceof HttpRequestMethodNotSupportedException) { + message = "接口请求不规范,请检查接口请求"; + }else if (e instanceof HttpMessageNotReadableException){ + message = "接口请求不规范,参数格式错误"; +// } else if (e instanceof UnauthorizedException || e instanceof AuthorizationException) { +// message = "权限不足,请联系管理员。"; + } else if (e instanceof RuntimeException){ + message = e.getMessage(); + } else if( e instanceof NullPointerException){ + message = "参数不能为空"; + } else { + message = e.getMessage(); + } + return handleErrorInfo(code, message); + } + + private Result handleErrorInfo(int code, String message) { + return Result.error(code, message); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/mapper/BootLogMapper.java b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/BootLogMapper.java new file mode 100644 index 0000000..8adb76e --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/BootLogMapper.java @@ -0,0 +1,17 @@ +package com.cbsd.universaltestsoftware_client.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.cbsd.universaltestsoftware_client.entity.BootLog; + +/** + *

+ * Mapper 接口 + *

+ * + * @author wt + * @since 2025-09-03 + */ +public interface BootLogMapper extends BaseMapper { + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/mapper/ChannelDataHitMapper.java b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/ChannelDataHitMapper.java new file mode 100644 index 0000000..0dd025b --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/ChannelDataHitMapper.java @@ -0,0 +1,18 @@ +package com.cbsd.universaltestsoftware_client.mapper; + +import com.cbsd.universaltestsoftware_client.entity.ChannelDataHit; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author SanJie +* @description 针对表【channel_data_hit(通道协议命中次数)】的数据库操作Mapper +* @createDate 2026-01-23 16:24:09 +* @Entity com.cbsd.universaltestsoftware_client.entity.ChannelDataHit +*/ +public interface ChannelDataHitMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/mapper/ChannelDataMapper.java b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/ChannelDataMapper.java new file mode 100644 index 0000000..b76cc77 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/ChannelDataMapper.java @@ -0,0 +1,18 @@ +package com.cbsd.universaltestsoftware_client.mapper; + +import com.cbsd.universaltestsoftware_client.entity.ChannelData; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author SanJie +* @description 针对表【channel_data(解析的数据)】的数据库操作Mapper +* @createDate 2025-09-09 09:53:44 +* @Entity com.cbsd.universaltestsoftware_client.entity.ChannelData +*/ +public interface ChannelDataMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/mapper/ChannelDataTrafficMapper.java b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/ChannelDataTrafficMapper.java new file mode 100644 index 0000000..a60034a --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/ChannelDataTrafficMapper.java @@ -0,0 +1,18 @@ +package com.cbsd.universaltestsoftware_client.mapper; + +import com.cbsd.universaltestsoftware_client.entity.ChannelDataTraffic; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author SanJie +* @description 针对表【channel_data_traffic(通道流量统计)】的数据库操作Mapper +* @createDate 2026-01-23 10:07:19 +* @Entity com.cbsd.universaltestsoftware_client.entity.ChannelDataTraffic +*/ +public interface ChannelDataTrafficMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/mapper/DataBoardChannelMapper.java b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/DataBoardChannelMapper.java new file mode 100644 index 0000000..6b7d9da --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/DataBoardChannelMapper.java @@ -0,0 +1,20 @@ +package com.cbsd.universaltestsoftware_client.mapper; + +import com.cbsd.universaltestsoftware_client.config.IBaseMapper; +import com.cbsd.universaltestsoftware_client.entity.DataBoardChannel; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import java.util.List; + +/** + *

+ * Mapper 接口 + *

+ * + * @author admin + * @since 2025-09-10 + */ +public interface DataBoardChannelMapper extends IBaseMapper { + + List getChannelList(String dataBoardId); +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/mapper/DataBoardMapper.java b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/DataBoardMapper.java new file mode 100644 index 0000000..56baf31 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/DataBoardMapper.java @@ -0,0 +1,16 @@ +package com.cbsd.universaltestsoftware_client.mapper; + +import com.cbsd.universaltestsoftware_client.entity.DataBoard; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + *

+ * 数据看板 Mapper 接口 + *

+ * + * @author admin + * @since 2025-09-10 + */ +public interface DataBoardMapper extends BaseMapper { + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/mapper/DownloadMapper.java b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/DownloadMapper.java new file mode 100644 index 0000000..9cdecd6 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/DownloadMapper.java @@ -0,0 +1,16 @@ +package com.cbsd.universaltestsoftware_client.mapper; + +import com.cbsd.universaltestsoftware_client.entity.Download; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + *

+ * Mapper 接口 + *

+ * + * @author admin + * @since 2026-02-03 + */ +public interface DownloadMapper extends BaseMapper { + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/mapper/InstructMapper.java b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/InstructMapper.java new file mode 100644 index 0000000..e565c66 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/InstructMapper.java @@ -0,0 +1,70 @@ +package com.cbsd.universaltestsoftware_client.mapper; + +import com.cbsd.universaltestsoftware_client.config.IBaseMapper; +import com.cbsd.universaltestsoftware_client.entity.Instruct; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + *

+ * Mapper 接口 + *

+ * + * @author admin + * @since 2025-09-01 + */ +public interface InstructMapper extends IBaseMapper { + + + /** + * 查找当前节点的前一个兄弟节点(同父节点下,sort < 当前,取最大) + */ + Instruct findPrevSibling(@Param("sort") Integer sort, @Param("parentEncoding") String parentEncoding, @Param("invocationId") String invocationId); + + /** + * 查找当前节点的下一个兄弟节点(同父节点下,sort > 当前,取最小) + */ + Instruct findNextSibling(@Param("sort") Integer sort, @Param("parentEncoding") String parentEncoding, @Param("invocationId") String invocationId); + + /** + * 更新指令(只更新 sort 字段也可单独抽离) + */ + void updateSort(Instruct instruct); + + + /** + * 按 invocationId 全局重排 sort(1,2,3…) + */ + void reorderGlobalSortContinuous(@Param("invocationId") String invocationId); + + + // 根据 ids + invocationId 查询 + List selectByIdsAndInvocation(@Param("ids") List ids, + @Param("invocationId") String invocationId); + + // 根据 parentEncoding 精确查询直接子节点(不含孙) + List listByParentEncodingAndInvocation(@Param("invocationId") String invocationId, + @Param("parentCode") String parentCode); + + + void setSortZeroByIds(@Param("ids") List ids); + + + void shiftSortAfter(@Param("invocationId") String invocationId, @Param("sort") Integer sort,String instructId); + + /** + * 区间-1 + * @param invocationId + * @param + * @param instructId + */ + void minusSort(@Param("invocationId") String invocationId, @Param("oldSort") Integer oldSort,@Param("newSort") Integer newSort,@Param("instructId") String instructId); + + /** + * 区间+1 + */ + void plusSort(@Param("invocationId") String invocationId, @Param("oldSort") Integer oldSort,@Param("newSort") Integer newSort,@Param("instructId") String instructId); + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/mapper/InvocationMapper.java b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/InvocationMapper.java new file mode 100644 index 0000000..2dad0c1 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/InvocationMapper.java @@ -0,0 +1,22 @@ +package com.cbsd.universaltestsoftware_client.mapper; + +import com.cbsd.universaltestsoftware_client.entity.Invocation; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + *

+ * 用列 Mapper 接口 + *

+ * + * @author admin + * @since 2025-09-01 + */ +public interface InvocationMapper extends BaseMapper { + + void subtractSort(String schemeId, int newSort, int oldSort); + + void addSort(String schemeId, int newSort, int oldSort); + + void unificationSubtractSort(String schemeId, int startSort); + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/mapper/ParserMapper.java b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/ParserMapper.java new file mode 100644 index 0000000..21bb8bc --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/ParserMapper.java @@ -0,0 +1,18 @@ +package com.cbsd.universaltestsoftware_client.mapper; + +import com.cbsd.universaltestsoftware_client.entity.Parser; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author SanJie +* @description 针对表【parser(解析器)】的数据库操作Mapper +* @createDate 2025-09-04 15:55:48 +* @Entity com.cbsd.universaltestsoftware_client.entity.Parser +*/ +public interface ParserMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/mapper/ParserProtocolMapper.java b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/ParserProtocolMapper.java new file mode 100644 index 0000000..81b7fe7 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/ParserProtocolMapper.java @@ -0,0 +1,18 @@ +package com.cbsd.universaltestsoftware_client.mapper; + +import com.cbsd.universaltestsoftware_client.entity.ParserProtocol; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author SanJie +* @description 针对表【parser_protocol(解析器协议)】的数据库操作Mapper +* @createDate 2025-09-04 15:56:52 +* @Entity com.cbsd.universaltestsoftware_client.entity.ParserProtocol +*/ +public interface ParserProtocolMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/mapper/ProtocolMapper.java b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/ProtocolMapper.java new file mode 100644 index 0000000..832dc16 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/ProtocolMapper.java @@ -0,0 +1,16 @@ +package com.cbsd.universaltestsoftware_client.mapper; + +import com.cbsd.universaltestsoftware_client.entity.Protocol; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + *

+ * 协议 Mapper 接口 + *

+ * + * @author admin + * @since 2025-09-02 + */ +public interface ProtocolMapper extends BaseMapper { + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/mapper/SchemeMapper.java b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/SchemeMapper.java new file mode 100644 index 0000000..6c56bb7 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/SchemeMapper.java @@ -0,0 +1,16 @@ +package com.cbsd.universaltestsoftware_client.mapper; + +import com.cbsd.universaltestsoftware_client.entity.Scheme; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + *

+ * 方案 Mapper 接口 + *

+ * + * @author admin + * @since 2025-09-01 + */ +public interface SchemeMapper extends BaseMapper { + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/mapper/SensorEquipmentMapper.java b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/SensorEquipmentMapper.java new file mode 100644 index 0000000..4d4ece7 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/SensorEquipmentMapper.java @@ -0,0 +1,7 @@ +package com.cbsd.universaltestsoftware_client.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.cbsd.universaltestsoftware_client.entity.SensorEquipment; + +public interface SensorEquipmentMapper extends BaseMapper { +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/mapper/ServerMapper.java b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/ServerMapper.java new file mode 100644 index 0000000..a770465 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/ServerMapper.java @@ -0,0 +1,16 @@ +package com.cbsd.universaltestsoftware_client.mapper; + +import com.cbsd.universaltestsoftware_client.entity.ServerInfo; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + *

+ * 服务端 Mapper 接口 + *

+ * + * @author admin + * @since 2025-08-26 + */ +public interface ServerMapper extends BaseMapper { + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/mapper/SystemConfigMapper.java b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/SystemConfigMapper.java new file mode 100644 index 0000000..d572932 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/mapper/SystemConfigMapper.java @@ -0,0 +1,18 @@ +package com.cbsd.universaltestsoftware_client.mapper; + +import com.cbsd.universaltestsoftware_client.entity.SystemConfig; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author SanJie +* @description 针对表【system_config(系统配置(仅类型和内容))】的数据库操作Mapper +* @createDate 2025-09-19 11:18:42 +* @Entity com.cbsd.universaltestsoftware_client.entity.SystemConfig +*/ +public interface SystemConfigMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/config/NettyClientConfig.java b/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/config/NettyClientConfig.java new file mode 100644 index 0000000..b8587cf --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/config/NettyClientConfig.java @@ -0,0 +1,33 @@ +package com.cbsd.universaltestsoftware_client.nettyclient.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +/** + * Netty客户端配置类 + */ +@Configuration +public class NettyClientConfig { + + /** + * 创建线程池任务调度器,用于管理重连任务和异步操作 + */ + @Bean + public ThreadPoolTaskScheduler threadPoolTaskScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setPoolSize(5); + scheduler.setThreadNamePrefix("netty-client-scheduler-"); + scheduler.setAwaitTerminationSeconds(60); + scheduler.setWaitForTasksToCompleteOnShutdown(true); + return scheduler; + } + @Bean + public ScheduledExecutorService scheduledExecutorService() { + ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); + return scheduledExecutorService; + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/config/ServerConfig.java b/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/config/ServerConfig.java new file mode 100644 index 0000000..2a5a23a --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/config/ServerConfig.java @@ -0,0 +1,19 @@ +package com.cbsd.universaltestsoftware_client.nettyclient.config; + +import com.cbsd.universaltestsoftware_client.entity.ServerInfo; +import lombok.Data; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/** + * 服务端配置信息 + */ +@Component +@Data +public class ServerConfig { + // 服务端配置集合,key为服务端标识,value为服务端信息 + private Map servers = new HashMap<>(); + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/connection/ConnectionManager.java b/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/connection/ConnectionManager.java new file mode 100644 index 0000000..67364cd --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/connection/ConnectionManager.java @@ -0,0 +1,332 @@ +package com.cbsd.universaltestsoftware_client.nettyclient.connection; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.cbsd.universaltestsoftware_client.entity.ChannelDataTraffic; +import com.cbsd.universaltestsoftware_client.entity.ServerInfo; +import com.cbsd.universaltestsoftware_client.mapper.ChannelDataTrafficMapper; +import com.cbsd.universaltestsoftware_client.mapper.ServerMapper; +import com.cbsd.universaltestsoftware_client.nettyclient.entity.ReceiveTcpContent; +import com.cbsd.universaltestsoftware_client.nettyclient.entity.SendTcpContent; +import com.cbsd.universaltestsoftware_client.parser.ParserManager; +import com.cbsd.universaltestsoftware_client.parser.ProtocolParserHelper; +import com.cbsd.universaltestsoftware_client.parser.model.OriginalData; +import com.cbsd.universaltestsoftware_client.service.ParserService; +import com.cbsd.universaltestsoftware_client.service.ServeService; +import com.cbsd.universaltestsoftware_client.util.DateUtils; +import com.cbsd.universaltestsoftware_client.util.UUIDUtils; +import com.cbsd.universaltestsoftware_client.websocket.NoticeWebSocketHandler; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; + +/** + * 多服务端连接管理器:统一管理连接生命周期、消息转发、状态查询 + */ +@Component +public class ConnectionManager implements ServerConnection.ConnectionListener { + private static final Logger logger = LoggerFactory.getLogger(ConnectionManager.class); + + private final ScheduledExecutorService scheduler; // 重连调度线程池 + private final Map connections = new ConcurrentHashMap<>(); // 连接缓存(线程安全) + + /** + * 协议解析线程map + */ + private final Map protocolParserMap = new ConcurrentHashMap<>(); // 连接缓存(线程安全) + /** + * 线程安全的消息队列, 用于统计通道发送数据流量 + */ + private static final BlockingQueue tcpQueue = new LinkedBlockingQueue<>(); + + @Resource + private NoticeWebSocketHandler noticeWebSocketHandler; + + @Resource + private ServerMapper serverMapper; + + @Resource + private ParserManager parserManager; + @Resource + private ChannelDataTrafficMapper channelDataTrafficMapper; + /** + * 解析器service + */ + @Resource + private ParserService parserService; + + @Resource + private ServeService serveService; + + + // 构造器注入依赖 + @Autowired + public ConnectionManager(ScheduledExecutorService scheduler) { + this.scheduler = scheduler; + } + + /** + * 初始化数据库中所有连接 + */ + @PostConstruct + public void initConnections() { + new Thread(() -> { + Map servers = new HashMap<>(); + List serverInfos = serverMapper.selectList(new LambdaQueryWrapper().eq(ServerInfo::getIsDelete, "0") + .eq(ServerInfo::getConnectStatus, 1) + ); + for (ServerInfo serverInfo : serverInfos) { + servers.put(serverInfo.getServerId(), serverInfo); + } + + if (!servers.isEmpty()) { + servers.forEach(this::addAndConnectServer); +// logger.info("已初始化{}个服务端连接", servers.size()); + } else { +// logger.warn("未找到任何服务端配置,请在数添加"); + } + }).start(); + + new Thread(() -> { + //处理数据流量 + while (true) { + updateChannelDataTraffic(); + } + }).start(); + + } + + /** + * 添加并启动新服务端连接 + */ + public void addAndConnectServer(String serverId, ServerInfo serverInfo) { + if (connections.containsKey(serverId)) { +// logger.warn("服务端[{}]已存在,跳过重复添加", serverId); + return; + } + // 创建连接实例并加入缓存 + ServerConnection connection = new ServerConnection(serverId, serverInfo, scheduler, this); + connections.put(serverId, connection); + // 异步启动连接(避免阻塞主线程) + scheduler.execute(connection::connect); +// logger.info("已添加服务端[{}],正在启动连接...", serverId); + } + + /** + * 强制移除服务端连接(彻底删除,不触发重连) + */ + public void removeServer(String serverId) { + ServerConnection conn = connections.remove(serverId); + if (conn != null) { + conn.disconnect(); // 主动断开并清理资源 +// logger.info("已移除服务端[{}]及其连接", serverId); + } else { +// logger.warn("服务端[{}]不存在,无法移除", serverId); + } + } + + /** + * 主动断开连接(会触发自动重连) + */ + public void disconnectServer(String serverId) { + ServerConnection conn = connections.get(serverId); + if (conn != null) { + conn.disconnect(); +// logger.info("已主动断开服务端[{}]连接,将自动重连", serverId); + } else { +// logger.warn("服务端[{}]不存在,无法断开", serverId); + } + } + + /** + * 手动触发重连(修复:直接调用connect,避免重复断开) + */ + public void reconnectServer(String serverId) { + ServerConnection conn = connections.get(serverId); + if (conn != null) { + scheduler.execute(conn::connect); +// logger.info("已手动触发服务端[{}]重连", serverId); + } else { +// logger.warn("服务端[{}]不存在,无法重连", serverId); + } + } + + /** + * 向指定服务端发送十六进制消息(带参数校验) + */ + public void sendMessageToServer(String serverId, String hexMessage, String channelId, Integer portNumber, + SendTcpContent.OperationType operationType, String protocolXml, String extraData) { + // 前置参数校验 + if (serverId == null || serverId.trim().isEmpty()) { + logger.error("发送消息失败:服务端ID不能为空"); + return; + } + if (operationType == null) { + logger.error("发送消息失败:操作类型(operationType)不能为空"); + return; + } + // 获取连接并发送 + ServerConnection conn = connections.get(serverId); + if (conn != null) { + conn.sendHexMessage(hexMessage, channelId, portNumber, operationType, protocolXml, extraData); + } else { + logger.warn("服务端[{}]不存在,无法发送消息", serverId); + } + } + + /** + * 获取所有服务端ID + */ + public Set getAllServerIds() { + return connections.keySet(); + } + + /** + * 获取所有服务端连接状态(ID -> 连接状态) + */ + public Map getServerConnectionStatus() { + Map statusMap = new HashMap<>(connections.size()); + connections.forEach((serverId, conn) -> statusMap.put(serverId, conn.isConnected())); + return statusMap; + } + + /** + * 获取指定服务端连接实例 + */ + public ServerConnection getServerConnection(String serverId) { + return connections.get(serverId); + } + + /** + * 应用关闭时优雅清理所有连接(@PreDestroy:Spring容器销毁前执行) + */ + @PreDestroy + public void destroy() { +// logger.info("应用关闭中,开始清理所有服务端连接..."); + connections.values().forEach(ServerConnection::disconnect); + connections.clear(); +// logger.info("所有服务端连接已清理完成"); + } + + /* ===== 实现ServerConnection.ConnectionListener接口(连接事件回调)===== */ + @Override + public void onConnected(String serverId) { + logger.info("服务端[{}]连接成功", serverId); + } + + @Override + public void onDisconnected(String serverId) { + logger.warn("服务端[{}]连接断开,将自动重连", serverId); + reconnectServer(serverId); // 触发重连(调用修复后的reconnectServer) + } + + @Override + public void onConnectionFailed(String serverId) { + logger.error("服务端[{}]连接失败", serverId); + } + + @Override + public void onMessageReceived(String serverId, ReceiveTcpContent message) { + if (message == null || message.getOperationType() == null) { + return; + } + // 按消息类型转发(示例:通知消息通过WebSocket广播) + switch (message.getOperationType()) { + case DATA: + try { + tcpQueue.put(message); + } catch (InterruptedException e) { + e.printStackTrace(); + } + // 数据消息处理(可扩展:如转发到业务服务) + String channelId = message.getChannelId(); + String dataBoardId = message.getDataBoardId(); + String key = serverId + "_" + channelId + "_" + dataBoardId + "_" + message.getExtraData(); + ProtocolParserHelper protocolParserHelper = protocolParserMap.get(key); + if (protocolParserHelper == null) { + protocolParserHelper = new ProtocolParserHelper(key, channelId, dataBoardId, 1, parserService, parserManager); + protocolParserMap.put(key, protocolParserHelper); + } + protocolParserHelper.putData(new OriginalData(dataBoardId, message.getBytesContent(), message.getTimestamp())); + break; + case NOTICE: + // 通知消息广播到WebSocket客户端(空校验避免NPE) + if (message.getBytesContent() != null) { + noticeWebSocketHandler.broadcast(new String(message.getBytesContent())); + } else { +// logger.warn("服务端[{}]通知消息字节数组为空", serverId); + } + break; + } + } + + @Override + public void oncloseConnected(String serverId) { + serveService.disconnectServer(serverId); + //推送给前端 + noticeWebSocketHandler.broadcast(serverId); + } + + /** + * 移除原始数据 + * + * @param dataBoardId 数据看板id + * + */ + public void removeOriginalData(String dataBoardId) { + for (ProtocolParserHelper helper : protocolParserMap.values()) { + helper.removeOriginalData(dataBoardId); + } + } + + /** + * 更新通道数据流量 + */ + private void updateChannelDataTraffic() { + try { + ReceiveTcpContent message = tcpQueue.take(); + String channelId = message.getChannelId(); + int byteLength = message.getBytesContent().length; + if (StringUtils.isNotBlank(channelId)) { + // 第一步:尝试更新计数(原子操作) + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() + .eq(ChannelDataTraffic::getChannelId, channelId) + .setSql("byte_count = byte_count + " + byteLength) + .set(ChannelDataTraffic::getUpdateTime, DateUtils.dateToString()); + + int updateCount = channelDataTrafficMapper.update(null, updateWrapper); + // 第二步:如果更新行数为0,说明记录不存在,执行插入 + if (updateCount == 0) { + ChannelDataTraffic channelDataTraffic = new ChannelDataTraffic(); + channelDataTraffic.setDataTrafficId(UUIDUtils.getUUID()); + channelDataTraffic.setChannelId(channelId); + channelDataTraffic.setByteCount(byteLength); + channelDataTraffic.setUpdateTime(DateUtils.dateToString()); + // 插入时加重试,避免并发插入冲突 + try { + channelDataTrafficMapper.insert(channelDataTraffic); + } catch (Exception e) { + // 插入失败(唯一键冲突),再次执行更新 + channelDataTrafficMapper.update(null, updateWrapper); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/connection/ServerConnection.java b/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/connection/ServerConnection.java new file mode 100644 index 0000000..6569c8f --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/connection/ServerConnection.java @@ -0,0 +1,650 @@ +package com.cbsd.universaltestsoftware_client.nettyclient.connection; + +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.cbsd.universaltestsoftware_client.entity.ServerInfo; +import com.cbsd.universaltestsoftware_client.nettyclient.entity.FrameDecoder; +import com.cbsd.universaltestsoftware_client.nettyclient.entity.FrameEncoder; +import com.cbsd.universaltestsoftware_client.nettyclient.entity.ReceiveTcpContent; +import com.cbsd.universaltestsoftware_client.nettyclient.entity.SendTcpContent; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.util.concurrent.DefaultEventExecutorGroup; +import io.netty.util.concurrent.EventExecutorGroup; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +/** + * 服务端TCP连接管理类(高并发优化版) + * 重点优化大量消息的发送和接收性能 + */ +public class ServerConnection { + + private static final Logger logger = LoggerFactory.getLogger(ServerConnection.class); + + // 连接标识与配置 + private final String serverId; + private final ServerInfo serverInfo; + private final ScheduledExecutorService scheduler; + private final ConnectionListener connectionListener; + + // Netty核心组件 + private Channel channel; + private EventLoopGroup group; + private ScheduledFuture reconnectFuture; + // 用于接收消息的业务线程池(与IO线程分离) + private static final EventExecutorGroup businessExecutor = new DefaultEventExecutorGroup( + Runtime.getRuntime().availableProcessors() * 2, + new DefaultThreadFactory("business-executor-") + ); + + // 连接状态(线程安全) + private final AtomicInteger reconnectAttempts = new AtomicInteger(0); + private final AtomicBoolean isConnected = new AtomicBoolean(false); + private final AtomicBoolean isConnecting = new AtomicBoolean(false); + private final AtomicLong sendCounter = new AtomicLong(0); // 发送消息计数器 + private final AtomicLong receiveCounter = new AtomicLong(0); // 接收消息计数器 + + // 1. 消息发送优化:分级缓冲区 + 流量控制 + private final ConcurrentLinkedQueue highPriorityBuffer = new ConcurrentLinkedQueue<>(); + private final ConcurrentLinkedQueue normalBuffer = new ConcurrentLinkedQueue<>(); + private final int MAX_BUFFER_SIZE = 10000; // 总缓冲上限(根据内存调整) + private final AtomicInteger totalBuffered = new AtomicInteger(0); + private final AtomicBoolean isFlushing = new AtomicBoolean(false); // 防止重复触发缓冲刷新 + + // 2. 线程池优化:动态调整 + 合理参数 + private static final ThreadPoolExecutor sendWorkerPool = new ThreadPoolExecutor( + 4, // 核心线程数 + 16, // 最大线程数 + 30, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(5000), // 任务队列 + new DefaultThreadFactory("send-worker-"), + new ThreadPoolExecutor.CallerRunsPolicy() // 轻量拒绝策略,避免消息丢失 + ); + + /** + * 自定义线程工厂(统一命名 + 守护线程) + */ + static class DefaultThreadFactory implements ThreadFactory { + private final String prefix; + private final AtomicInteger threadNum = new AtomicInteger(1); + + public DefaultThreadFactory(String prefix) { + this.prefix = prefix; + } + + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r, prefix + threadNum.getAndIncrement()); + thread.setDaemon(true); // 守护线程,不阻塞JVM退出 + thread.setPriority(Thread.NORM_PRIORITY); + return thread; + } + } + + /** + * 构造器 + */ + public ServerConnection(String serverId, ServerInfo serverInfo, + ScheduledExecutorService scheduler, ConnectionListener connectionListener) { + this.serverId = serverId; + this.serverInfo = Objects.requireNonNull(serverInfo, "serverInfo cannot be null"); + this.scheduler = Objects.requireNonNull(scheduler, "scheduler cannot be null"); + this.connectionListener = connectionListener; + + // 初始化重连参数默认值 + if (this.serverInfo.getReconnectInitialDelay() <= 0) { + this.serverInfo.setReconnectInitialDelay(1000); + } + if (this.serverInfo.getReconnectMultiplier() <= 1.0) { + this.serverInfo.setReconnectMultiplier(2.0); + } + if (this.serverInfo.getReconnectMaxDelay() <= 0) { + this.serverInfo.setReconnectMaxDelay(30000); + } + } + + /** + * 建立与服务端的连接(优化Netty配置) + */ + public void connect() { + if (isConnected.get()) { +// logger.info("服务端[{}]已连接,无需重复连接", serverId); + return; + } + if (isConnecting.getAndSet(true)) { +// logger.info("服务端[{}]正在连接中,跳过本次请求", serverId); + return; + } + +// logger.info("开始连接服务端[{}]:{}:{}", serverId, serverInfo.getIpAddress(), serverInfo.getTcpPort()); + + // 校验服务端配置 + if (serverInfo == null || StringUtils.isBlank(serverInfo.getIpAddress()) || serverInfo.getTcpPort() <= 0) { +// logger.error("服务端[{}]配置无效:IP为空或端口非法", serverId); + isConnecting.set(false); + handleConnectionFailure(); + return; + } + + // 3. Netty配置优化:调整IO线程数 + TCP参数 + group = new NioEventLoopGroup(1); // 单个IO线程足够处理大量连接 + Bootstrap bootstrap = new Bootstrap(); + bootstrap.group(group) + .channel(NioSocketChannel.class) + // 优化TCP参数 + .option(ChannelOption.TCP_NODELAY, true) // 禁用Nagle算法,降低延迟 + .option(ChannelOption.SO_KEEPALIVE, true) // 启用心跳检测 + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) + .option(ChannelOption.SO_RCVBUF, 1024 * 1024) // 接收缓冲区1MB + .option(ChannelOption.SO_SNDBUF, 1024 * 1024) // 发送缓冲区1MB + // 4. 写缓冲区水位线控制:防止OOM + .option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark( + 32 * 1024, // 低水位:32KB + 512 * 1024 // 高水位:512KB + )) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) { + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast(new FrameDecoder()); + pipeline.addLast(new FrameEncoder()); + // 5. 接收消息使用业务线程池,避免阻塞IO线程 + pipeline.addLast(businessExecutor, new ClientHandler(ServerConnection.this)); + } + }); + + // 发起异步连接 + ChannelFuture future = bootstrap.connect(serverInfo.getIpAddress(), serverInfo.getTcpPort()); + future.addListener((ChannelFutureListener) channelFuture -> { + isConnecting.set(false); + + if (channelFuture.isSuccess()) { + channel = channelFuture.channel(); + isConnected.set(true); + reconnectAttempts.set(0); +// logger.info("服务端[{}]连接成功", serverId); + + // 监听通道关闭事件 + channel.closeFuture().addListener(closeFuture -> handleDisconnect()); + + // 注册可写性监听器,缓冲区可用时自动发送 + channel.pipeline().addLast(new ChannelDuplexHandler() { + @Override + public void channelWritabilityChanged(ChannelHandlerContext ctx) { + if (ctx.channel().isWritable()) { + triggerFlush(); // 通道可写时触发缓冲刷新 + } + ctx.fireChannelWritabilityChanged(); + } + }); + + if (connectionListener != null) { + connectionListener.onConnected(serverId); + } + // 连接成功后立即刷新缓冲消息 + triggerFlush(); + } else { +// logger.error("服务端[{}]连接失败", serverId, channelFuture.cause()); + handleConnectionFailure(); + } + }); + } + + /** + * 处理连接失败 + */ + private void handleConnectionFailure() { + isConnected.set(false); + cleanupResources(); + scheduleReconnect(); + + if (connectionListener != null) { + connectionListener.onConnectionFailed(serverId); + } + } + + /** + * 处理连接断开 + */ + private void handleDisconnect() { + if (isConnected.getAndSet(false)) { +// logger.warn("服务端[{}]连接已断开,累计发送:{}条,接收:{}条", +// serverId, sendCounter.get(), receiveCounter.get()); + cleanupResources(); + scheduleReconnect(); + + if (connectionListener != null) { + connectionListener.onDisconnected(serverId); + } + } + } + + /** + * 调度重连任务 + */ + public void scheduleReconnect() { + if (reconnectFuture != null && !reconnectFuture.isDone()) { + try { + reconnectFuture.cancel(false); + } catch (Exception e) { +// logger.error("服务端[{}]取消重连任务失败", serverId, e); + } finally { + reconnectFuture = null; + } + } + + int attempt = reconnectAttempts.incrementAndGet(); + long delay = (long) (serverInfo.getReconnectInitialDelay() * + Math.pow(serverInfo.getReconnectMultiplier(), attempt - 1)); + delay = Math.min(delay, serverInfo.getReconnectMaxDelay()); + delay = Math.max(delay, 1000); + +// logger.info("服务端[{}]第{}次重连将在{}ms后执行", serverId, attempt, delay); + //大于10次重连直接移除 + if (attempt > 10) { + //调用方法 + connectionListener.oncloseConnected(serverId); + } + try { + reconnectFuture = scheduler.schedule(this::connect, delay, TimeUnit.MILLISECONDS); + } catch (Exception e) { +// logger.error("服务端[{}]调度重连任务失败", serverId, e); + scheduler.execute(this::connect); + } + } + + /** + * 发送十六进制字符串消息 + */ + public void sendHexMessage(String message, String channelId, Integer portNumber, + SendTcpContent.OperationType operationType, String protocolXml, String extraData) { + try { + byte[] data; + if (SendTcpContent.OperationType.REGISTER == operationType) { + data = message.getBytes(); + } else { + data = hexStringToByteArray(message); + } + sendMessage(data, channelId, portNumber, operationType, protocolXml, extraData, false); + } catch (IllegalArgumentException e) { + logger.error("服务端[{}]解析十六进制消息失败", serverId, e); + } + } + + /** + * 发送字节数组消息(支持优先级) + */ + public void sendMessage(byte[] data, String channelId, Integer portNumber, + SendTcpContent.OperationType operationType, String protocolXml, + String extraData, boolean highPriority) { + if (data == null || data.length == 0) { + logger.error("服务端[{}]发送消息失败:字节数组为空", serverId); + return; + } + if (operationType == null) { + logger.error("服务端[{}]发送消息失败:操作类型不能为空", serverId); + return; + } + + SendTcpContent message = new SendTcpContent(data, channelId, operationType, + portNumber, protocolXml, extraData); + sendMessage(message, highPriority); + } + + /** + * 核心消息发送逻辑(高并发优化) + */ + public void sendMessage(SendTcpContent message, boolean highPriority) { + // 快速失败检查 + if (!isConnected.get()) { + logger.warn("服务端[{}]发送消息失败:未连接,缓冲消息", serverId); + bufferMessage(message, highPriority); + scheduleReconnect(); + return; + } + if (channel == null || !channel.isActive()) { + logger.error("服务端[{}]发送消息失败:通道无效,缓冲消息", serverId); + isConnected.set(false); + bufferMessage(message, highPriority); + scheduleReconnect(); + return; + } + + // 6. 非IO线程发送消息:提交到worker池避免阻塞调用线程 + sendWorkerPool.submit(() -> { + try { + // 通道可写时直接发送,否则缓冲 + if (channel.isWritable()) { + doSend(message); + } else { + bufferMessage(message, highPriority); + triggerFlush(); // 尝试触发一次刷新 + } + } catch (Exception e) { + logger.error("服务端[{}]发送任务执行失败", serverId, e); + bufferMessage(message, highPriority); + } + }); + } + + /** + * 实际发送消息(Netty IO线程中执行) + */ + private void doSend(SendTcpContent message) { + channel.writeAndFlush(message).addListener((ChannelFutureListener) future -> { + if (future.isSuccess()) { +// logger.info("服务端[{}]发送消息成功:{}", serverId, message); + // 发送成功后检查是否还有缓冲消息 + if (channel.isWritable()) { + triggerFlush(); + } + } else { + logger.error("服务端[{}]发送消息失败", serverId, future.cause()); + if (isConnected.get()) { + bufferMessage(message, true); // 失败消息提升为高优先级 + triggerFlush(); + } + } + }); + } + + /** + * 缓冲消息(分级存储) + */ + private void bufferMessage(SendTcpContent message, boolean highPriority) { + // 控制总缓冲大小,防止内存溢出 + if (totalBuffered.get() >= MAX_BUFFER_SIZE) { + // 缓冲区满时优先保留高优先级消息 + if (highPriority && !normalBuffer.isEmpty()) { + normalBuffer.poll(); // 移除一条普通消息 + totalBuffered.decrementAndGet(); + } else if (!highPriority) { + logger.warn("服务端[{}]普通消息缓冲区已满({}条),丢弃消息", + serverId, MAX_BUFFER_SIZE); + return; + } else { + logger.warn("服务端[{}]高优先级消息缓冲区已满({}条),丢弃消息", + serverId, MAX_BUFFER_SIZE); + return; + } + } + + // 按优先级加入不同缓冲区 + if (highPriority) { + highPriorityBuffer.offer(message); + } else { + normalBuffer.offer(message); + } + totalBuffered.incrementAndGet(); + } + + /** + * 触发缓冲消息刷新(防止并发重复执行) + */ + private void triggerFlush() { + if (isFlushing.compareAndSet(false, true)) { + // 提交到IO线程执行,避免线程安全问题 + if (channel != null && channel.eventLoop().inEventLoop()) { + flushBufferedMessages(); + } else if (channel != null) { + channel.eventLoop().execute(this::flushBufferedMessages); + } else { + isFlushing.set(false); + } + } + } + + /** + * 刷新缓冲消息(批量处理) + */ + private void flushBufferedMessages() { + try { + if (!isConnected.get() || channel == null || !channel.isWritable()) { + return; + } + + // 7. 批量发送:每次最多发送30条,避免长时间阻塞IO线程 + int maxBatch = 30; + int sent = 0; + + // 先发送高优先级消息 + SendTcpContent msg; + while ((msg = highPriorityBuffer.poll()) != null && sent < maxBatch) { + doSend(msg); + totalBuffered.decrementAndGet(); + sent++; + } + + // 再发送普通消息 + while ((msg = normalBuffer.poll()) != null && sent < maxBatch) { + doSend(msg); + totalBuffered.decrementAndGet(); + sent++; + } + + // 如果还有消息未发送,继续触发下一轮刷新 + if ((!highPriorityBuffer.isEmpty() || !normalBuffer.isEmpty()) && channel.isWritable()) { + channel.eventLoop().schedule(this::triggerFlush, 10, TimeUnit.MILLISECONDS); + } + } finally { + isFlushing.set(false); // 释放锁 + } + } + + /** + * 主动断开连接 + */ + public void disconnect() { +// logger.info("主动断开服务端[{}]连接,累计发送:{}条,接收:{}条", +// serverId, sendCounter.get(), receiveCounter.get()); + + // 取消重连任务 + if (reconnectFuture != null && !reconnectFuture.isDone()) { + reconnectFuture.cancel(false); + reconnectFuture = null; + } + + // 关闭通道 + if (channel != null && channel.isOpen()) { + channel.close().addListener(future -> { + if (future.isSuccess()) { +// logger.info("服务端[{}]通道关闭成功", serverId); + } else { +// logger.error("服务端[{}]通道关闭失败", serverId, future.cause()); + } + }); + } + + // 清理资源 + cleanupResources(); + isConnected.set(false); + isConnecting.set(false); + // 清空缓冲区 + highPriorityBuffer.clear(); + normalBuffer.clear(); + totalBuffered.set(0); + } + + /** + * 清理Netty资源 + */ + private void cleanupResources() { + if (group != null && !group.isShuttingDown()) { + group.shutdownGracefully(1000, 5000, TimeUnit.MILLISECONDS) + .addListener(future -> { + if (future.isSuccess()) { +// logger.info("服务端[{}]EventLoopGroup关闭成功", serverId); + } else { +// logger.error("服务端[{}]EventLoopGroup关闭失败", serverId, future.cause()); + } + }); + } + group = null; + channel = null; + } + + /** + * 处理接收到的消息(批量+异步) + */ + public void handleReceivedMessage(ReceiveTcpContent msg) { + if (msg == null) { +// logger.warn("服务端[{}]收到空消息", serverId); + return; + } + + // 8. 接收消息统计 + long count = receiveCounter.incrementAndGet(); + if (count % 1000 == 0) { +// logger.info("服务端[{}]累计接收消息:{}条", serverId, count); + } + + // 交给监听器处理(已在业务线程池执行) + if (connectionListener != null) { + connectionListener.onMessageReceived(serverId, msg); + } + } + + /** + * 处理通道异常 + */ + public void handleException(Throwable cause) { + logger.error("服务端[{}]通道发生异常", serverId, cause); + if (isConnected.get()) { + scheduleReconnect(); + } + } + + /* Getter方法 */ + public String getServerId() { + return serverId; + } + + public boolean isConnected() { + return isConnected.get(); + } + + public ServerInfo getServerInfo() { + return serverInfo; + } + + public int getBufferedMessageCount() { + return totalBuffered.get(); + } + + /** + * Netty客户端通道处理器(高并发优化) + */ + public static class ClientHandler extends SimpleChannelInboundHandler { + private final ServerConnection connection; + // 9. 批量接收缓冲区(减少业务线程切换) + private final ConcurrentLinkedQueue batchBuffer = new ConcurrentLinkedQueue<>(); + private final ScheduledFuture batchProcessor; + + public ClientHandler(ServerConnection connection) { + this.connection = connection; + // 定时批量处理接收的消息(每10ms或满100条时触发) + this.batchProcessor = businessExecutor.scheduleWithFixedDelay( + this::processBatchMessages, 10, 10, TimeUnit.MILLISECONDS); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, ReceiveTcpContent msg) { + // 先加入批量缓冲区 + batchBuffer.offer(msg); + // 满100条时立即处理,避免延迟 + if (batchBuffer.size() >= 100) { + processBatchMessages(); + } + } + + /** + * 批量处理接收的消息,减少线程切换开销 + */ + private void processBatchMessages() { + int processed = 0; + ReceiveTcpContent msg; + while ((msg = batchBuffer.poll()) != null) { + connection.handleReceivedMessage(msg); + processed++; + } + if (processed > 0) { +// logger.trace("服务端[{}]批量处理消息:{}条", connection.serverId, processed); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { +// logger.error("通道异常,即将关闭", cause); + connection.handleException(cause); + ctx.close(); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { +// logger.warn("通道已失效(服务端断开连接)"); + batchProcessor.cancel(false); // 停止批量处理器 + ctx.close(); + connection.handleDisconnect(); + } + } + + /** + * 十六进制字符串转字节数组 + */ + public static byte[] hexStringToByteArray(String hexString) { + if (StringUtils.isBlank(hexString)) { + return new byte[0]; + } + hexString = hexString.replaceAll("\\s", ""); + if (hexString.length() % 2 != 0) { + throw new IllegalArgumentException("十六进制字符串长度必须为偶数: " + hexString); + } + + int len = hexString.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + int high = Character.digit(hexString.charAt(i), 16); + int low = Character.digit(hexString.charAt(i + 1), 16); + if (high == -1 || low == -1) { + throw new IllegalArgumentException("无效的十六进制字符: " + hexString.substring(i, i + 2)); + } + data[i / 2] = (byte) ((high << 4) + low); + } + return data; + } + + /** + * 字节数组转十六进制字符串 + */ + public static String bytesToHex(byte[] bytes) { + if (bytes == null || bytes.length == 0) { + return ""; + } + StringBuilder sb = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + sb.append(String.format("%02X", b)); + } + return sb.toString(); + } + + /** + * 连接事件监听接口 + */ + public interface ConnectionListener { + void onConnected(String serverId); + void onDisconnected(String serverId); + void onConnectionFailed(String serverId); + void onMessageReceived(String serverId, ReceiveTcpContent message); + + void oncloseConnected(String serverId); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/entity/FrameDecoder.java b/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/entity/FrameDecoder.java new file mode 100644 index 0000000..d0166a3 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/entity/FrameDecoder.java @@ -0,0 +1,56 @@ +package com.cbsd.universaltestsoftware_client.nettyclient.entity; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; + +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public class FrameDecoder extends ByteToMessageDecoder { + + // 帧头固定为4字节:0xCAFEBABE(十六进制) + private static final int FRAME_HEADER = 0xCAFEBABE; + private static final int HEADER_LENGTH = 4; // 帧头字节数 + private static final int LENGTH_FIELD_LENGTH = 4; // 长度字段字节数 + // JSON序列化工具 + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + // 1. 确保有足够的字节验证帧头(至少帧头长度+长度字段长度) + if (in.readableBytes() < HEADER_LENGTH + LENGTH_FIELD_LENGTH) { + return; // 字节不足,等待更多数据 + } + + // 2. 标记当前读指针位置,方便验证失败时重置 + in.markReaderIndex(); + + // 3. 读取帧头并验证 + int header = in.readInt(); // 读取4字节帧头 + if (header != FRAME_HEADER) { + // 帧头不匹配,重置读指针并跳过一个字节,然后重新尝试 + in.resetReaderIndex(); + in.readByte(); // 跳过一个字节 + // 递归调用decode继续处理剩余数据 + decode(ctx, in, out); + return; + } + + // 4. 读取长度字段(内容的字节数) + int contentLength = in.readInt(); + + // 5. 确保有足够的字节读取内容 + if (in.readableBytes() < contentLength) { + in.resetReaderIndex(); // 重置读指针,等待更多数据 + return; + } + + byte[] contentBytes = new byte[contentLength]; // 从长度字段获取contentLength + in.readBytes(contentBytes); + ReceiveTcpContent receiveTcpContent = objectMapper.readValue(contentBytes, ReceiveTcpContent.class); // 反序列化 + + out.add(receiveTcpContent); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/entity/FrameEncoder.java b/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/entity/FrameEncoder.java new file mode 100644 index 0000000..497c371 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/entity/FrameEncoder.java @@ -0,0 +1,27 @@ +package com.cbsd.universaltestsoftware_client.nettyclient.entity; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; + +public class FrameEncoder extends MessageToByteEncoder { + + private static final int FRAME_HEADER = 0xCAFEBABE; + // JSON序列化工具 + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + protected void encode(ChannelHandlerContext ctx, SendTcpContent content, ByteBuf out) throws JsonProcessingException { + byte[] contentBytes = objectMapper.writeValueAsBytes(content); + int contentLength = contentBytes.length; + // 1. 写入帧头(4字节) + out.writeInt(FRAME_HEADER); + // 2. 写入长度字段(4字节,内容的字节数) + out.writeInt(contentLength); + // 3. 写入内容 + out.writeBytes(contentBytes); // 写入序列化后的内容 + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/entity/NoticeMessage.java b/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/entity/NoticeMessage.java new file mode 100644 index 0000000..b57a19a --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/entity/NoticeMessage.java @@ -0,0 +1,12 @@ +package com.cbsd.universaltestsoftware_client.nettyclient.entity; + +import lombok.Data; + +@Data +public class NoticeMessage { + //debug,info,warn,error + private String messageType; + private String message; + + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/entity/ReceiveTcpContent.java b/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/entity/ReceiveTcpContent.java new file mode 100644 index 0000000..9ead836 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/entity/ReceiveTcpContent.java @@ -0,0 +1,93 @@ +package com.cbsd.universaltestsoftware_client.nettyclient.entity; + +import com.baomidou.mybatisplus.generator.IFill; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * TCP传递消息体 + */ +@Data +public class ReceiveTcpContent { + + public enum OperationType{ + DATA(0),NOTICE(1); + + OperationType(int operationType) { + this.operationType = operationType; + } + private final int operationType; + public int getOperationType() { + return operationType; + } + + } + + /** + * 为空正常数据,不为空数据回访数据 + */ + private String dataBoardId; + + private byte[] bytesContent; + // 消息时间戳 + private long timestamp; + + /** + * 通道ID + */ + private String channelId; + + /** + * 额外数据 + */ + private String extraData; + + /** + * 端口号 + */ + private int portNumber; + + /** + * 操作类型 数据=0,寄存器=1 + */ + private OperationType operationType; + + // /** + // * 驱动类型 + // */ + // private int driverType; + // /** + // * 同一驱动类型下的不同设备类型(型号) + // */ + // private String deviceType; + /** + * 索引 编号 + */ + + + @Override + public String toString() { + + StringBuilder sb = new StringBuilder(); + sb.append("TcpContent{"); + sb.append(" channelId=").append(channelId); + sb.append(", operationType=").append(operationType.getOperationType()); + sb.append(", dataBoardId=").append(dataBoardId); + sb.append(", portNumber=").append(portNumber).append(","); + if (bytesContent != null){ + for (byte elem : bytesContent) { + sb.append("bytesContent=").append(bytesToHex(elem)); + } + } + sb.append(", timestamp=").append(timestamp); + sb.append('}'); + return sb.toString(); + } + + // 将字节数组转换为十六进制字符串 + private String bytesToHex(byte b) { + return String.format("%02X ", b).trim(); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/entity/SendTcpContent.java b/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/entity/SendTcpContent.java new file mode 100644 index 0000000..03f623e --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/nettyclient/entity/SendTcpContent.java @@ -0,0 +1,105 @@ +package com.cbsd.universaltestsoftware_client.nettyclient.entity; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * TCP传递消息体 + */ +@Data +public class SendTcpContent { + + public enum OperationType{ + DATA(0),REGISTER(1); + + OperationType(int operationType) { + this.operationType = operationType; + } + private final int operationType; + public int getOperationType() { + return operationType; + } + + } + private byte[] bytesContent; + + private String protocolXml; + // 消息时间戳 + private long timestamp; + + /** + * 通道ID + */ + private String channelId; + + /** + * 端口号 + */ + private int portNumber; + + /** + * 操作类型 数据=0,寄存器=1 + */ + private OperationType operationType; + + private String extraData; + + // /** + // * 驱动类型 + // */ + // private int driverType; + // /** + // * 同一驱动类型下的不同设备类型(型号) + // */ + // private String deviceType; + /** + * 索引 编号 + */ + // private int index; + public SendTcpContent() { + + } + + public SendTcpContent(byte[] bytesContent, String channelId, OperationType operationType, int portNumber,String protocolXml,String extraData) { + this.bytesContent = bytesContent; + this.channelId = channelId; + this.operationType = operationType; + this.portNumber = portNumber; + this.timestamp = System.currentTimeMillis(); + this.protocolXml = protocolXml; + this.extraData = extraData; + } + + + + + + @Override + public String toString() { + + StringBuilder sb = new StringBuilder(); + sb.append("TcpContent{"); + sb.append(" channelId=").append(channelId); + sb.append(", operationType=").append(operationType); + sb.append(", portNumber=").append(portNumber); + sb.append("bytesContent=").append(bytesToHex(bytesContent)); + sb.append(", protocolXml=").append(protocolXml); + sb.append(", timestamp=").append(timestamp); + sb.append(", extraData=").append(extraData); + sb.append('}'); + return sb.toString(); + } + + // 将字节数组转换为十六进制字符串 + private String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02X ", b)); + } + return sb.toString().trim(); + } + + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/parser/ParserManager.java b/src/main/java/com/cbsd/universaltestsoftware_client/parser/ParserManager.java new file mode 100644 index 0000000..1b2cc3b --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/parser/ParserManager.java @@ -0,0 +1,161 @@ +package com.cbsd.universaltestsoftware_client.parser; + +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.cbsd.universaltestsoftware_client.entity.Parser; +import com.cbsd.universaltestsoftware_client.entity.ParserProtocol; +import com.cbsd.universaltestsoftware_client.entity.Protocol; +import com.cbsd.universaltestsoftware_client.mapper.ParserMapper; +import com.cbsd.universaltestsoftware_client.mapper.ParserProtocolMapper; +import com.cbsd.universaltestsoftware_client.mapper.ProtocolMapper; +import com.cbsd.universaltestsoftware_client.parser.model.Filed; +import com.cbsd.universaltestsoftware_client.parser.model.Meta; +import com.cbsd.universaltestsoftware_client.parser.model.ProtocolXml; +import com.cbsd.universaltestsoftware_client.util.XmlToObjectConverter; +import com.cbsd.universaltestsoftware_client.websocket.NoticeWebSocketHandler; +import lombok.Getter; +import lombok.Setter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Component +public class ParserManager { + + @Resource + private ParserMapper parserMapper; + + @Resource + private ParserProtocolMapper parserProtocolMapper; + + @Resource + private ProtocolMapper protocolMapper; + + @Setter + private OnParserChangeListener onParserChangeListener; + + @Getter + @Value("${cbsd.sampling-interval-ms:1000}") + private long samplingIntervalMs; + + @Resource + private NoticeWebSocketHandler noticeWebSocketHandler; + + /** + * 解析器map + */ + private final Map parserMap = new HashMap<>(); + /** + * 解析器协议map + */ + private final Map> parserProtocolMap = new HashMap<>(); + /** + * 协议map + */ + private final Map protocolMap = new HashMap<>(); + + @PostConstruct + public void initParser() { + List parserList = parserMapper.selectList(new LambdaQueryWrapper().eq(Parser::getIsDelete, 0).orderByAsc(Parser::getParserSort)); + processParserList(parserList); + } + + private void processParserList(List parserList) { + parserProtocolMap.clear(); + parserMap.clear(); + protocolMap.clear(); + if (parserList != null && !parserList.isEmpty()) { + for (Parser parser : parserList) { + parserMap.put(parser.getParserId(), parser); + //解析器绑定的协议列表 + List parserProtocolList = parserProtocolMapper.selectList(new LambdaQueryWrapper() + .eq(ParserProtocol::getParserId, parser.getParserId())); + if (parserProtocolList != null && !parserProtocolList.isEmpty()) { + List protocolIdList = parserProtocolList.stream().map(ParserProtocol::getProtocolId).collect(Collectors.toList()); + List protocolList = protocolMapper.selectList(new LambdaQueryWrapper().in(Protocol::getProtocolId, protocolIdList)); + parserProtocolMap.put(parser.getParserId(), protocolList); + for (Protocol protocol : protocolList) { + try { + ProtocolXml protocolXml = XmlToObjectConverter.convertXmlToObject(protocol.getProtocolContent(), ProtocolXml.class); + Meta meta = protocolXml.getMeta(); + if (meta != null) { + List filedList = protocolXml.getFileds(); + filedList.sort(Comparator.comparingInt(Filed::getSnInt)); + Map filedMap = new HashMap<>(); + int totalBitslen = 0; + for (Filed filed : filedList) { + filedMap.put(filed.getType(), filed); + totalBitslen += filed.getBitsLen(); + } + meta.setDataLength(totalBitslen / 8); + protocolXml.setFileds(filedList); + protocolXml.setMeta(meta); + + protocol.setFiledMap(filedMap); + } + protocol.setProtocolXml(protocolXml); + } catch (Exception ignored) { + + } + protocolMap.put(protocol.getProtocolId(), protocol); + } + } + } + } + + if (onParserChangeListener != null) { + onParserChangeListener.onParserChanged(); + } + } + + /** + * 更新通道解析器 + * + */ + public void refresh() { + //先将该通道移除 + initParser(); + Map msgMap = new HashMap<>(); + msgMap.put("messageType", "PROTOCOL_CHANGED"); + msgMap.put("message", "协议改变, 刷新遥测及数据回放数据看板"); + noticeWebSocketHandler.broadcast(JSON.toJSONString(msgMap)); + } + + /** + * 获取解析器协议列表 + * + * @param parserId 解析器id + */ + public List getParserProtocolList(String parserId) { + return parserProtocolMap.get(parserId); + } + + /** + * 获取解析器 + * + * @param parserId 解析器id + */ + public Parser getParser(String parserId) { + return parserMap.get(parserId); + } + + /** + * 获取协议 + * + * @param protocolId 协议id + */ + public Protocol getProtocol(String protocolId) { + return protocolMap.get(protocolId); + } + + public interface OnParserChangeListener { + void onParserChanged(); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/parser/ParserMessageQueueConsumer.java b/src/main/java/com/cbsd/universaltestsoftware_client/parser/ParserMessageQueueConsumer.java new file mode 100644 index 0000000..bf3cdff --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/parser/ParserMessageQueueConsumer.java @@ -0,0 +1,125 @@ +package com.cbsd.universaltestsoftware_client.parser; + +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.cbsd.universaltestsoftware_client.entity.ChannelData; +import com.cbsd.universaltestsoftware_client.entity.ChannelDataHit; +import com.cbsd.universaltestsoftware_client.mapper.ChannelDataHitMapper; +import com.cbsd.universaltestsoftware_client.parser.model.ParserMessage; +import com.cbsd.universaltestsoftware_client.service.ChannelDataService; +import com.cbsd.universaltestsoftware_client.util.DateUtils; +import com.cbsd.universaltestsoftware_client.util.UUIDUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +@Component +public class ParserMessageQueueConsumer { + + @Resource + private ChannelDataService channelDataService; + + @Resource + private ChannelDataHitMapper channelDataHitMapper; + + /** + * 已经处理的消息,避免重复处理 + */ + private final Set processedMessageIds = Collections.synchronizedSet(new HashSet<>()); + + @PostConstruct + private void start() { + new Thread(() -> { + while (true) { + processData(); + } + }).start(); + } + + private void processData() { + try { + ParserMessage message = ParserMessageQueueManager.takeMessage(); + if (!tryProcessMessage(message.getMessageId())) { + //已经处理过,直接跳过 + return; + } +// channelDataService.checkDataLimitToClear(); + + String channelId = message.getChannelId(); + if (StringUtils.isNotBlank(channelId)) { + // 始终原子更新hitCount(无论是否采样,每帧都计数) + updateHitCountAtomically(channelId, message.getProtocolId()); + + // 仅对采样帧保存ChannelData字段详情 + if (message.isSampled() && message.getFiledParseDataList() != null + && !message.getFiledParseDataList().isEmpty()) { + ChannelData channelData = new ChannelData(); + channelData.setDataId(message.getMessageId()); + channelData.setChannelId(channelId); + channelData.setData(JSON.toJSONString(message.getFiledParseDataList())); + channelData.setProtocolId(message.getProtocolId()); + channelData.setProtocolContent(message.getProtocolContent()); + channelData.setTime(DateUtils.timestampToDateString(message.getTimestamp(), "yyyy-MM-dd HH:mm:ss.SSS")); + try { + channelDataService.save(channelData); + } catch (Exception ignored) { + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 尝试处理消息,校验消息是否处理过 + * + * @param messageId + * @return + */ + private boolean tryProcessMessage(String messageId) { + // 超过1小时的记录清理,避免内存溢出 + if (processedMessageIds.size() > 10000) { + processedMessageIds.clear(); // 可优化为按时间清理 + } + return processedMessageIds.add(messageId); // add返回false表示已处理过 + } + + /** + * 原子更新hitCount,解决并发计数问题 + */ + private void updateHitCountAtomically(String channelId, String protocolId) { + // 第一步:尝试更新计数(原子操作) + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() + .eq(ChannelDataHit::getChannelId, channelId) + .eq(ChannelDataHit::getProtocolId, protocolId) + // 数据库层面的原子累加,避免丢失更新 + .setSql("hit_count = hit_count + 1") + .set(ChannelDataHit::getUpdateTime, DateUtils.dateToString()); + + int updateCount = channelDataHitMapper.update(null, updateWrapper); + + // 第二步:如果更新行数为0,说明记录不存在,执行插入 + if (updateCount == 0) { + ChannelDataHit dataHit = new ChannelDataHit(); + dataHit.setHitId(UUIDUtils.getUUID()); + dataHit.setChannelId(channelId); + dataHit.setProtocolId(protocolId); + dataHit.setHitCount(1); + dataHit.setUpdateTime(DateUtils.dateToString()); + + // 插入时加重试,避免并发插入冲突 + try { + channelDataHitMapper.insert(dataHit); + } catch (Exception e) { + // 插入失败(唯一键冲突),再次执行更新 + channelDataHitMapper.update(null, updateWrapper); + } + } + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/parser/ParserMessageQueueManager.java b/src/main/java/com/cbsd/universaltestsoftware_client/parser/ParserMessageQueueManager.java new file mode 100644 index 0000000..2767b17 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/parser/ParserMessageQueueManager.java @@ -0,0 +1,86 @@ +package com.cbsd.universaltestsoftware_client.parser; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.serializer.ValueFilter; +import com.cbsd.universaltestsoftware_client.parser.model.ParserMessage; +import com.cbsd.universaltestsoftware_client.util.DataUtils; +import com.cbsd.universaltestsoftware_client.websocket.DataPlayBackWebSocketHandler; +import com.cbsd.universaltestsoftware_client.websocket.DataWebSocketHandler; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +@Component +public class ParserMessageQueueManager { + + // 线程安全的消息队列 + private static final BlockingQueue messageQueue = new LinkedBlockingQueue<>(); + + // 用于接收 Spring 注入的实例 (非 static) + @Resource + private DataWebSocketHandler dataWebSocketHandlerInstance; + @Resource + private DataPlayBackWebSocketHandler dataPlayBackWebSocketHandler; + + // 用于 static 方法访问的 static 字段 + private static DataWebSocketHandler staticDataWebSocketHandler; + + private static DataPlayBackWebSocketHandler staticDataPlayBackWebSocketHandler; + + // 在 Bean 初始化完成后,将注入的实例赋值给 static 字段 + @PostConstruct + public void initStaticHandler() { + staticDataWebSocketHandler = this.dataWebSocketHandlerInstance; + staticDataPlayBackWebSocketHandler = this.dataPlayBackWebSocketHandler; + } + + // 添加消息到队列 + public static void pushMessage(ParserMessage message) { + if (StringUtils.isBlank(message.getDataBoardId())){ + try { + messageQueue.put(message); + } catch (InterruptedException e) { + e.printStackTrace(); + } + // 使用 static 字段调用方法 + if (staticDataWebSocketHandler != null) { + //推送广播给前端,对象转化成字符串对象 + staticDataWebSocketHandler.broadcast(JSON.toJSONString(message)); +// staticDataWebSocketHandler.broadcast(JSON.toJSONString(message, (ValueFilter) (object, name, value) -> { +// if ("sourceValue".equalsIgnoreCase(name)){ +// return DataUtils.byteToHex((byte[])value); +// } +// return value; +// })); + } else { + // 可能发生在 Spring 上下文尚未完全初始化或配置错误时 + System.err.println("Warning: DataWebSocketHandler is not initialized!"); + } + } else { + if (staticDataPlayBackWebSocketHandler != null) { + staticDataPlayBackWebSocketHandler.broadcast(JSON.toJSONString(message)); +// staticDataPlayBackWebSocketHandler.broadcast(JSON.toJSONString(message, (ValueFilter) (object, name, value) -> { +// if ("sourceValue".equalsIgnoreCase(name)){ +// return DataUtils.byteToHex((byte[])value); +// } +// return value; +// })); + } else { + System.err.println("Warning: DataPlayBackWebSocketHandler is not initialized!"); + } + } + } + + // 从队列获取消息 + public static ParserMessage takeMessage() throws InterruptedException { + return messageQueue.take(); + } + + public static int getParserMessageCount() { + return messageQueue.size(); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/parser/ParserThread.java b/src/main/java/com/cbsd/universaltestsoftware_client/parser/ParserThread.java new file mode 100644 index 0000000..bca1b47 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/parser/ParserThread.java @@ -0,0 +1,184 @@ +package com.cbsd.universaltestsoftware_client.parser; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.cbsd.universaltestsoftware_client.entity.Parser; +import com.cbsd.universaltestsoftware_client.entity.Protocol; +import com.cbsd.universaltestsoftware_client.parser.model.OriginalData; +import com.cbsd.universaltestsoftware_client.service.ParserService; +import com.cbsd.universaltestsoftware_client.util.DataUtils; + +import java.util.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.stream.Collectors; + +public class ParserThread extends Thread implements ParserManager.OnParserChangeListener { + /** + * 解析器service + */ + private final ParserService parserService; + /** + * 协议管理器 + */ + private final ParserManager parserManager; + /** + * 设备通道id + */ + private final String currentChannelId; + /** + * 前端数据回放看板Id, 为空正常数据,不为空数据回访数据 + */ + private final String dataBoardId; + /** + * 缓存数据 + */ + private final BlockingQueue dataQueue; + /** + * 解析器序号 + */ + private final int parserSort; + /** + * 当前解析器 + */ + private Parser currentParser; + + // TODO: 2025/9/26 更改为 BlockingQueue + private final List> protocolQueueList = new ArrayList<>(); + /** + * 协议线程map + */ + private final Map protocolThreadMap = new HashMap<>(); + + /** + * 构造函数 + * + * @param currentChannelId 设备通道id + * @param dataBoardId 回访看板Id + * @param dataQueue 数据队列 + * @param parserSort 解析器排序 + * @param parserService 解析器service + * @param parserManager 解析器管理器 + */ + public ParserThread(String currentChannelId, String dataBoardId, BlockingQueue dataQueue, int parserSort, ParserService parserService, ParserManager parserManager) { + this.currentChannelId = currentChannelId; + this.dataBoardId = dataBoardId; + this.dataQueue = dataQueue; + this.parserSort = parserSort; + this.parserService = parserService; + this.parserManager = parserManager; + + this.parserManager.setOnParserChangeListener(this); + } + + @Override + public void run() { + while (true) { + try { + pollToByteBuffer(); + } catch (Exception ignored) { + + } + } + } + + @Override + public void onParserChanged() { + checkProtocol(); + } + + /** + * 拉取队列数据到byteBuffer + */ + private void pollToByteBuffer() { + try { + OriginalData readData = dataQueue.take(); + for (Queue queue : protocolQueueList) { + queue.offer(readData); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 初始化 + */ + public boolean init() { + currentParser = parserService.getOne(new LambdaQueryWrapper() + .eq(Parser::getChannelId, currentChannelId) + .eq(Parser::getParserSort, parserSort), false); + if (currentParser != null) { + //解析器下的协议 + List protocolList = parserManager.getParserProtocolList(currentParser.getParserId()); + if (protocolList != null) { + for (Protocol protocol : protocolList) { + BlockingQueue queue = new LinkedBlockingQueue<>(); + protocolQueueList.add(queue); + ProtocolThread protocolThread = new ProtocolThread(currentChannelId, dataBoardId, protocol.getProtocolId(), queue, parserSort, parserService, parserManager); + protocolThread.start(); + protocolThreadMap.put(protocol.getProtocolId(), protocolThread); + } + return true; + } + } + return false; + + } + + /** + * 检查协议 + */ + public void checkProtocol() { + if (currentParser != null) { + List protocolList = parserManager.getParserProtocolList(currentParser.getParserId()); + if (protocolList != null) { + //先判断运行中的协议是否在解析器协议列表中, 不存在的则停止 + List protocolIdList = protocolList.stream().map(Protocol::getProtocolId).collect(Collectors.toList()); + List keyList = new ArrayList<>(protocolThreadMap.keySet()); + for (String runningProtocolId : keyList) { + if (!protocolIdList.contains(runningProtocolId)) { + //该解析器的协议列表已经不存在该协议, 停止 + ProtocolThread protocolThread = protocolThreadMap.get(runningProtocolId); + if (protocolThread != null) { + protocolThread.stopParse(); + protocolThreadMap.remove(runningProtocolId); + } + } + } + //添加新的协议 + for (Protocol protocol : protocolList) { + if (protocolThreadMap.get(protocol.getProtocolId()) == null) { + BlockingQueue queue = new LinkedBlockingQueue<>(); + protocolQueueList.add(queue); + ProtocolThread protocolThread = new ProtocolThread(currentChannelId, dataBoardId, protocol.getProtocolId(), queue, parserSort, parserService, parserManager); + protocolThread.start(); + protocolThreadMap.put(protocol.getProtocolId(), protocolThread); + } + } + } else { + //停止所有 + if (!protocolThreadMap.isEmpty()) { + for (ProtocolThread protocolThread : protocolThreadMap.values()) { + protocolThread.stopParse(); + } + protocolThreadMap.clear(); + } + } + } + } + + /** + * 移除原始数据 + * @param dataBoardId 数据看板id + */ + public void removeOriginalData(String dataBoardId){ + if (dataQueue != null){ + dataQueue.removeIf(originalData -> originalData.getDataBoardId().equals(dataBoardId)); + } + + for (ProtocolThread protocolThread : protocolThreadMap.values()){ + protocolThread.removeOriginalData(dataBoardId); + } + } + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/parser/ProtocolParserHelper.java b/src/main/java/com/cbsd/universaltestsoftware_client/parser/ProtocolParserHelper.java new file mode 100644 index 0000000..c9f0e2e --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/parser/ProtocolParserHelper.java @@ -0,0 +1,48 @@ +package com.cbsd.universaltestsoftware_client.parser; + +import com.cbsd.universaltestsoftware_client.parser.model.OriginalData; +import com.cbsd.universaltestsoftware_client.service.ParserService; +import lombok.Data; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +@Data +public class ProtocolParserHelper { + /** + * 缓存数据 + */ + private final BlockingQueue dataQueue = new LinkedBlockingQueue<>(); + + private ParserThread parserThread; + + private String uniqueKey; + + public ProtocolParserHelper(String uniqueKey, String channelId, String dataBoardId, int parserSort, ParserService parserService, ParserManager parserManager) { + this.uniqueKey = uniqueKey; + parserThread = new ParserThread(channelId, dataBoardId, dataQueue, parserSort, parserService, parserManager); + if (parserThread.init()) { + parserThread.start(); + } + } + + public void putData(OriginalData data) { + try { + dataQueue.put(data); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + /** + * 移除原始数据 + * + * @param dataBoardId 数据看板id + */ + public void removeOriginalData(String dataBoardId) { + dataQueue.removeIf(originalData -> originalData.getDataBoardId().equals(dataBoardId)); + if (parserThread != null) { + parserThread.removeOriginalData(dataBoardId); + } + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/parser/ProtocolThread.java b/src/main/java/com/cbsd/universaltestsoftware_client/parser/ProtocolThread.java new file mode 100644 index 0000000..8c5cb52 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/parser/ProtocolThread.java @@ -0,0 +1,529 @@ +package com.cbsd.universaltestsoftware_client.parser; + +import com.cbsd.client.parse.IParseDataInterface; +import com.cbsd.client.verification.IVerificationInterface; +import com.cbsd.client.verification.VerifyUtils; +import com.cbsd.universaltestsoftware_client.entity.Protocol; +import com.cbsd.universaltestsoftware_client.parser.enums.ProtocolEnum; +import com.cbsd.universaltestsoftware_client.parser.model.*; +import com.cbsd.universaltestsoftware_client.script.ScriptManager; +import com.cbsd.universaltestsoftware_client.service.ParserService; +import com.cbsd.universaltestsoftware_client.util.DataUtils; +import com.cbsd.universaltestsoftware_client.util.UUIDUtils; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +public class ProtocolThread extends Thread { + /** + * 解析器service + */ + private final ParserService parserService; + /** + * 协议管理器 + */ + private final ParserManager parserManager; + /** + * 当前通道id + */ + private final String currentChannelId; + /** + * 数据回放看板Id, 为空正常数据, 否则为回放数据 + */ + private final String dataBoardId; + /** + * 解析器序号 + */ + private final int parserSort; + /** + * /** + * 协议id + */ + private final String protocolId; + /** + * 原始数据队列 + */ + private final BlockingQueue dataQueue; + /** + * 处理数据 + */ + private final int BYTE_BUF_INITIAL_CAPACITY = 128; + private final ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + private ByteBuf byteBuf = allocator.buffer(BYTE_BUF_INITIAL_CAPACITY); + + /** + * 当前正在解析的原始数据 + */ + private OriginalData currentParseOriginalData; + + private boolean isStop = false; + + private final long samplingIntervalMs; + private long lastSampledTimestamp = 0; + + /** + * 构造函数 + * + * @param channelId 通道id + * @param dataBoardId 数据回放看板Id + * @param protocolId 协议id + * @param dataQueue 数据队列 + * @param parserSort 解析器序号 + * @param parserService 解析器service + * @param parserManager 解析器管理器 + */ + public ProtocolThread(String channelId, String dataBoardId, String protocolId, BlockingQueue dataQueue, int parserSort, ParserService parserService, ParserManager parserManager) { + this.currentChannelId = channelId; + this.dataBoardId = dataBoardId; + this.protocolId = protocolId; + this.dataQueue = dataQueue; + this.parserSort = parserSort; + this.parserService = parserService; + this.parserManager = parserManager; + this.samplingIntervalMs = parserManager.getSamplingIntervalMs(); + setName(this.protocolId); + } + + @Override + public void run() { + super.run(); + while (true) { + try { + processData(); + } catch (Exception ignored) { + + } + + } + } + + /** + * 停止解析 + */ + public void stopParse() { + if (!isStop) { + isStop = true; + } + try { + join(1000); + interrupt(); + } catch (InterruptedException ignored) { + + } + } + + /** + * 处理数据 + */ + public void processData() { + //剩余空间 + int remaining = byteBuf.readableBytes(); + if (remaining > 0) { + dataParse(); + byteBufCompact(); + } else { + byteBufClear(); + pollToByteBuffer(); + } + } + + /** + * 1. **检查可读数据**:获取缓冲区中可读字节的长度 + * 2. **有数据时**:调用 `discardReadBytes()` 丢弃已读数据,将剩余数据移到缓冲区开头 + * 3. **无数据时**:直接调用 `byteBufClear()` 清空缓冲区 + * 目的是优化缓冲区内存使用,避免已读数据占用空间。 + */ + private void byteBufCompact() { + int readableLength = byteBuf.readableBytes(); + if (readableLength > 0) { + // 它会丢弃已读数据,并将剩余数据移动到缓冲区开头 + byteBuf.discardReadBytes(); + } else { + // 没有可读数据,直接清空 + byteBufClear(); + } + } + + /** + * 清空byteBuf + */ + private void byteBufClear() { + // 步骤1:释放原有缓冲区(关键,避免内存泄漏) + byteBuf.release(); + // 步骤2:重新创建 128 字节的缓冲区 + byteBuf = allocator.buffer(BYTE_BUF_INITIAL_CAPACITY); + } + + /** + * 重置byteBuf + */ + private void byteBufRewind() { + byteBuf.readerIndex(0); + byteBuf.writerIndex(0); + } + + /** + * 拉取队列数据到byteBuffer + */ + private void pollToByteBuffer() { + try { + currentParseOriginalData = dataQueue.take(); + byteBuf.writeBytes(currentParseOriginalData.getBytesContent()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 解析数据 + */ + private void dataParse() { + try { + //避免协议内容被更改,重新获取最新协议 + Protocol protocol = parserManager.getProtocol(protocolId); + if (protocol == null) { + //未找到协议 + return; + } + ProtocolXml protocolXml = protocol.getProtocolXml(); + //协议元数据 + Meta meta = protocolXml.getMeta(); + if (meta == null) { + //放弃解析当前协议 + return; + } + //协议字段列表 + List filedList = protocolXml.getFileds(); +// filedList.sort(Comparator.comparingInt(Filed::getSnInt)); + //key为字段类型的map + Map filedMap = protocol.getFiledMap(); +// for (Filed filed : filedList) { +// filedMap.put(filed.getType(), filed); +// meta.setDataLength(meta.getDataLength() + (filed.getBitsLen() / 8)); +// } + if (filedMap.containsKey(ProtocolEnum.VARIABLE_LENGTH.getValue())) { + //包含可变长度字段 + //尝试从头解析到可变长度字段 + int variableLengthIndex = 0; + for (Filed filed : filedList) { + variableLengthIndex += filed.getBitsLen() / 8; + if (filed.getType().equals(ProtocolEnum.VARIABLE_LENGTH.getValue())) { + break; + } + } + //判断剩余长度数据是否可满足需要数据长度 + if (byteBuf.readableBytes() < variableLengthIndex) { + pollToByteBuffer(); + return; + } + + //计算并匹配数据长度 + int dataLength = 0; + for (Filed filed : filedList) { + int byteLen = filed.getBitsLen() / 8; + //剩余数据长度不足 + if (byteBuf.readableBytes() < byteLen) { + return; + } + byte[] filedDataBytes = new byte[byteLen]; + byteBuf.readBytes(filedDataBytes); + + if (filed.getType().equals(ProtocolEnum.VARIABLE_LENGTH.getValue())) { + //解析可变长度字段值,并从可变长度描述列表中匹配数据长度 + String variableLengthValue = DataUtils.byteToHex(filedDataBytes); + if (meta.getVariableLengthDescList() != null && !meta.getVariableLengthDescList().isEmpty()) { + for (VariableLengthDesc desc : meta.getVariableLengthDescList()) { + if (desc.getValue().equals(variableLengthValue)) { + dataLength = desc.getLengthInt(); + break; + } + } + } + break; + } + } + //将解析出来的数据长度赋值给VAR字段,并重新结算dataLength + for (Filed filed : filedList) { + if (filed.getType().equals(ProtocolEnum.VAR.getValue())) { + filed.setBitslen(String.valueOf(dataLength)); + } + meta.setDataLength(meta.getDataLength() + (filed.getBitsLen() / 8)); + } + //保证后续数据从0开始 + byteBufRewind(); + } + + //判断剩余数据长度是否满足协议数据长度 + if (byteBuf.readableBytes() < meta.getDataLength()) { + //尝试判断帧头是否匹配,不匹配则将当前数据抛弃 + String p_metaFrameHeader = meta.getFrameHeadValue(); + if (StringUtils.isNotBlank(p_metaFrameHeader) && filedMap.get(ProtocolEnum.FRAME_HEADER.getValue()) != null) { + Filed frameHeadFiled = filedMap.get(ProtocolEnum.FRAME_HEADER.getValue()); + int headLength = frameHeadFiled.getBitsLen() / 8; + if (byteBuf.readableBytes() >= headLength) { + byte[] headBytes = new byte[headLength]; + byteBuf.readBytes(headBytes); + byte[] headBits = DataUtils.byteArrayToBitArray(headBytes); + String d_frameHeaderValue = getDFrameHeaderStringValue(frameHeadFiled.getBitsLen(), headBits); + if (!p_metaFrameHeader.equals(d_frameHeaderValue)) { + return; + } else { + byteBufRewind(); + } + } else { + byteBufClear(); + } + } + pollToByteBuffer(); + return; + } + //读取协议所有字节数 + byte[] totalBytes = new byte[meta.getDataLength()]; + byteBuf.readBytes(totalBytes); + //将所有字节转换为bit + byte[] totalBits = DataUtils.byteArrayToBitArray(totalBytes); + //校验数据 + List checkBitList = new ArrayList<>(); + //帧数据 + List sourceBitList = new ArrayList<>(); + + int checkStartSn = 0; + try { + checkStartSn = Integer.parseInt(meta.getCheckStartSn()); + } catch (Exception ignored) { + } + int checkEndSn = 0; + try { + checkEndSn = Integer.parseInt(meta.getCheckEndSn()); + } catch (Exception ignored) { + } + + int readPosition = 0; + for (Filed filed : filedList) { + int bitsLen = filed.getBitsLen(); + //剩余数据长度不足 + //读取该字段二进制 bit + byte[] filedDataBits = new byte[bitsLen]; + System.arraycopy(totalBits, readPosition, filedDataBits, 0, bitsLen); + readPosition += bitsLen; + + for (byte filedDataBit : filedDataBits) { + if (filed.getSnInt() >= checkStartSn && filed.getSnInt() <= checkEndSn) { + checkBitList.add(filedDataBit); + } + sourceBitList.add(filedDataBit); + } + filed.setBinStr(DataUtils.bitsToBinaryString(filedDataBits)); + filed.setFiledValue(filedDataBits); + } + if (isStop) { + return; + } + //协议元数据中帧头 + String p_metaFrameHeader = meta.getFrameHeadValue(); + if (StringUtils.isNotBlank(p_metaFrameHeader)) { + p_metaFrameHeader = p_metaFrameHeader.replace("0x", "").replace("0X", ""); +// int p_frameHeader = Integer.parseInt(p_metaFrameHeader.replace("0x", ""), 16); + //帧头 + if (filedMap.get(ProtocolEnum.FRAME_HEADER.getValue()) != null) { + Filed frameHeadFiled = filedMap.get(ProtocolEnum.FRAME_HEADER.getValue()); + String d_frameHeaderValue = getDFrameHeaderStringValue(frameHeadFiled.getBitsLen(), frameHeadFiled.getFiledValue()); + if (d_frameHeaderValue.equals(p_metaFrameHeader)) { + //帧头匹配成功 + //找到协议中的校验字段 + if (filedMap.get(ProtocolEnum.CALC.getValue()) != null) { + Filed calcFiled = filedMap.get(ProtocolEnum.CALC.getValue()); + try { + //获取校验脚本 + IVerificationInterface checkInterface = (IVerificationInterface) ScriptManager.getScriptClass(ScriptManager.ScriptType.IVerificationInterface.getValue(), meta.getVerifyScript()).newInstance(); + byte[] checkBits = DataUtils.byteListToBytes(checkBitList); + byte[] checkBytes = DataUtils.bitArrayToByteArray(checkBits); + + byte[] calcBytes = DataUtils.bitArrayToByteArray(calcFiled.getFiledValue()); + boolean checkSuccess = checkInterface.verification(checkBytes, calcBytes, new VerifyUtils()); + if (checkSuccess) { + pushSampledParseResult(protocol, filedList, sourceBitList); + } + } catch (Exception e) { + e.printStackTrace(); + System.out.println("该协议校验脚本异常:" + e.getMessage()); + } + } else { + pushSampledParseResult(protocol, filedList, sourceBitList); + } + } + } + } else { + pushSampledParseResult(protocol, filedList, sourceBitList); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + + /** + * 获取帧头数据 + * + * @param frameHeadFiled 帧头字段 + * @return 帧头数据 + */ + private long getDFrameHeaderValue(Filed frameHeadFiled) { + long d_frameHeaderValue = -1; + if (frameHeadFiled.getBitsLen() % 8 == 0) { + byte[] frameHeadBytes = DataUtils.bitArrayToByteArray(frameHeadFiled.getFiledValue()); + if (frameHeadFiled.getEndian().equalsIgnoreCase(ProtocolEnum.ENDIAN_BIG.getValue())) { + //大端序 + d_frameHeaderValue = DataUtils.toIntBigEndian(frameHeadBytes); + } else { + //小端序 + d_frameHeaderValue = DataUtils.toIntLittleEndian(frameHeadBytes); + } + } + return d_frameHeaderValue; + } + + private String getDFrameHeaderStringValue(int bitsLength, byte[] bitArr) { + String d_frameHeaderValue = ""; + if (bitsLength % 8 == 0) { + byte[] frameHeadBytes = DataUtils.bitArrayToByteArray(bitArr); + d_frameHeaderValue = DataUtils.byteToHex(frameHeadBytes); + } + return d_frameHeaderValue; + } + + /** + * 解析字段物理数据 + * + * @param filedList 字段列表 + * @param sourceBytes 源数据 + */ + private List parseFiledPhysicsData(List filedList, byte[] sourceBytes) { + //获取解析数据脚本 + List filedParseDataList = new ArrayList<>(); + for (Filed filed : filedList) { + //判断字段类型是否为解析字段 +// if (filed.getType().equalsIgnoreCase(ProtocolEnum.VAR.getValue())) { +// boolean isByte = filed.getBitsLen() % 8 == 0; +// String parserResultValue; +// if (isByte) { +// //8的倍数,按字节解析 +// parserResultValue = DataUtils.byteToHex(DataUtils.bitArrayToByteArray(filed.getFiledValue())); +// } else { +// //非8的倍数,按位解析 +// parserResultValue = DataUtils.bitsToBinaryString(filed.getFiledValue()); +// } +// +// if (StringUtils.isNotBlank(filed.getPhyScript())) { +// //字段解析脚本 +// try { +// IParseDataInterface parserDataInterface = (IParseDataInterface) ScriptManager.getScriptClass(ScriptManager.ScriptType.IParseDataInterface.getValue(), filed.getPhyScript()).newInstance(); +// if (isByte) { +// //8的倍数,按字节解析 +// parserResultValue = parserDataInterface.parsePhysicalFromData(filed.getSnInt(), sourceBytes, DataUtils.bitArrayToByteArray(filed.getFiledValue()), filed.getEndian(), filed.getBinStr().replaceAll(" ", "")); +// } else { +// //非8的倍数,按位解析 +// parserResultValue = parserDataInterface.parsePhysicalFromData(filed.getSnInt(), sourceBytes, filed.getFiledValue(), filed.getEndian(), filed.getBinStr().replaceAll(" ", "")); +// } +// } catch (Exception e) { +// e.printStackTrace(); +// } +// } +// if (isByte) { +// //8的倍数,按字节解析 +// filedParseDataList.add(new FiledParseData(DataUtils.byteToHex(DataUtils.bitArrayToByteArray(filed.getFiledValue())) + "(十六)", parserResultValue, filed.getLabel(), filed.getReadName(), filed.getPhyUnit(), filed.getPhyType())); +// } else { +// //非8的倍数,按位解析 +// filedParseDataList.add(new FiledParseData(filed.getBinStr() + "(二)", parserResultValue, filed.getLabel(), filed.getReadName(), filed.getPhyUnit(), filed.getPhyType())); +// } +// } else + if (filed.getType().equalsIgnoreCase(ProtocolEnum.FRAME.getValue())) { + //下一个解析器 + BlockingQueue framePipList = new LinkedBlockingQueue<>(); + try { + framePipList.put(new OriginalData(currentParseOriginalData.getDataBoardId(), filed.getFiledValue(), currentParseOriginalData.getTimestamp())); + ParserThread nextParserThread = new ParserThread(currentChannelId, dataBoardId, framePipList, parserSort + 1, parserService, parserManager); + if (nextParserThread.init()) { + nextParserThread.start(); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } else { + boolean isByte = filed.getBitsLen() % 8 == 0; + String parserResultValue; + if (isByte) { + //8的倍数,按字节解析 + parserResultValue = DataUtils.byteToHex(DataUtils.bitArrayToByteArray(filed.getFiledValue())); + } else { + //非8的倍数,按位解析 + parserResultValue = DataUtils.bitsToBinaryString(filed.getFiledValue()); + } + + if (StringUtils.isNotBlank(filed.getPhyScript())) { + //字段解析脚本 + try { + IParseDataInterface parserDataInterface = (IParseDataInterface) ScriptManager.getScriptClass(ScriptManager.ScriptType.IParseDataInterface.getValue(), filed.getPhyScript()).newInstance(); + if (isByte) { + //8的倍数,按字节解析 + parserResultValue = parserDataInterface.parsePhysicalFromData(filed.getSnInt(), sourceBytes, DataUtils.bitArrayToByteArray(filed.getFiledValue()), filed.getEndian(), filed.getBinStr().replaceAll(" ", "")); + } else { + //非8的倍数,按位解析 + parserResultValue = parserDataInterface.parsePhysicalFromData(filed.getSnInt(), sourceBytes, filed.getFiledValue(), filed.getEndian(), filed.getBinStr().replaceAll(" ", "")); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + if (isByte) { + //8的倍数,按字节解析 + filedParseDataList.add(new FiledParseData(DataUtils.byteToHex(DataUtils.bitArrayToByteArray(filed.getFiledValue())) + "(十六)", parserResultValue, filed.getLabel(), filed.getReadName(), filed.getPhyUnit(), filed.getPhyType())); + } else { + //非8的倍数,按位解析 + filedParseDataList.add(new FiledParseData(filed.getBinStr() + "(二)", parserResultValue, filed.getLabel(), filed.getReadName(), filed.getPhyUnit(), filed.getPhyType())); + } + } + } + return filedParseDataList; + } + + /** + * 采样推送解析结果 -- 根据时间间隔决定是否完整解析字段 + */ + private void pushSampledParseResult(Protocol protocol, List filedList, List sourceBitList) { + long now = System.currentTimeMillis(); + boolean shouldSample = (now - lastSampledTimestamp) >= samplingIntervalMs; + + List filedParseDataList; + if (shouldSample) { + lastSampledTimestamp = now; + byte[] sourceBits = DataUtils.byteListToBytes(sourceBitList); + byte[] sourceBytes = DataUtils.bitArrayToByteArray(sourceBits); + filedParseDataList = parseFiledPhysicsData(filedList, sourceBytes); + } else { + filedParseDataList = Collections.emptyList(); + } + + ParserMessage msg = new ParserMessage(UUIDUtils.getUUID(), getName(), currentChannelId, dataBoardId, filedParseDataList, protocol.getProtocolId(), protocol.getProtocolContent(), currentParseOriginalData.getTimestamp(), shouldSample); + ParserMessageQueueManager.pushMessage(msg); + } + + /** + * 移除原始数据 + * + * @param dataBoardId 数据看板id + */ + public void removeOriginalData(String dataBoardId) { + if (dataQueue != null) { + dataQueue.removeIf(originalData -> originalData.getDataBoardId().equals(dataBoardId)); + } + } + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/parser/enums/ProtocolEnum.java b/src/main/java/com/cbsd/universaltestsoftware_client/parser/enums/ProtocolEnum.java new file mode 100644 index 0000000..6d97d03 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/parser/enums/ProtocolEnum.java @@ -0,0 +1,59 @@ +package com.cbsd.universaltestsoftware_client.parser.enums; + +import lombok.Getter; + +@Getter +public enum ProtocolEnum { + /** + * 协议头 + */ + FRAME_HEADER("HEAD"), + /** + * 长度 + */ + LENGTH("LENGTH"), + /** + * 可变长度 + */ + VARIABLE_LENGTH("VARIABLE_LENGTH"), + /** + * 计算 + */ + CALC("CALC"), + /** + * 变量 + */ + VAR("VAR"), + /** + * 帧 + */ + FRAME("FRAME"), + /** + * 帧尾尾 + */ + TAIL ("TAIL "), + /** + * 大端 + */ + ENDIAN_BIG("BIG"), + /** + * 小端 + */ + ENDIAN_LITTLE("LITTLE"); + + private final String value; + + ProtocolEnum(String value) { + this.value = value; + } + + public static ProtocolEnum getByValue(String value) { + for (ProtocolEnum item : ProtocolEnum.values()) { + if (item.value.equals(value)) { + return item; + } + } + return null; + } + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/Filed.java b/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/Filed.java new file mode 100644 index 0000000..21a1fe8 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/Filed.java @@ -0,0 +1,302 @@ +package com.cbsd.universaltestsoftware_client.parser.model; + +import com.alibaba.fastjson.JSON; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; + +@XmlAccessorType(XmlAccessType.FIELD) +public class Filed { + @XmlElement(name = "sn") + private String sn = ""; + /** + * 变量名 + */ + @XmlElement(name = "label") + private String label = ""; + /** + * 中文现实名称 + */ + @XmlElement(name = "read_name") + private String readName = ""; + /** + * 物理类型 int, unsigned int, float,double,hex,time + */ + @XmlElement(name = "phy_type") + private String phyType = ""; + /** + * 时间格式 + */ + @XmlElement(name = "time_format") + private String timeFormat = ""; + /** + * 字段的占用的比特长度 + */ + @XmlElement(name = "bitslen") + private String bitslen; + /** + * 1 代表对应位的值不关心,在转化成物理值和后续计算、验证 + * 时都不使用 + */ + @XmlElement(name = "bitmask") + private String bitmask = ""; + /** + * 内置脚本 + */ + @XmlElement(name = "phy_inner_script") + private String phyInnerScript = ""; + /** + * 将码流转换为物理值 + */ + @XmlElement(name = "phy_script") + private String phyScript = ""; + @XmlElement(name = "phy_script_file") + private String phyScriptFile = ""; + @XmlElement(name = "endian") + private String endian = ""; + @XmlElement(name = "type") + private String type = ""; + @XmlElement(name = "frame") + private String frame = ""; + @XmlElement(name = "calc_script") + private String calcScript = ""; + @XmlElement(name = "calc_script_file") + private String calcScriptFile = ""; + @XmlElement(name = "calc_script_args") + private String calcScriptArgs = ""; + @XmlElement(name = "phy_unit") + private String phyUnit = ""; + @XmlElement(name = "bin_str") + private String binStr = ""; + @XmlElement(name = "hex") + private String hex = ""; + @XmlElement(name = "phy") + private String phy = ""; + @XmlElement(name = "avg") + private String avg = ""; + @XmlElement(name = "min") + private String min = ""; + @XmlElement(name = "max") + private String max = ""; + /** + * 存储字段值 + */ + private byte[] filedData; + + public Filed() { + } + + public String getSn() { + return sn; + } + + public int getSnInt() { + try { + return Integer.parseInt(sn); + }catch (Exception e) { + return 0; + } + } + + public void setSn(String sn) { + this.sn = sn; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public String getReadName() { + return readName; + } + + public void setReadName(String readName) { + this.readName = readName; + } + + public String getPhyType() { + return phyType; + } + + public void setPhyType(String phyType) { + this.phyType = phyType; + } + + public String getTimeFormat() { + return timeFormat; + } + + public void setTimeFormat(String timeFormat) { + this.timeFormat = timeFormat; + } + + public String getBitslen() { + return bitslen; + } + + public int getBitsLen() { + try { + return Integer.parseInt(bitslen); + }catch (Exception e) { + return 0; + } + } + + public void setBitslen(String bitslen) { + this.bitslen = bitslen; + } + + public String getBitmask() { + return bitmask; + } + + public void setBitmask(String bitmask) { + this.bitmask = bitmask; + } + + public String getPhyInnerScript() { + return phyInnerScript; + } + + public void setPhyInnerScript(String phyInnerScript) { + this.phyInnerScript = phyInnerScript; + } + + public String getPhyScript() { + return phyScript; + } + + public void setPhyScript(String phyScript) { + this.phyScript = phyScript; + } + + public String getPhyScriptFile() { + return phyScriptFile; + } + + public void setPhyScriptFile(String phyScriptFile) { + this.phyScriptFile = phyScriptFile; + } + + public String getEndian() { + return endian; + } + + public void setEndian(String endian) { + this.endian = endian; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getFrame() { + return frame; + } + + public void setFrame(String frame) { + this.frame = frame; + } + + public String getCalcScript() { + return calcScript; + } + + public void setCalcScript(String calcScript) { + this.calcScript = calcScript; + } + + public String getCalcScriptFile() { + return calcScriptFile; + } + + public void setCalcScriptFile(String calcScriptFile) { + this.calcScriptFile = calcScriptFile; + } + + public String getCalcScriptArgs() { + return calcScriptArgs; + } + + public void setCalcScriptArgs(String calcScriptArgs) { + this.calcScriptArgs = calcScriptArgs; + } + + public String getPhyUnit() { + return phyUnit; + } + + public void setPhyUnit(String phyUnit) { + this.phyUnit = phyUnit; + } + + public String getBinStr() { + return binStr; + } + + public void setBinStr(String binStr) { + this.binStr = binStr; + } + + public String getHex() { + return hex; + } + + public void setHex(String hex) { + this.hex = hex; + } + + public String getPhy() { + return phy; + } + + public void setPhy(String phy) { + this.phy = phy; + } + + public String getAvg() { + return avg; + } + + public void setAvg(String avg) { + this.avg = avg; + } + + public String getMin() { + return min; + } + + public void setMin(String min) { + this.min = min; + } + + public String getMax() { + return max; + } + + public void setMax(String max) { + this.max = max; + } + + public byte[] getFiledValue() { + return filedData; + } + + public void setFiledValue(byte[] value) { + this.filedData = value; + } + + @Override + public String toString() { + return JSON.toJSONString(this); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/FiledParseData.java b/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/FiledParseData.java new file mode 100644 index 0000000..0032d81 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/FiledParseData.java @@ -0,0 +1,29 @@ +package com.cbsd.universaltestsoftware_client.parser.model; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class FiledParseData { + + @ApiModelProperty("原始数据") + private String sourceValue; + // @ApiModelProperty("16进制数据") +// private String hexValue; + @ApiModelProperty("物理数据") + private String phyValue; + @ApiModelProperty("变量名") + private String label; + @ApiModelProperty("读取字段名称") + private String readName; + @ApiModelProperty("物理数据单位") + private String phyUnit; + @ApiModelProperty("物理数据类型") + private String phyType; + + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/Filed_Back.java b/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/Filed_Back.java new file mode 100644 index 0000000..56caf71 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/Filed_Back.java @@ -0,0 +1,283 @@ +package com.cbsd.universaltestsoftware_client.parser.model; + +import com.alibaba.fastjson.JSON; + +import javax.xml.bind.annotation.XmlElement; + +public class Filed_Back { + private int sn; + /** + * 变量名 + */ + private String label; + /** + * 中文现实名称 + */ + private String readName; + /** + * 物理类型 int, unsigned int, float,double,hex,time + */ + private String phyType; + /** + * 时间格式 + */ + private String timeFormat; + /** + * 字段的占用的比特长度 + */ + private int bitslen; + /** + * 1 代表对应位的值不关心,在转化成物理值和后续计算、验证 + * 时都不使用 + */ + private String bitmask; + /** + * 内置脚本 + */ + private String phyInnerScript; + /** + * 将码流转换为物理值 + */ + private String phyScript; + private String phyScriptFile; + private String endian; + private String type; + private String frame; + private String calcScript; + private String calcScriptFile; + private String calcScriptArgs; + private String phyUnit; + private String binStr; + private String hex; + private String phy; + private String avg; + private String min; + private String max; + /** + * 存储字段值 + */ + private byte[] filedData; + + public Filed_Back() { + } + + @XmlElement(name = "sn") + public int getSn() { + return sn; + } + + public void setSn(int sn) { + this.sn = sn; + } + + @XmlElement(name = "label") + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + @XmlElement(name = "read_name") + public String getReadName() { + return readName; + } + + public void setReadName(String readName) { + this.readName = readName; + } + + @XmlElement(name = "phy_type") + public String getPhyType() { + return phyType; + } + + public void setPhyType(String phyType) { + this.phyType = phyType; + } + + @XmlElement(name = "time_format") + public String getTimeFormat() { + return timeFormat; + } + + public void setTimeFormat(String timeFormat) { + this.timeFormat = timeFormat; + } + + @XmlElement(name = "bitslen") + public int getBitslen() { + return bitslen; + } + + public void setBitslen(int bitslen) { + this.bitslen = bitslen; + } + + @XmlElement(name = "bitmask") + public String getBitmask() { + return bitmask; + } + + public void setBitmask(String bitmask) { + this.bitmask = bitmask; + } + + @XmlElement(name = "phy_inner_script") + public String getPhyInnerScript() { + return phyInnerScript; + } + + public void setPhyInnerScript(String phyInnerScript) { + this.phyInnerScript = phyInnerScript; + } + + @XmlElement(name = "phy_script") + public String getPhyScript() { + return phyScript; + } + + public void setPhyScript(String phyScript) { + this.phyScript = phyScript; + } + + @XmlElement(name = "phy_script_file") + public String getPhyScriptFile() { + return phyScriptFile; + } + + public void setPhyScriptFile(String phyScriptFile) { + this.phyScriptFile = phyScriptFile; + } + + @XmlElement(name = "endian") + public String getEndian() { + return endian; + } + + public void setEndian(String endian) { + this.endian = endian; + } + + @XmlElement(name = "type") + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + @XmlElement(name = "frame") + public String getFrame() { + return frame; + } + + public void setFrame(String frame) { + this.frame = frame; + } + + @XmlElement(name = "calc_script") + public String getCalcScript() { + return calcScript; + } + + public void setCalcScript(String calcScript) { + this.calcScript = calcScript; + } + + @XmlElement(name = "calc_script_file") + public String getCalcScriptFile() { + return calcScriptFile; + } + + public void setCalcScriptFile(String calcScriptFile) { + this.calcScriptFile = calcScriptFile; + } + + @XmlElement(name = "calc_script_args") + public String getCalcScriptArgs() { + return calcScriptArgs; + } + + public void setCalcScriptArgs(String calcScriptArgs) { + this.calcScriptArgs = calcScriptArgs; + } + + @XmlElement(name = "phy_unit") + public String getPhyUnit() { + return phyUnit; + } + + public void setPhyUnit(String phyUnit) { + this.phyUnit = phyUnit; + } + + @XmlElement(name = "bin_str") + public String getBinStr() { + return binStr; + } + + public void setBinStr(String binStr) { + this.binStr = binStr; + } + + @XmlElement(name = "hex") + public String getHex() { + return hex; + } + + public void setHex(String hex) { + this.hex = hex; + } + + @XmlElement(name = "phy") + public String getPhy() { + return phy; + } + + public void setPhy(String phy) { + this.phy = phy; + } + + @XmlElement(name = "avg") + public String getAvg() { + return avg; + } + + public void setAvg(String avg) { + this.avg = avg; + } + + @XmlElement(name = "min") + public String getMin() { + return min; + } + + public void setMin(String min) { + this.min = min; + } + + @XmlElement(name = "max") + public String getMax() { + return max; + } + + public void setMax(String max) { + this.max = max; + } + + public byte[] getFiledValue() { + return filedData; + } + + public void setFiledValue(byte[] value) { + this.filedData = value; + } + + @Override + public String toString() { + return JSON.toJSONString(this); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/FrameHeader.java b/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/FrameHeader.java new file mode 100644 index 0000000..2659bf2 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/FrameHeader.java @@ -0,0 +1,22 @@ +package com.cbsd.universaltestsoftware_client.parser.model; + +import lombok.Data; + +/** + * 帧头 + */ +@Data +public class FrameHeader { + /** + * 值 + */ + private String value; + /** + * 长度 + */ + private int frameHeaderLength; + /** + * 协议id + */ + private String protocolId; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/Meta.java b/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/Meta.java new file mode 100644 index 0000000..d1ddd57 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/Meta.java @@ -0,0 +1,82 @@ +package com.cbsd.universaltestsoftware_client.parser.model; + +import javax.xml.bind.annotation.*; +import java.util.List; + +@XmlAccessorType(XmlAccessType.FIELD) +public class Meta { + @XmlElement(name = "frame_head_value") + private String frameHeadValue = ""; + @XmlElement(name = "frame_tail_value") + private String frameTailValue = ""; + @XmlElement(name = "check_start_sn") + private String checkStartSn = ""; + @XmlElement(name = "check_end_sn") + private String checkEndSn = ""; + @XmlElement(name = "verify_script") + private String verifyScript = ""; + @XmlElementWrapper(name = "variable_length_desc") + @XmlElement(name = "desc") + private List variableLengthDescList; + /** + * 数据总长度 + */ + @XmlTransient + private int dataLength; + + public String getFrameHeadValue() { + return frameHeadValue; + } + + public void setFrameHeadValue(String frameHeadValue) { + this.frameHeadValue = frameHeadValue; + } + + public String getFrameTailValue() { + return frameTailValue; + } + + public void setFrameTailValue(String frameTailValue) { + this.frameTailValue = frameTailValue; + } + + public String getCheckStartSn() { + return checkStartSn; + } + + public void setCheckStartSn(String checkStartSn) { + this.checkStartSn = checkStartSn; + } + + public String getCheckEndSn() { + return checkEndSn; + } + + public void setCheckEndSn(String checkEndSn) { + this.checkEndSn = checkEndSn; + } + + public String getVerifyScript() { + return verifyScript; + } + + public void setVerifyScript(String verifyScript) { + this.verifyScript = verifyScript; + } + + public List getVariableLengthDescList() { + return variableLengthDescList; + } + + public void setVariableLengthDescList(List variableLengthDescList) { + this.variableLengthDescList = variableLengthDescList; + } + + public int getDataLength() { + return dataLength; + } + + public void setDataLength(int dataLength) { + this.dataLength = dataLength; + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/OriginalData.java b/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/OriginalData.java new file mode 100644 index 0000000..40cdd09 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/OriginalData.java @@ -0,0 +1,17 @@ +package com.cbsd.universaltestsoftware_client.parser.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class OriginalData { + + private String dataBoardId; + + private byte[] bytesContent; + + private long timestamp; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/ParserMessage.java b/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/ParserMessage.java new file mode 100644 index 0000000..ceebc6c --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/ParserMessage.java @@ -0,0 +1,32 @@ +package com.cbsd.universaltestsoftware_client.parser.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ParserMessage { + + private String messageId; + + private String threadName; + + private String channelId; + + private String dataBoardId; + + private List filedParseDataList; + + private String protocolId; + + private String protocolContent; + + private long timestamp; + + private boolean sampled; + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/ProtocolXml.java b/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/ProtocolXml.java new file mode 100644 index 0000000..04b606b --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/ProtocolXml.java @@ -0,0 +1,53 @@ +package com.cbsd.universaltestsoftware_client.parser.model; + +import javax.xml.bind.annotation.*; +import java.util.ArrayList; +import java.util.List; + +@XmlRootElement(name = "protocol") +@XmlAccessorType(XmlAccessType.FIELD) +public class ProtocolXml { + @XmlElement(name = "read_name") + private String readName = ""; + @XmlElement(name = "meta") + private Meta meta; + @XmlElementWrapper(name = "fileds") + @XmlElement(name = "filed") + private List fileds; + + public ProtocolXml() { + this.fileds = new ArrayList<>(); + } + + public String getReadName() { + return readName; + } + + public void setReadName(String readName) { + this.readName = readName; + } + + public Meta getMeta() { + return meta; + } + + public void setMeta(Meta meta) { + this.meta = meta; + } + + public List getFileds() { + return fileds; + } + + public void setFileds(List fileds) { + this.fileds = fileds; + } + + @Override + public String toString() { + return "Protocol{" + + "readName='" + readName + '\'' + + ", fileds.size=" + fileds.size() + + '}'; + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/VariableLengthDesc.java b/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/VariableLengthDesc.java new file mode 100644 index 0000000..29f736e --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/parser/model/VariableLengthDesc.java @@ -0,0 +1,39 @@ +package com.cbsd.universaltestsoftware_client.parser.model; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; + +/** + * 可变字段描述 + */ +@XmlAccessorType(XmlAccessType.FIELD) +public class VariableLengthDesc { + @XmlElement(name = "value") + private String value = ""; + @XmlElement(name = "length") + private String length; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + public String getLength() { + return length; + } + + public int getLengthInt() { + try { + return Integer.parseInt(length); + }catch (Exception e) { + return 0; + } + } + + public void setLength(String length) { + this.length = length; + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/script/AccumulatePreTreatment.java b/src/main/java/com/cbsd/universaltestsoftware_client/script/AccumulatePreTreatment.java new file mode 100644 index 0000000..090ad14 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/script/AccumulatePreTreatment.java @@ -0,0 +1,28 @@ +package com.cbsd.universaltestsoftware_client.script; + +import com.cbsd.client.pretreatment.CalculateUtils; +import com.cbsd.client.pretreatment.IPreTreatmentInterface; + +import java.util.Map; + +/** + * 累加预处理 + */ +public class AccumulatePreTreatment implements IPreTreatmentInterface { + + @Override + public String pretreatment(String command, int repeatCount, int hitCount, String latestHitTime, Map dataMap, CalculateUtils calculateUtils) { + String resultCommand = ""; + if (hitCount < 10) { + resultCommand += "0" + hitCount; + } else { + resultCommand = String.valueOf(hitCount); + } + return resultCommand; + } + + @Override + public String comment() { + return "根据命中次数累加拼接"; + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/script/CrcCheck.java b/src/main/java/com/cbsd/universaltestsoftware_client/script/CrcCheck.java new file mode 100644 index 0000000..965ae24 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/script/CrcCheck.java @@ -0,0 +1,31 @@ +package com.cbsd.universaltestsoftware_client.script; + +import com.cbsd.client.verification.IVerificationInterface; +import com.cbsd.client.verification.VerifyUtils; +import org.springframework.stereotype.Component; + +/** + * CRC校验 + */ +@Component +public class CrcCheck implements IVerificationInterface { + + @Override + public boolean verification(byte[] data, byte[] verify, VerifyUtils verifyUtils) { + try { + byte[] verifyCRC = new byte[data.length + verify.length]; + System.arraycopy(data, 0, verifyCRC, 0, data.length); + System.arraycopy(verify, 0, verifyCRC, data.length, verify.length); + return verifyUtils.verifyCRC(verifyCRC); + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } + + @Override + public String comment() { + return ""; + } + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/script/DigitalParser.java b/src/main/java/com/cbsd/universaltestsoftware_client/script/DigitalParser.java new file mode 100644 index 0000000..b52b24d --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/script/DigitalParser.java @@ -0,0 +1,50 @@ +package com.cbsd.universaltestsoftware_client.script; + +import com.cbsd.client.parse.IParseDataInterface; + +public class DigitalParser implements IParseDataInterface { + @Override + public String parsePhysicalFromData(int sn, byte[] sourceData, byte[] fieldData, String endian, String binStr) { + + StringBuilder result = new StringBuilder(); + if (fieldData.length == 1) { + byte b = fieldData[0]; + String binary = byteToBinary(b); + for (int i = 0; i < binary.length(); i++) { + result.append("通道:"); + result.append(i); + if (binary.charAt(i) == '1') { + result.append(" 高电平"); + } else { + result.append(" 低电平"); + } + result.append(","); + } + result.deleteCharAt(result.lastIndexOf(",")); + } + return result.toString(); + } + + @Override + public String comment() { + return "数字信号解析"; + } + + /** + * 将byte转换为8位二进制字符串 + * + * @param b 要转换的byte值 + * @return 8位二进制字符串 + */ + public static String byteToBinary(byte b) { + // 将byte转换为无符号整数,避免符号位扩展 + int unsignedByte = b & 0xFF; + // 转换为二进制字符串 + String binary = Integer.toBinaryString(unsignedByte); + // 确保结果是8位,不足的在前面补0 + while (binary.length() < 8) { + binary = "0" + binary; + } + return binary; + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/script/ScriptManager.java b/src/main/java/com/cbsd/universaltestsoftware_client/script/ScriptManager.java new file mode 100644 index 0000000..a09056e --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/script/ScriptManager.java @@ -0,0 +1,501 @@ +package com.cbsd.universaltestsoftware_client.script; + +import com.cbsd.client.branch.IBranchConditionInterface; +import com.cbsd.client.check.ICheckInterface; +import com.cbsd.client.parse.IParseDataInterface; +import com.cbsd.client.pretreatment.IPreTreatmentInterface; +import com.cbsd.client.verification.IVerificationInterface; +import com.cbsd.universaltestsoftware_client.exception.BaseException; +import com.cbsd.universaltestsoftware_client.service.JavaCompileService; +import com.cbsd.universaltestsoftware_client.util.DateUtils; +import com.cbsd.universaltestsoftware_client.util.DynamicClassLoader; +import com.cbsd.universaltestsoftware_client.util.FileUtils; +import com.cbsd.universaltestsoftware_client.util.Result; +import com.cbsd.universaltestsoftware_client.vo.InstructScriptVo; +import com.cbsd.universaltestsoftware_client.vo.ScriptTypeVo; +import com.cbsd.universaltestsoftware_client.vo.ScriptVo; +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.system.ApplicationHome; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Component +public class ScriptManager { + + @Resource + private JavaCompileService javaCompileService; + + public enum ScriptType { + IVerificationInterface("verification", "校验脚本", ScriptTemplate.VERIFICATION), + IParseDataInterface("parse", "解析脚本", ScriptTemplate.PARSE), + IPreTreatmentInterface("pretreatment", "预处理脚本", ScriptTemplate.PRE_TREATMENT), + IBranchConditionInterface("branch", "分支条件脚本", ScriptTemplate.BRANCH_CONDITION), + ICheckInterface("check", "检查脚本", ScriptTemplate.CHECK); + + @Getter + private final String value; + + @Getter + private final String typeName; + + @Getter + private final String template; + + ScriptType(String value, String typeName, String template) { + this.value = value; + this.typeName = typeName; + this.template = template; + } + + public static List getValues() { + return Stream.of(values()).map(ScriptType::getValue).collect(Collectors.toList()); + } + } + + /** + * 脚本包名 + */ + @Value("${cbsd.scriptBasePackageName}") + private String scriptBasePackageName; + /** + * 脚本文件路径 + */ + @Value("${cbsd.scriptRootPath}") + private String scriptRootPath; + + + private String scriptRealRootPath; + + /** + * 脚本类型map + */ + public static final Map>> scriptTypeMap = new ConcurrentHashMap<>(); + + @PostConstruct + public void init() { + String jarPath = getJarPath(); + if (!jarPath.endsWith(".jar")) { + scriptRealRootPath = scriptRootPath; + } else { + scriptRealRootPath = jarPath.substring(0, jarPath.lastIndexOf(File.separator)) + File.separator + "script"; + } + new Thread(this::compileAllScript).start(); + } + + public String getJarPath() { + // 获取当前应用的ApplicationHome对象 + ApplicationHome home = new ApplicationHome(getClass()); + // 获取JAR文件对象 + File jarFile = home.getSource(); + if (jarFile != null) { + // 返回JAR文件的绝对路径 + return jarFile.getAbsolutePath(); + } + // 如果不是在JAR包中运行(如IDE中),返回当前工作目录 + return System.getProperty("user.dir"); + } + + /** + * 重新编译所有脚本 + */ + public void recompileAllScript() { + scriptTypeMap.clear(); + new Thread(this::compileAllScript).start(); + } + + /** + * 编译脚本 + */ + private void compileAllScript() { + for (ScriptType scriptType : ScriptType.values()) { + File file = new File(scriptRealRootPath, scriptType.getValue()); + if (!file.exists()) { + file.mkdirs(); + } + List fileList = FileUtils.getDirectoryFiles(file.getPath()); + File classDic = new File(file.getPath(), "classes"); + if (!classDic.exists()) { + classDic.mkdirs(); + } + Map> scriptMap = new ConcurrentHashMap<>(); + for (File scriptFile : fileList) { + if (scriptFile.getPath().endsWith(".java")) { + try { + String srcPackageName = scriptBasePackageName + "." + scriptType.getValue(); + String loadClassName = javaCompileService.compileJavaSource(srcPackageName, classDic.getPath(), scriptFile.getPath(), scriptFile.getName().replace(".java", "")) + .replace(classDic.getPath() + File.separatorChar, "") + .replace(File.separatorChar, '.') + .replace(".class", ""); + DynamicClassLoader dynamicClassLoader = new DynamicClassLoader(classDic.getPath()); + Class dynamicClass = dynamicClassLoader.loadClass(loadClassName); + scriptMap.put(dynamicClass.getSimpleName(), dynamicClass); + } catch (Exception e) { + e.printStackTrace(); + System.out.println(e.getMessage()); + } + } else { + System.out.println(scriptFile.getPath() + " 不是一个java文件, 不能编译成java脚本"); + } + } + scriptTypeMap.put(scriptType.getValue(), scriptMap); + } + } + + /** + * 编译脚本文件 + * + * @param scriptType 脚本类型 + * @param scriptFile 脚本文件 + */ + public void compileScriptFile(String scriptType, File scriptFile) { + File scriptTypeDic = new File(scriptRealRootPath, scriptType); + if (!scriptTypeDic.exists()) { + scriptTypeDic.mkdirs(); + } + File classDic = new File(scriptTypeDic.getPath(), "classes"); + if (!classDic.exists()) { + classDic.mkdirs(); + } + if (scriptFile.getPath().endsWith(".java")) { + try { + Map> scriptMap = scriptTypeMap.get(scriptType); + if (scriptMap == null) { + scriptMap = new ConcurrentHashMap<>(); + } + String srcPackageName = scriptBasePackageName + "." + scriptType; + String loadClassName = javaCompileService.compileJavaSource(srcPackageName, classDic.getPath(), scriptFile.getPath(), scriptFile.getName().replace(".java", "")) + .replace(classDic.getPath() + File.separatorChar, "") + .replace(File.separatorChar, '.') + .replace(".class", ""); + DynamicClassLoader dynamicClassLoader = new DynamicClassLoader(classDic.getPath()); + Class dynamicClass = dynamicClassLoader.loadClass(loadClassName); + scriptMap.put(dynamicClass.getSimpleName(), dynamicClass); + + scriptTypeMap.put(scriptType, scriptMap); + } catch (Exception e) { + throw new BaseException(e.getMessage()); + } + } + } + + /** + * 创建脚本文件 + * + * @param scriptType 脚本类型 + * @param scriptContent 脚本内容 + */ + public void createScriptFile(String scriptType, String scriptContent) { + String className = scriptContent.substring(scriptContent.indexOf("public class") + 12, scriptContent.indexOf("implements")).trim(); + File scriptTypeDic = new File(scriptRealRootPath, scriptType); + if (!scriptTypeDic.exists()) { + scriptTypeDic.mkdirs(); + } + File scriptFile = new File(scriptTypeDic, className + ".java"); + try { + Files.write(Paths.get(scriptFile.getPath()), scriptContent.getBytes(StandardCharsets.UTF_8)); + compileScriptFile(scriptType, scriptFile); + } catch (Exception e) { + scriptFile.delete(); + throw new BaseException(e.getMessage()); + } + } + + /** + * 删除脚本文件 + * + * @param scriptType 脚本类型 + * @param scriptFile 脚本文件 + */ + public void deleteScriptFile(String scriptType, String scriptFile) { + File scriptTypeDic = new File(scriptRealRootPath, scriptType); + if (scriptTypeDic.exists()) { + File existsFile = new File(scriptTypeDic, scriptFile); + if (existsFile.exists()) { + existsFile.delete(); + } + } + } + + /** + * 获取脚本类 + * + * @param scriptType 脚本类型 + * @param scriptName 脚本名称 + */ + public static Class getScriptClass(String scriptType, String scriptName) { + if (scriptTypeMap.get(scriptType) != null) { + return scriptTypeMap.get(scriptType).get(scriptName); + } else { + return null; + } + } + + /** + * 获取脚本类型列表 + */ + public List getScriptTypeList() { + List scriptTypeVoList = new ArrayList<>(); + for (ScriptType scriptType : ScriptType.values()) { + ScriptTypeVo scriptTypeVo = new ScriptTypeVo(); + scriptTypeVo.setScriptType(scriptType.getValue()); + scriptTypeVo.setScriptTypeName(scriptType.getTypeName()); + scriptTypeVo.setScriptTemplateContent(scriptType.getTemplate()); + scriptTypeVo.setScriptDirectoryPath(scriptRealRootPath + File.separator + scriptType.getValue()); + scriptTypeVoList.add(scriptTypeVo); + } + return scriptTypeVoList; + } + + /** + * 获取脚本列表 + * + * @param scriptType 脚本类型 + */ + public List getScriptList(String scriptType) { + List scriptVoList = new ArrayList<>(); + File scriptTypeDic = new File(scriptRealRootPath, scriptType); + if (scriptTypeDic.exists()) { + List fileList = FileUtils.getDirectoryFiles(scriptTypeDic.getPath()); + fileList.sort((o1, o2) -> { + if (o1.lastModified() > o2.lastModified()){ + return -1; + } else if (o1.lastModified() < o2.lastModified()){ + return 1; + } + return 0; + }); + + Map> scriptMap = scriptTypeMap.get(scriptType); + for (File file : fileList) { + ScriptVo scriptVo = new ScriptVo(); + scriptVo.setFilePath(file.getPath()); + scriptVo.setFileName(file.getName()); + scriptVo.setScriptType(scriptType); + scriptVo.setLastModifiedTime(DateUtils.timestampToDateString(file.lastModified())); + + String scriptName = file.getName().replace(".java", ""); + + if (scriptMap != null) { + Class cls = scriptMap.get(scriptName); + if (cls != null) { + //已编译 + scriptVo.setCompileStatus(1); + scriptVo.setScriptName(scriptName); + if (scriptType.equalsIgnoreCase(ScriptType.IVerificationInterface.getValue())) { + try { + IVerificationInterface checkInterface = (IVerificationInterface) getScriptClass(ScriptType.IVerificationInterface.getValue(), scriptName).newInstance(); + scriptVo.setScriptComment(checkInterface.comment()); + } catch (InstantiationException | IllegalAccessException ignored) { + scriptVo.setCompileStatus(0); + } + } else if (scriptType.equalsIgnoreCase(ScriptType.IParseDataInterface.getValue())) { + try { + IParseDataInterface parserDataInterface = (IParseDataInterface) getScriptClass(ScriptType.IParseDataInterface.getValue(), scriptName).newInstance(); + scriptVo.setScriptComment(parserDataInterface.comment()); + } catch (InstantiationException | IllegalAccessException ignored) { + scriptVo.setCompileStatus(0); + } + } else if (scriptType.equalsIgnoreCase(ScriptType.IPreTreatmentInterface.getValue())) { + try { + IPreTreatmentInterface preTreatmentInterface = (IPreTreatmentInterface) getScriptClass(ScriptType.IPreTreatmentInterface.getValue(), scriptName).newInstance(); + scriptVo.setScriptComment(preTreatmentInterface.comment()); + } catch (InstantiationException | IllegalAccessException ignored) { + scriptVo.setCompileStatus(0); + } + } else if (scriptType.equalsIgnoreCase(ScriptType.IBranchConditionInterface.getValue())) { + try { + IBranchConditionInterface branchConditionInterface = (IBranchConditionInterface) getScriptClass(ScriptType.IBranchConditionInterface.getValue(), scriptName).newInstance(); + scriptVo.setScriptComment(branchConditionInterface.comment()); + } catch (InstantiationException | IllegalAccessException ignored) { + scriptVo.setCompileStatus(0); + } + } else if (scriptType.equalsIgnoreCase(ScriptType.ICheckInterface.getValue())) { + try { + ICheckInterface checkInterface = (ICheckInterface) getScriptClass(ScriptType.ICheckInterface.getValue(), scriptName).newInstance(); + scriptVo.setScriptComment(checkInterface.comment()); + } catch (InstantiationException | IllegalAccessException ignored) { + scriptVo.setCompileStatus(0); + } + } + } else { + //未编译 + scriptVo.setCompileStatus(0); + } + } + scriptVoList.add(scriptVo); + } + } + return scriptVoList; + } + + /** + * 编译脚本 + * + * @param scriptType 脚本类型 + * @param scriptFileName 脚本文件名 + */ + public Result compileScript(String scriptType, String scriptFileName) { + File scriptTypeDic = new File(scriptRealRootPath, scriptType); + File scriptFile = new File(scriptTypeDic, scriptFileName); + if (!scriptFile.exists()) { + return Result.error("脚本文件不存在"); + } + try { + compileScriptFile(scriptType, scriptFile); + } catch (Exception e) { + return Result.error(e.getMessage()); + } + return Result.success(); + } + + /** + * 删除脚本 + * + * @param scriptType 脚本类型 + * @param scriptFileName 脚本文件名 + * @param scriptName 脚本名称 + */ + public void deleteScript(String scriptType, String scriptFileName, String scriptName) { + File scriptTypeDic = new File(scriptRealRootPath, scriptType); + File scriptFile = new File(scriptTypeDic, scriptFileName); + if (scriptFile.exists()) { + //删除脚本java文件 + scriptFile.delete(); + } + if (StringUtils.isNotBlank(scriptName)) { + //删除脚本class文件 + File scriptClassDic = new File(scriptRealRootPath, scriptType + File.separator + "classes" + + File.separator + "com" + File.separator + "cbsd" + File.separator + "client" + File.separator + scriptType); + File scriptClassFile = new File(scriptClassDic, scriptName + ".class"); + if (scriptClassFile.exists()) { + scriptClassFile.delete(); + } + + if (scriptTypeMap.get(scriptType) != null) { + scriptTypeMap.get(scriptType).remove(scriptName); + } + } + } + + /** + * 上传脚本文件 + * + * @param scriptType 脚本类型 + * @param file 文件 + */ + public InstructScriptVo uploadScript(String scriptType, MultipartFile file) { + if (StringUtils.isBlank(scriptType)) { + throw new NullPointerException("脚本类型不能为空"); + } + if (!ScriptManager.ScriptType.getValues().contains(scriptType)) { + throw new BaseException("脚本类型错误"); + } + if (file == null) { + throw new NullPointerException("文件不能为空"); + } + String fileName = file.getOriginalFilename(); + if (StringUtils.isBlank(fileName)) { + throw new NullPointerException("文件名称不能为空"); + } + File parentFile = new File(scriptRealRootPath, scriptType); + if (!parentFile.exists()) { + parentFile.mkdirs(); + } + String suffix = fileName.substring(fileName.lastIndexOf(".")); + if (!".java".equals(suffix)) { + throw new BaseException("请上传Java文件(.java)"); + } + File existsFile = new File(parentFile, fileName); + if (!existsFile.exists()) { + try { + Path savePath = Paths.get(parentFile.getPath(), fileName); + Files.write(savePath, file.getBytes()); + //添加进脚本 + compileScriptFile(scriptType, savePath.toFile()); + } catch (Exception e) { + throw new BaseException(e.getMessage()); + } + } + InstructScriptVo vo = new InstructScriptVo(); + vo.setScriptType(scriptType); + vo.setScriptFile(fileName); + vo.setScriptName(fileName.replace(suffix, "")); + return vo; + } + + /** + * 保存脚本内容 + * + * @param instructId 指令ID + * @param scriptType 脚本类型 + * @return + */ + public InstructScriptVo saveScriptContent(String instructId, String scriptType, String scriptContent) { + if (StringUtils.isBlank(instructId)) { + throw new NullPointerException("指令ID不能为空"); + } + if (StringUtils.isBlank(scriptType)) { + throw new NullPointerException("脚本类型不能为空"); + } + if (!ScriptManager.ScriptType.getValues().contains(scriptType)) { + throw new BaseException("脚本类型错误"); + } + File parentFile = new File(scriptRealRootPath, scriptType); + if (!parentFile.exists()) { + parentFile.mkdirs(); + } + String firstChar = scriptType.substring(0, 1); + String className = firstChar.toUpperCase() + scriptType.replaceFirst(firstChar, "") + "_" + instructId; + if (StringUtils.isNotBlank(scriptContent)) { + className = scriptContent.substring(scriptContent.indexOf("public class") + 12, scriptContent.indexOf("implements")).trim(); + } + File scriptFile = new File(parentFile, className + ".java"); + //脚本内容为空的情况下删除之前的文件 + if (scriptFile.exists()) { + scriptFile.delete(); + } + if (StringUtils.isNotBlank(scriptContent)) { + // 使用try-with-resources语法,自动关闭资源 + try (BufferedWriter writer = Files.newBufferedWriter( + Paths.get(scriptFile.getPath()), + StandardCharsets.UTF_8)) { + writer.write(scriptContent); + } catch (IOException e) { + scriptFile.delete(); + throw new BaseException(e.getMessage()); + } + try { + //添加进脚本 + compileScriptFile(scriptType, scriptFile); + } catch (Exception e) { + scriptFile.delete(); + throw new BaseException(e.getMessage()); + } + } + InstructScriptVo vo = new InstructScriptVo(); + vo.setScriptFile(scriptFile.getName()); + vo.setScriptType(scriptType); + vo.setScriptName(className); + return vo; + } + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/script/ScriptTemplate.java b/src/main/java/com/cbsd/universaltestsoftware_client/script/ScriptTemplate.java new file mode 100644 index 0000000..c8f95ab --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/script/ScriptTemplate.java @@ -0,0 +1,183 @@ +package com.cbsd.universaltestsoftware_client.script; + +/** + * 脚本模板 + */ +public class ScriptTemplate { + /** + * 预处理脚本 + */ + public static final String PRE_TREATMENT = "package com.cbsd.client.pretreatment;\n" + + "\n" + + "import com.cbsd.client.pretreatment.CalculateUtils;\n" + + "import com.cbsd.client.pretreatment.IPreTreatmentInterface;\n" + + "\n" + + "import java.util.Map;\n" + + "\n" + + "public class 【类名-请修改】 implements IPreTreatmentInterface {\n" + + " \n" + + " /**\n" + + " * 预处理\n" + + " * @param command 指令内容\n" + + " * @param repeatCount 重复次数\n" + + " * @param hitCount 命中次数\n" + + " * @param latestHitTime 最后命中时间\n" + + " * @param dataMap 数据集合,key为协议字段属性名(label),value为物理值\n" + + " * @param calculateUtils 计算工具类\n" + + " * @return 结果\n" + + " */\n" + + " @Override\n" + + " public String pretreatment(String command, int repeatCount, int hitCount, String latestHitTime, Map dataMap, CalculateUtils calculateUtils) {\n" + + " return \"\";\n" + + " }\n" + + "\n" + + " /**\n" + + " * 描述\n" + + " *\n" + + " * @return 描述内容\n" + + " */\n" + + " @Override\n" + + " public String comment() {\n" + + " return \"\";\n" + + " }\n" + + "}\n"; + + /** + * 分支条件脚本 branchCondition 方法 + */ + public static final String BRANCH_CONDITION = "package com.cbsd.client.branch;\n" + + "\n" + + "import com.cbsd.client.branch.IBranchConditionInterface;\n" + + "\n" + + "import java.util.Map;\n" + + "\n" + + "public class 【类名-请修改】 implements IBranchConditionInterface {\n" + + "\n" + + " /**\n" + + " * 分支条件\n" + + " * @param originalCommand 原始指令数据\n" + + " * @param preTreatmentCommand 预处理指令数据,若无预处理,则为原始指令数据\n" + + " * @param repeatCount 重复次数\n" + + " * @param hitCount 命中次数\n" + + " * @param latestHitTime 最后命中时间\n" + + " * @param dataMap 数据集合,key为协议字段属性名(label),value为物理值\n" + + " * @return 需要跳转的指令名称,为空则为不跳转\n" + + " */\n" + + " @Override\n" + + " public String branchCondition(String originalCommand, String preTreatmentCommand, int repeatCount, int hitCount, String latestHitTime, Map dataMap) {\n" + + " return \"\";\n" + + " }\n" + + " \n" + + " /**\n" + + " * 描述\n" + + " *\n" + + " * @return 描述内容\n" + + " */\n" + + " @Override\n" + + " public String comment() {\n" + + " return \"\";\n" + + " }\n" + + "}\n"; + + /** + * 检查脚本 check 方法 + */ + public static final String CHECK = "package com.cbsd.client.check;\n" + + "\n" + + "import com.cbsd.client.check.ICheckInterface;\n" + + "\n" + + "import java.util.List;\n" + + "import java.util.Map;\n" + + "\n" + + "public class 【类名-请修改】 implements ICheckInterface {\n" + + "\n" + + " /**\n" + + " * 检查\n" + + " * @param originalCommand 原始指令数据\n" + + " * @param preTreatmentCommand 预处理指令数据,若无预处理,则为原始指令数据\n" + + " * @param repeatCount 重复次数\n" + + " * @param hitCount 命中次数\n" + + " * @param latestHitTime 最后命中时间\n" + + " * @param dataMap 数据集合,key为协议字段属性名(label),value为物理值\n" + + " * @return 日志信息列表\n" + + " */\n" + + " @Override\n" + + " public List check(String originalCommand, String preTreatmentCommand, int repeatCount, int hitCount, String latestHitTime, Map dataMap) {\n" + + " return null;\n" + + " }\n" + + "\n" + + " /**\n" + + " * 描述\n" + + " *\n" + + " * @return 描述内容\n" + + " */\n" + + " @Override\n" + + " public String comment() {\n" + + " return \"\";\n" + + " }\n" + + "}\n"; + + public static final String PARSE = "package com.cbsd.client.parse;\n" + + "\n" + + "import com.cbsd.client.parse.IParseDataInterface;\n" + + "\n" + + "public class 【类名-请修改】 implements IParseDataInterface {\n" + + "\n" + + " /**\n" + + " * 从数据解析物理值\n" + + " *\n" + + " * @param sn 当前字段序号\n" + + " * @param sourceData 源数据\n" + + " * @param fieldData 当前字段数据\n" + + " * @param endian BIG:大端序 LITTLE:小端序\n" + + " * @param binStr 二进制字符串\n" + + " * @return 解析后的数据值\n" + + " */\n" + + " @Override\n" + + " public String parsePhysicalFromData(int sn, byte[] sourceData, byte[] fieldData, String endian, String binStr) {\n" + + " return \"\";\n" + + " }\n" + + "\n" + + " /**\n" + + " * 描述\n" + + " *\n" + + " * @return 描述内容\n" + + " */\n" + + " @Override\n" + + " public String comment() {\n" + + " return \"\";\n" + + " }\n" + + "}\n"; + + public static final String VERIFICATION = "package com.cbsd.client.verification;\n" + + "\n" + + "import com.cbsd.client.verification.IVerificationInterface;\n" + + "import com.cbsd.client.verification.VerifyUtils;\n" + + "\n" + + "public class 【类名-请修改】 implements IVerificationInterface {\n" + + "\n" + + " /**\n" + + " * 校验\n" + + " *\n" + + " * @param data 校验数据\n" + + " * @param verify 校验位\n" + + " * @param verifyUtils 校验工具类\n" + + " * @return 校验结果\n" + + " */\n" + + " @Override\n" + + " public boolean verification(byte[] data, byte[] verify, VerifyUtils verifyUtils) {\n" + + " return false;\n" + + " }\n" + + "\n" + + " /**\n" + + " * 描述\n" + + " *\n" + + " * @return 描述内容\n" + + " */\n" + + " @Override\n" + + " public String comment() {\n" + + " return \"\";\n" + + " }\n" + + "}\n"; + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/script/TemperatureParser.java b/src/main/java/com/cbsd/universaltestsoftware_client/script/TemperatureParser.java new file mode 100644 index 0000000..e9de431 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/script/TemperatureParser.java @@ -0,0 +1,33 @@ +package com.cbsd.universaltestsoftware_client.script; + +import com.cbsd.client.parse.IParseDataInterface; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Component +public class TemperatureParser implements IParseDataInterface { + + @Override + public String parsePhysicalFromData(int sn, byte[] sourceData, byte[] fieldData, String endian, String binStr) { + List serialPortDataHexList = new ArrayList<>(); + for (byte b : fieldData) { + int h = (b >>> 4) & 0xF; + int l = b & 0xF; + char ch = (char) ((h < 10) ? ('0' + h) : ('A' + h - 10)); + char cl = (char) ((l < 10) ? ('0' + l) : ('A' + l - 10)); + String sb = String.valueOf(ch) + cl; + serialPortDataHexList.add(sb); + } + //温度 + String temperatureChar = serialPortDataHexList.get(0) + serialPortDataHexList.get(1); + double temperature = Integer.parseInt(temperatureChar, 16) / 10.0; + return String.valueOf(temperature); + } + + @Override + public String comment() { + return ""; + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/script/TestBranchCondition.java b/src/main/java/com/cbsd/universaltestsoftware_client/script/TestBranchCondition.java new file mode 100644 index 0000000..bb543fe --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/script/TestBranchCondition.java @@ -0,0 +1,26 @@ +package com.cbsd.universaltestsoftware_client.script; + +import com.cbsd.client.branch.IBranchConditionInterface; + +import java.util.Map; + +public class TestBranchCondition implements IBranchConditionInterface { + + @Override + public String branchCondition(String originalCommand, String preTreatmentCommand, int repeatCount, int hitCount, String latestHitTime, Map dataMap) { + String temperatureStr = dataMap.get("temperature"); + if (temperatureStr != null && !temperatureStr.isEmpty()){ + double temperature = Double.parseDouble(temperatureStr); + if (temperature > 25.5){ + //此处为指令名称 + return "打开降温指令"; + } + } + return null; + } + + @Override + public String comment() { + return "跳转指令5"; + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/script/TestCheckScript.java b/src/main/java/com/cbsd/universaltestsoftware_client/script/TestCheckScript.java new file mode 100644 index 0000000..e015adf --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/script/TestCheckScript.java @@ -0,0 +1,23 @@ +package com.cbsd.universaltestsoftware_client.script; + +import com.cbsd.client.check.ICheckInterface; +import com.cbsd.universaltestsoftware_client.util.DateUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class TestCheckScript implements ICheckInterface { + + @Override + public List check(String originalCommand, String preTreatmentCommand, int repeatCount, int hitCount, String latestHitTime, Map dataMap) { + List list = new ArrayList<>(); + list.add(DateUtils.dateToString() + " 检查合格"); + return list; + } + + @Override + public String comment() { + return "打印检查日志"; + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/script/TestScript.java b/src/main/java/com/cbsd/universaltestsoftware_client/script/TestScript.java new file mode 100644 index 0000000..e13478b --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/script/TestScript.java @@ -0,0 +1,8 @@ +package com.cbsd.universaltestsoftware_client.script; + +public class TestScript { + + public static void main(String[] args) { + System.out.println("测试脚本"); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/script/VoltageParser.java b/src/main/java/com/cbsd/universaltestsoftware_client/script/VoltageParser.java new file mode 100644 index 0000000..f0062c9 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/script/VoltageParser.java @@ -0,0 +1,46 @@ +package com.cbsd.universaltestsoftware_client.script; + +import com.cbsd.client.parse.IParseDataInterface; + +import java.nio.ByteBuffer; + +public class VoltageParser implements IParseDataInterface { + @Override + public String parsePhysicalFromData(int sn, byte[] sourceData, byte[] fieldData, String endian, String binStr) { + if (fieldData.length % 8 != 0){ + return ""; + } + double[] doubles = bytesToDoubles(fieldData); + if (doubles.length == 0) { + return ""; + } + return String.valueOf(doubles[0]); + } + + @Override + public String comment() { + return "ADDA卡获取电压0到电压31"; + } + + /** + * 将byte数组转换回double数组(还原原始数据) + * + * @param bytes 输入的byte数组 + * @return 转换后的double数组 + * @throws IllegalArgumentException 如果字节数组长度不是8的倍数 + */ + public static double[] bytesToDoubles(byte[] bytes) { + // 计算可以转换的double数量 + int doubleLength = bytes.length / 8; + // 创建ByteBuffer并放入字节数组 + ByteBuffer buffer = ByteBuffer.wrap(bytes); + double[] doubles = new double[doubleLength]; + + // 从缓冲区中读取所有double值 + for (int i = 0; i < doubleLength; i++) { + doubles[i] = buffer.getDouble(); + } + + return doubles; + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/BoardService.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/BoardService.java new file mode 100644 index 0000000..d7fc79b --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/BoardService.java @@ -0,0 +1,48 @@ +package com.cbsd.universaltestsoftware_client.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.cbsd.universaltestsoftware_client.config.BasePage; +import com.cbsd.universaltestsoftware_client.dto.BoardTemperatureDto; +import com.cbsd.universaltestsoftware_client.dto.ChannelPageDto; +import com.cbsd.universaltestsoftware_client.dto.PageBoard; +import com.cbsd.universaltestsoftware_client.entity.Board; +import com.baomidou.mybatisplus.extension.service.IService; +import com.cbsd.universaltestsoftware_client.entity.Channel; +import com.cbsd.universaltestsoftware_client.util.Result; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; + +/** + *

+ * 板卡 服务类 + *

+ * + * @author admin + * @since 2025-08-26 + */ +public interface BoardService { + + String pageList(PageBoard pageBoard); + + String deleteById(String serverId,String boardId); + + String getBoardType(String serverId); + + String saveOrUpdateBoard(Board board); + + String saveOrUpdateChannel(Channel channel); + + Result pageChannelList(ChannelPageDto channelPageDto); + + String deleteChannelById(String serverId, String channelId); + + String getChannelPath(String serverId,String boardId); + + String refressBoardInformation( String serverId); + + String getChannelTypeAll(String serverId,String boardTypeId); + + String getBoardTemperature(@RequestBody BoardTemperatureDto dto); +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/ChannelDataHitService.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/ChannelDataHitService.java new file mode 100644 index 0000000..77da26f --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/ChannelDataHitService.java @@ -0,0 +1,13 @@ +package com.cbsd.universaltestsoftware_client.service; + +import com.cbsd.universaltestsoftware_client.entity.ChannelDataHit; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author SanJie +* @description 针对表【channel_data_hit(通道协议命中次数)】的数据库操作Service +* @createDate 2026-01-23 16:24:09 +*/ +public interface ChannelDataHitService extends IService { + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/ChannelDataService.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/ChannelDataService.java new file mode 100644 index 0000000..9b3d307 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/ChannelDataService.java @@ -0,0 +1,34 @@ +package com.cbsd.universaltestsoftware_client.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.cbsd.universaltestsoftware_client.dto.ChannelDataHistoryDto; +import com.cbsd.universaltestsoftware_client.dto.ChannelDataStatisticsDto; +import com.cbsd.universaltestsoftware_client.dto.PageChannelDataDto; +import com.cbsd.universaltestsoftware_client.entity.ChannelData; +import com.cbsd.universaltestsoftware_client.util.Result; +import com.cbsd.universaltestsoftware_client.vo.ChannelDataStatisticsVo; + +import java.util.List; + +/** +* @author SanJie +* @description 针对表【channel_data(解析的数据)】的数据库操作Service +* @createDate 2025-09-09 09:53:44 +*/ +public interface ChannelDataService extends IService { + void start(PageChannelDataDto pageChannelDataDto); + + void stop(String dataBoardId); + + void updateData(PageChannelDataDto pageChannelDataDto); + + String channelDataQuery(ChannelDataHistoryDto channelDataHistoryDto); + + String channelDataDelete(ChannelDataHistoryDto channelDataHistoryDto); + + Result> getChannelDataStatistics(ChannelDataStatisticsDto dto); + + Result clearChannelDataStatistics(ChannelDataStatisticsDto dto); + + void clearChannelData(); +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/ChannelDataTrafficService.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/ChannelDataTrafficService.java new file mode 100644 index 0000000..f1cd28b --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/ChannelDataTrafficService.java @@ -0,0 +1,13 @@ +package com.cbsd.universaltestsoftware_client.service; + +import com.cbsd.universaltestsoftware_client.entity.ChannelDataTraffic; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author SanJie +* @description 针对表【channel_data_traffic(通道流量统计)】的数据库操作Service +* @createDate 2026-01-23 10:07:19 +*/ +public interface ChannelDataTrafficService extends IService { + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/DataBoardChannelService.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/DataBoardChannelService.java new file mode 100644 index 0000000..5d8127f --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/DataBoardChannelService.java @@ -0,0 +1,16 @@ +package com.cbsd.universaltestsoftware_client.service; + +import com.cbsd.universaltestsoftware_client.entity.DataBoardChannel; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + *

+ * 服务类 + *

+ * + * @author admin + * @since 2025-09-10 + */ +public interface DataBoardChannelService extends IService { + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/DataBoardService.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/DataBoardService.java new file mode 100644 index 0000000..16a20a4 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/DataBoardService.java @@ -0,0 +1,39 @@ +package com.cbsd.universaltestsoftware_client.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.cbsd.universaltestsoftware_client.dto.DownloadDto; +import com.cbsd.universaltestsoftware_client.dto.ExportDataBoardDto; +import com.cbsd.universaltestsoftware_client.dto.InsertDataBoardDto; +import com.cbsd.universaltestsoftware_client.entity.DataBoard; +import com.baomidou.mybatisplus.extension.service.IService; +import com.cbsd.universaltestsoftware_client.entity.DataBoardChannel; +import com.cbsd.universaltestsoftware_client.entity.Download; +import com.cbsd.universaltestsoftware_client.util.Result; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + *

+ * 数据看板 服务类 + *

+ * + * @author admin + * @since 2025-09-10 + */ +public interface DataBoardService extends IService { + + void saveOrUpdateDataBoard(InsertDataBoardDto insertDataBoardDto) throws Exception; + + void removeDataBoardById(String dataBoardId); + + List listDataBoard(Integer dataType); + + void updateDataBoardById(DataBoardChannel dataBoardChannel); + + Result exportDataBoard(ExportDataBoardDto exportDataBoardDto, HttpServletResponse response); + + void clear(ExportDataBoardDto exportDataBoardDto); + + Result> exportHistory(DownloadDto downloadDto); +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/DownloadService.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/DownloadService.java new file mode 100644 index 0000000..14b3e85 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/DownloadService.java @@ -0,0 +1,16 @@ +package com.cbsd.universaltestsoftware_client.service; + +import com.cbsd.universaltestsoftware_client.entity.Download; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + *

+ * 服务类 + *

+ * + * @author admin + * @since 2026-02-03 + */ +public interface DownloadService extends IService { + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/IBootLogService.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/IBootLogService.java new file mode 100644 index 0000000..ed51d75 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/IBootLogService.java @@ -0,0 +1,24 @@ +package com.cbsd.universaltestsoftware_client.service; + + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.cbsd.universaltestsoftware_client.dto.BootLogDeleteDto; +import com.cbsd.universaltestsoftware_client.dto.BootLogPageDto; +import com.cbsd.universaltestsoftware_client.entity.BootLog; +import com.cbsd.universaltestsoftware_client.util.Result; + +/** + *

+ * 服务类 + *

+ * + * @author wt + * @since 2025-09-03 + */ +public interface IBootLogService extends IService { + + Result> bootLogPage(BootLogPageDto bootLogPageDto); + + Result bootLogDelete(BootLogDeleteDto bootLogDeleteDto); +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/InstructService.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/InstructService.java new file mode 100644 index 0000000..0308fea --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/InstructService.java @@ -0,0 +1,72 @@ +package com.cbsd.universaltestsoftware_client.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.cbsd.universaltestsoftware_client.dto.*; +import com.cbsd.universaltestsoftware_client.entity.Instruct; +import com.baomidou.mybatisplus.extension.service.IService; +import com.cbsd.universaltestsoftware_client.parser.model.Filed; +import com.cbsd.universaltestsoftware_client.util.Result; +import com.cbsd.universaltestsoftware_client.vo.InstructScriptVo; +import com.cbsd.universaltestsoftware_client.vo.ServerListVo; +import com.cbsd.universaltestsoftware_client.websocket.command.CommandRunMessage; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + *

+ * 服务类 + *

+ * + * @author admin + * @since 2025-09-01 + */ +public interface InstructService extends IService { + + Page pageList(InstructPageDto instructPageDto); + +// Result execute(String instructId, CommandRunMessage runMessage); + + Result> getChannelList(String keyword); + + String getChannelFunctionName(String channelId, String serverId); + + Result saveOrUpdateInstructGroup(InstructGroupDto instructGroupDto); + + Result saveOrUpdateInstruct(Instruct instruct); + + /** + * 上移:与前一个兄弟交换 sort + */ + Result moveUp(String instructId); + + /** + * 下移:与后一个兄弟交换 sort + */ + Result moveDown(String instructId); + + Result deleteInstructById(InstructDeleteDto dto); + + String getBoardDriverExtraData(String channelId, String serverId); + + Result> getInstructProtocolParam(String instructId); + + void createGroupFromList(CreateGroupDTO dto); + + Result batchChangeParam(BatchChangeInstructParamDto dto); + + void batchCopyBelow(BatchCopyBelowDTO dto); + + void batchMoveBelow(BatchMoveBelowDTO dto); + + Result uploadCommandFile(String instructId, MultipartFile file); + + void uploadScript(String instructId, String scriptType, String scriptName); + + void uploadScriptContent(String instructId, String scriptType, String scriptFileName, String scriptContent); + + Result deleteInstructFile(InstructDeleteFileDto dto); + + String getInstructDataRemark(String instructId); +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/InvocationService.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/InvocationService.java new file mode 100644 index 0000000..f64ccf6 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/InvocationService.java @@ -0,0 +1,34 @@ +package com.cbsd.universaltestsoftware_client.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.cbsd.universaltestsoftware_client.dto.InvocationPageDto; +import com.cbsd.universaltestsoftware_client.dto.InvocationSortDto; +import com.cbsd.universaltestsoftware_client.entity.Invocation; +import com.baomidou.mybatisplus.extension.service.IService; +import com.cbsd.universaltestsoftware_client.util.Result; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + *

+ * 用列 服务类 + *

+ * + * @author admin + * @since 2025-09-01 + */ +public interface InvocationService extends IService { + + Page pageList(InvocationPageDto invocationPageDto); + + Result sort(InvocationSortDto dto); + + + void exportInvocation(String invocationId, HttpServletResponse response) throws IOException; + + void importXml(MultipartFile file,String schemeId)throws Exception; + + Result deleteById(String invocationId); +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/JavaCompileService.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/JavaCompileService.java new file mode 100644 index 0000000..3f76396 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/JavaCompileService.java @@ -0,0 +1,297 @@ +package com.cbsd.universaltestsoftware_client.service; + +import com.cbsd.universaltestsoftware_client.DynamicClassBootstrap; +import com.cbsd.universaltestsoftware_client.exception.BaseException; +import com.cbsd.universaltestsoftware_client.util.DynamicClassLoader; +import com.cbsd.universaltestsoftware_client.util.Result; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.system.ApplicationHome; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import javax.tools.*; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Service +public class JavaCompileService { + + // 编译输出目录 + private final String OUTPUT_DIR; + private final String scriptBasePackageName; + // 用于匹配包名声明的正则表达式 + private static final Pattern PACKAGE_PATTERN = Pattern.compile("^\\s*package\\s+([\\w.]+)\\s*;\\s*$", Pattern.MULTILINE); + + public JavaCompileService(@Value("${cbsd.uploadfilesrootpath}") String outputDir, + @Value("${cbsd.scriptBasePackageName}") String scriptBasePackageName) { + this.OUTPUT_DIR = outputDir; + this.scriptBasePackageName = scriptBasePackageName; + + // 初始化输出目录 + File dir = new File(outputDir); + if (!dir.exists()) { + dir.mkdirs(); + } + } + + /** + * 编译上传的Java文件 + */ + public Result compileJavaFile(MultipartFile file) throws IOException { + // 验证文件是否为Java文件 + String fileName = file.getOriginalFilename(); + if (fileName == null || !fileName.endsWith(".java")) { + return Result.error("请上传Java文件(.java)"); + } + + // 创建临时目录存储上传的Java文件 + String tempDir = System.getProperty("java.io.tmpdir") + File.separator + UUID.randomUUID(); + new File(tempDir).mkdirs(); + + // 获取类名 + String className = fileName.substring(0, fileName.lastIndexOf(".")); + + // 保存上传的文件到临时目录 + Path javaFilePath = Paths.get(tempDir, fileName); + Files.write(javaFilePath, file.getBytes()); + + // 读取文件内容,校验包名 + String fileContent = new String(file.getBytes(), StandardCharsets.UTF_8); + Result packageCheckResult = validatePackageName(fileContent); + if (packageCheckResult.getCode() != 1) { + deleteDirectory(new File(tempDir)); + return packageCheckResult; + } + + String fullClassName = scriptBasePackageName + "." + className; + + + + // 清理临时文件 + deleteDirectory(new File(tempDir)); + + // 加载类到缓存 + + try { + DynamicClassLoader classLoader = new DynamicClassLoader(OUTPUT_DIR); + Class dynamicClass = classLoader.loadClass(fullClassName); + DynamicClassBootstrap.CLAZZ_CACHE.put(fullClassName, dynamicClass); + } catch (Exception e) { + e.printStackTrace(); + return Result.error("类加载失败: " + e.getMessage()); + } + + try { + // 编译Java文件 + String classResult = compileJavaSource(scriptBasePackageName, OUTPUT_DIR, javaFilePath.toString(), className); + return Result.success(classResult); + }catch (Exception e){ + return Result.error(e.getMessage()); + } + } + + /** + * 编译Java代码字符串 + */ + public Result compileJavaCode(String className, String code) throws IOException { + // 校验包名 + Result packageCheckResult = validatePackageName(code); + if (packageCheckResult.getCode() != 1) { + return packageCheckResult; + } + + // 创建临时目录 + String tempDir = System.getProperty("java.io.tmpdir") + File.separator + UUID.randomUUID(); + new File(tempDir).mkdirs(); + + // 创建Java文件 + String fileName = className + ".java"; + Path javaFilePath = Paths.get(tempDir, fileName); + Files.write(javaFilePath, code.getBytes(StandardCharsets.UTF_8)); + + // 编译Java文件 + String fullClassName = scriptBasePackageName + "." + className; + + // 加载类到缓存 + try { + DynamicClassLoader classLoader = new DynamicClassLoader(OUTPUT_DIR); + Class dynamicClass = classLoader.loadClass(fullClassName); + DynamicClassBootstrap.CLAZZ_CACHE.put(fullClassName, dynamicClass); + } catch (Exception e) { + e.printStackTrace(); + return Result.error("类加载失败: " + e.getMessage()); + } + + // 清理临时文件 + deleteDirectory(new File(tempDir)); + + try { + // 编译Java文件 + String classResult = compileJavaSource(scriptBasePackageName, OUTPUT_DIR, javaFilePath.toString(), className); + return Result.success(classResult); + }catch (Exception e){ + return Result.error(e.getMessage()); + } + } + + /** + * 校验Java代码中的包名是否与配置的一致 + */ + private Result validatePackageName(String javaCode) { + Matcher matcher = PACKAGE_PATTERN.matcher(javaCode); + + if (!matcher.find()) { + return Result.error("Java代码中未声明包名,请添加包名声明: package " + scriptBasePackageName + ";"); + } + + String codePackageName = matcher.group(1).trim(); + if (!codePackageName.equals(scriptBasePackageName)) { + return Result.error("包名不匹配,必须使用指定包名: " + scriptBasePackageName + ",但实际使用了: " + codePackageName); + } + + return Result.success("包名校验通过"); + } + + /** + * 实际执行Java编译的方法 + */ + public String compileJavaSource(String srcPackageName, String outPath, String filePath, String className) { + // 获取系统Java编译器 + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + if (compiler == null) { + throw new BaseException("未找到Java编译器,请确保使用JDK而非JRE运行程序"); + } + + // 创建诊断收集器,用于捕获编译错误 + DiagnosticCollector diagnostics = new DiagnosticCollector<>(); + + // 创建文件管理器 + StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null); + + // 要编译的文件 + File javaFile = new File(filePath); + List files = Collections.singletonList(javaFile); + + // 将文件转换为JavaFileObject + Iterable compilationUnits = + fileManager.getJavaFileObjectsFromFiles(files); + + // 编译选项,指定输出目录 + List options = new ArrayList<>(); + options.add("-d"); + options.add(outPath); + options.add("-encoding"); + options.add("UTF-8"); + + String jarPath = getJarPath(); + if (jarPath.endsWith(".jar")){ + // 设置类路径,关键步骤:添加所有依赖的JAR包 + options.add("-classpath"); + String classPath = buildClassPath(jarPath.substring(0, jarPath.lastIndexOf(File.separator))); + options.add(classPath); + + } + + // 编译任务 + JavaCompiler.CompilationTask task = compiler.getTask( + null, // 输出流 + fileManager, // 文件管理器 + diagnostics, // 诊断收集器 + options, // 编译选项 + null, // 要处理的类 + compilationUnits // 要编译的源文件 + ); + + // 执行编译任务 + boolean success = task.call(); + + try { + fileManager.close(); + } catch (IOException e) { + throw new BaseException("编译过程出错: " + e.getMessage()); + } + + // 收集错误信息 + if (!success) { + StringBuilder errorMsg = new StringBuilder(); + for (Diagnostic diagnostic : diagnostics.getDiagnostics()) { + errorMsg + .append(filePath) + .append(" ") + .append(String.format("错误位置: 第%d行, 第%d列 - %s%n", + diagnostic.getLineNumber(), + diagnostic.getColumnNumber(), + diagnostic.getMessage(null))); + } + + throw new BaseException(errorMsg.toString()); + } + + // 编译成功,返回class文件路径 + String[] pkgParts = srcPackageName.split("\\."); + Path classFile = Paths.get(outPath, pkgParts).resolve(className + ".class"); + + return classFile.toAbsolutePath().toString(); + } + + // 构建类路径的工具方法 + private String buildClassPath(String jarPath) { + // 系统默认类路径 + StringBuilder classpath = new StringBuilder(System.getProperty("java.class.path")); + + // 依赖的JAR文件目录 + File libDir = new File(jarPath, "libs" + File.separator + "lib"); + if (libDir.exists() && libDir.isDirectory()) { + File[] jars = libDir.listFiles((dir, name) -> name.endsWith(".jar")); + if (jars != null) { + for (File jar : jars) { + classpath.append(File.pathSeparator).append(jar.getAbsolutePath()); + } + } + } + // 额外的class文件目录 + classpath.append(File.pathSeparator).append(jarPath + File.separator + "libs" + File.separator + "classes" + File.separator); + + return classpath.toString(); + } + + /** + * 递归删除目录 + */ + private boolean deleteDirectory(File directory) { + if (directory.exists()) { + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + deleteDirectory(file); + } else { + file.delete(); + } + } + } + } + return directory.delete(); + } + + public String getJarPath() { + // 获取当前应用的ApplicationHome对象 + ApplicationHome home = new ApplicationHome(getClass()); + // 获取JAR文件对象 + File jarFile = home.getSource(); + if (jarFile != null) { + // 返回JAR文件的绝对路径 + return jarFile.getAbsolutePath(); + } + // 如果不是在JAR包中运行(如IDE中),返回当前工作目录 + return System.getProperty("user.dir"); + } + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/ParserProtocolService.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/ParserProtocolService.java new file mode 100644 index 0000000..3ce3a3f --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/ParserProtocolService.java @@ -0,0 +1,13 @@ +package com.cbsd.universaltestsoftware_client.service; + +import com.cbsd.universaltestsoftware_client.entity.ParserProtocol; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author SanJie +* @description 针对表【parser_protocol(解析器协议)】的数据库操作Service +* @createDate 2025-09-04 15:56:52 +*/ +public interface ParserProtocolService extends IService { + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/ParserService.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/ParserService.java new file mode 100644 index 0000000..617f42c --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/ParserService.java @@ -0,0 +1,28 @@ +package com.cbsd.universaltestsoftware_client.service; + +import com.cbsd.universaltestsoftware_client.entity.Parser; +import com.baomidou.mybatisplus.extension.service.IService; +import com.cbsd.universaltestsoftware_client.entity.ParserProtocol; +import com.cbsd.universaltestsoftware_client.util.Result; + +import java.util.List; + +/** +* @author SanJie +* @description 针对表【parser(解析器)】的数据库操作Service +* @createDate 2025-09-04 15:55:48 +*/ +public interface ParserService extends IService { + + Result> getParserList(String channelId); + + Result saveOrUpdateParser(Parser parser); + + Result deleteParserById(String parserId); + + Result addProtocol(List parserProtocol); + + Result> getProtocolList(String parserId); + + Result deleteParserProtocolById(String parserProtocolId); +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/ProtocolService.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/ProtocolService.java new file mode 100644 index 0000000..43c6fab --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/ProtocolService.java @@ -0,0 +1,29 @@ +package com.cbsd.universaltestsoftware_client.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.cbsd.universaltestsoftware_client.config.BasePage; +import com.cbsd.universaltestsoftware_client.dto.ProtocolPageDto; +import com.cbsd.universaltestsoftware_client.entity.Protocol; +import com.baomidou.mybatisplus.extension.service.IService; +import com.cbsd.universaltestsoftware_client.util.Result; +import com.cbsd.universaltestsoftware_client.vo.ProtocolSaveVo; +import org.springframework.web.multipart.MultipartFile; + +/** + *

+ * 协议 服务类 + *

+ * + * @author admin + * @since 2025-09-02 + */ +public interface ProtocolService extends IService { + + Result> pageListProtocol(ProtocolPageDto dto); + + Result deleteProtocolById(String protocolId); + + Result saveOrUpdateProtocol(Protocol protocol); + + Result importProtocol(MultipartFile file); +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/SchemeService.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/SchemeService.java new file mode 100644 index 0000000..e85680a --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/SchemeService.java @@ -0,0 +1,22 @@ +package com.cbsd.universaltestsoftware_client.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.cbsd.universaltestsoftware_client.config.BasePage; +import com.cbsd.universaltestsoftware_client.entity.Scheme; +import com.baomidou.mybatisplus.extension.service.IService; +import com.cbsd.universaltestsoftware_client.util.Result; + +/** + *

+ * 方案 服务类 + *

+ * + * @author admin + * @since 2025-09-01 + */ +public interface SchemeService extends IService { + + Result> pageList(BasePage basePage); + + Result deleteScheme(String schemeId); +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/SensorEquipmentService.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/SensorEquipmentService.java new file mode 100644 index 0000000..6557b52 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/SensorEquipmentService.java @@ -0,0 +1,21 @@ +package com.cbsd.universaltestsoftware_client.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.cbsd.universaltestsoftware_client.dto.SensorEquipmentDto; +import com.cbsd.universaltestsoftware_client.dto.SensorEquipmentPageDto; +import com.cbsd.universaltestsoftware_client.entity.SensorEquipment; +import com.cbsd.universaltestsoftware_client.util.Result; + +import java.util.List; + +public interface SensorEquipmentService extends IService { + + Result> pageList(SensorEquipmentPageDto dto); + + Result> getAllList(String channelId); + + Result addOrUpdate(SensorEquipmentDto dto); + + Result delete(String equipmentId); +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/ServeService.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/ServeService.java new file mode 100644 index 0000000..c7ec512 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/ServeService.java @@ -0,0 +1,28 @@ +package com.cbsd.universaltestsoftware_client.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.cbsd.universaltestsoftware_client.config.BasePage; +import com.cbsd.universaltestsoftware_client.dto.PageServerInfoDto; +import com.cbsd.universaltestsoftware_client.entity.ServerInfo; +import com.baomidou.mybatisplus.extension.service.IService; +import com.cbsd.universaltestsoftware_client.util.Result; + +/** + *

+ * 服务端 服务类 + *

+ * + * @author admin + * @since 2025-08-26 + */ +public interface ServeService extends IService { + + Result> pageList(PageServerInfoDto basePage); + + Result deleteById(String serverId); + + Result connectOrDisconnect(String serverId); + + + Result disconnectServer(String serverId); +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/SystemConfigService.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/SystemConfigService.java new file mode 100644 index 0000000..f253b2d --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/SystemConfigService.java @@ -0,0 +1,23 @@ +package com.cbsd.universaltestsoftware_client.service; + +import com.cbsd.universaltestsoftware_client.entity.SystemConfig; +import com.baomidou.mybatisplus.extension.service.IService; +import com.cbsd.universaltestsoftware_client.util.Result; + +import java.util.List; + +/** +* @author SanJie +* @description 针对表【system_config(系统配置(仅类型和内容))】的数据库操作Service +* @createDate 2025-09-19 11:18:42 +*/ +public interface SystemConfigService extends IService { + + Result> getShowColumnList(); + + Result setShowColumnList(List columnList); + + String getSystemConfig(int type); + + void setSystemConfig(int type, String value); +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/BoardServiceImpl.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/BoardServiceImpl.java new file mode 100644 index 0000000..6bda39f --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/BoardServiceImpl.java @@ -0,0 +1,273 @@ +package com.cbsd.universaltestsoftware_client.service.impl; + +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.cbsd.universaltestsoftware_client.config.BasePage; +import com.cbsd.universaltestsoftware_client.dto.BoardTemperatureDto; +import com.cbsd.universaltestsoftware_client.dto.BoardTemperatureServerDto; +import com.cbsd.universaltestsoftware_client.dto.ChannelPageDto; +import com.cbsd.universaltestsoftware_client.dto.PageBoard; +import com.cbsd.universaltestsoftware_client.entity.*; +import com.cbsd.universaltestsoftware_client.mapper.ChannelDataTrafficMapper; +import com.cbsd.universaltestsoftware_client.mapper.ParserMapper; +import com.cbsd.universaltestsoftware_client.mapper.ServerMapper; +import com.cbsd.universaltestsoftware_client.service.BoardService; +import com.cbsd.universaltestsoftware_client.util.Result; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 板卡服务实现类 + * 提供板卡与通道的远程增删改查功能 + * + * @author admin + * @since 2025-08-26 + */ +@Slf4j +@Service +public class BoardServiceImpl implements BoardService { + + @Resource + private ServerMapper serverMapper; + + @Resource + private ParserMapper parserMapper; + + @Resource + private ChannelDataTrafficMapper channelDataTrafficMapper; + + /** + * 构造服务端请求基础URL + */ + private String buildUrl(ServerInfo server, String path) { + return String.format("http://%s:%s%s", server.getIpAddress(), server.getHttpPort(), path); + } + + /** + * 检查服务端是否可连接 + */ + private void checkServerConnected(ServerInfo server) { + if (server == null) { + throw new RuntimeException("服务端不存在"); + } + if (server.getConnectStatus() == 0) { + throw new RuntimeException("服务端未连接"); + } + } + + /** + * 通用POST请求封装 + */ + private String postToServer(ServerInfo server, String path, Object body) { + try { + String url = buildUrl(server, path); + String result = HttpRequest.post(url) + .header("Content-Type", "application/json") + .body(JSONUtil.toJsonStr(body)) + .execute() + .body(); + if (StringUtils.isBlank(result)) { + throw new RuntimeException("服务端返回为空"); + } + return result; + } catch (Exception e) { + log.error("请求服务端失败, path={}, error={}", path, e.getMessage()); + throw new RuntimeException("服务端连接失败"); + } + } + + /** + * 通用GET请求封装 + */ + private String getFromServer(ServerInfo server, String path, Map params) { + try { + String url = buildUrl(server, path); + String result = HttpUtil.get(url, params); + if (StringUtils.isBlank(result)) { + throw new RuntimeException("服务端返回为空"); + } + return result; + } catch (Exception e) { + log.error("请求服务端失败, path={}, error={}", path, e.getMessage()); + throw new RuntimeException("服务端连接失败"); + } + } + + /** + * 分页查询板卡列表 + */ + @Override + public String pageList(PageBoard pageBoard) { + ServerInfo server = serverMapper.selectById(pageBoard.getServerId()); + if (server == null) { + return JSON.toJSONString(Result.success(new ArrayList<>())); + } + if (server.getConnectStatus() == 0) { + return JSON.toJSONString(Result.success(new ArrayList<>())); + } + return postToServer(server, "/universaltestsoftware_server_api/outer/client/pageListBoard", pageBoard); + } + + /** + * 删除板卡 + */ + @Override + public String deleteById(String serverId, String boardId) { + ServerInfo server = serverMapper.selectById(serverId); + checkServerConnected(server); + + Map params = Collections.singletonMap("boardId", boardId); + return getFromServer(server, "/universaltestsoftware_server_api/outer/client/deleteBoardById", params); + } + + /** + * 获取所有板卡类型 + */ + @Override + public String getBoardType(String serverId) { + ServerInfo server = serverMapper.selectById(serverId); + if (server == null || server.getConnectStatus() == 0) { + return JSON.toJSONString(Result.success(new ArrayList<>())); + } + return getFromServer(server, "/universaltestsoftware_server_api/outer/client/getBoardTypeAll", new HashMap<>()); + } + + /** + * 保存或更新板卡 + */ + @Override + public String saveOrUpdateBoard(Board board) { + ServerInfo server = serverMapper.selectById(board.getServerId()); + checkServerConnected(server); + return postToServer(server, "/universaltestsoftware_server_api/outer/client/saveOrUpdateBoard", board); + } + + /** + * 保存或更新通道 + */ + @Override + public String saveOrUpdateChannel(Channel channel) { + ServerInfo server = serverMapper.selectById(channel.getServerId()); + checkServerConnected(server); + return postToServer(server, "/universaltestsoftware_server_api/outer/client/channelInsertOrUpdate", channel); + } + + /** + * 分页查询通道列表,并补充解析数量 + */ + @Override + public Result pageChannelList(ChannelPageDto dto) { + ServerInfo server = serverMapper.selectById(dto.getServerId()); + if (server == null || server.getConnectStatus() == 0) { + return Result.success(); + } + + try { + String result = postToServer(server, "/universaltestsoftware_server_api/outer/client/channelPage", dto); + Result resultObj = JSON.parseObject(result, Result.class); + if (resultObj.getCode() == 1) { + BasePage basePage = JSON.parseObject(JSON.toJSONString(resultObj.getData()), BasePage.class); + List channels = JSON.parseArray(JSON.toJSONString(basePage.getRecords()), Channel.class); + + List channelDataTrafficList = channelDataTrafficMapper.selectList(Wrappers.emptyWrapper()); + Map channelDataTrafficMap = null; + if (channelDataTrafficList != null && !channelDataTrafficList.isEmpty()){ + channelDataTrafficMap = channelDataTrafficList.stream() + .collect(Collectors.toMap(ChannelDataTraffic::getChannelId, channelDataTraffic -> channelDataTraffic)); + } + // 补充解析数量 + for (Channel channel : channels) { + channel.setServerId(server.getServerId()); + long count = parserMapper.selectCount(new LambdaQueryWrapper() + .eq(Parser::getChannelId, channel.getChannelId())); + channel.setParsingQuantity((int) count); + + if (channelDataTrafficMap != null && channelDataTrafficMap.get(channel.getChannelId()) != null){ + channel.setByteCount(channelDataTrafficMap.get(channel.getChannelId()).getByteCount()); + } else { + channel.setByteCount(0); + } + } + + basePage.setRecords(channels); + resultObj.setData(basePage); + } + return resultObj; + } catch (Exception e) { + log.error("分页查询通道失败", e); + throw new RuntimeException("服务端连接失败"); + } + } + + /** + * 删除通道 + */ + @Override + public String deleteChannelById(String serverId, String channelId) { + ServerInfo server = serverMapper.selectById(serverId); + checkServerConnected(server); + + Map params = Collections.singletonMap("channelId", channelId); + return getFromServer(server, "/universaltestsoftware_server_api/outer/client/channelDeleteById", params); + } + + /** + * 获取通信通道路径 + */ + @Override + public String getChannelPath(String serverId, String boardId) { + ServerInfo server = serverMapper.selectById(serverId); + if (server == null || server.getConnectStatus() == 0) { + return JSON.toJSONString(Result.success(new ArrayList<>())); + } + + Map params = Collections.singletonMap("boardId", boardId); + return getFromServer(server, "/universaltestsoftware_server_api/common/getCommunicationChannelPath", params); + } + + @Override + public String refressBoardInformation(String serverId) { + ServerInfo server = serverMapper.selectById(serverId); + if (server == null || server.getConnectStatus() == 0) { + return JSON.toJSONString(Result.success(new ArrayList<>())); + } + return getFromServer(server, "/universaltestsoftware_server_api/common/refressBoardInformation", new HashMap<>()); + } + + @Override + public String getChannelTypeAll(String serverId, String boardTypeId) { + ServerInfo server = serverMapper.selectById(serverId); + if (server == null || server.getConnectStatus() == 0) { + return JSON.toJSONString(Result.success(new ArrayList<>())); + } + Map params = new HashMap<>(); + params.put("boardTypeId", boardTypeId); + return getFromServer(server, "/universaltestsoftware_server_api/outer/client/getChannelTypeAll", params); + } + + @Override + public String getBoardTemperature(BoardTemperatureDto dto) { + ServerInfo server = serverMapper.selectById(dto.getServerId()); + if (server == null || server.getConnectStatus() == 0) { + return JSON.toJSONString(Result.success()); + } + try { + BoardTemperatureServerDto serverDto = new BoardTemperatureServerDto(); + serverDto.setServerId(dto.getServerId()); + serverDto.setBoardId(dto.getBoardId()); + serverDto.setBoardIndex(Integer.parseInt(dto.getBoardIndex())); + return postToServer(server, "/universaltestsoftware_server_api/outer/client/getBoardTemperature", serverDto); + } catch (Exception e) { + return JSON.toJSONString(Result.success()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/BootLogServiceImpl.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/BootLogServiceImpl.java new file mode 100644 index 0000000..79132e4 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/BootLogServiceImpl.java @@ -0,0 +1,80 @@ +package com.cbsd.universaltestsoftware_client.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.cbsd.universaltestsoftware_client.dto.BootLogDeleteDto; +import com.cbsd.universaltestsoftware_client.dto.BootLogPageDto; +import com.cbsd.universaltestsoftware_client.entity.BootLog; +import com.cbsd.universaltestsoftware_client.mapper.BootLogMapper; +import com.cbsd.universaltestsoftware_client.service.IBootLogService; +import com.cbsd.universaltestsoftware_client.util.Result; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + *

+ * 服务实现类 + *

+ * + * @author wt + * @since 2025-09-03 + */ +@Service +public class BootLogServiceImpl extends ServiceImpl implements IBootLogService { + + + @Override + public Result> bootLogPage(BootLogPageDto bootLogPageDto) { + + if (bootLogPageDto.getLogType() == null || (bootLogPageDto.getLogType() != 1 && bootLogPageDto.getLogType() != 2)){ + bootLogPageDto.setLogType(1); + } + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.orderByDesc(BootLog::getTime) + .like(bootLogPageDto.getLogType() == 1, BootLog::getLogClass, "com.cbsd") + .notLike(bootLogPageDto.getLogType() == 2, BootLog::getLogClass, "com.cbsd") + .like(StringUtils.isNotEmpty(bootLogPageDto.getKeyword()), BootLog::getMessage, bootLogPageDto.getKeyword()) + .eq(StringUtils.isNotEmpty(bootLogPageDto.getLevel()), BootLog::getLevel, bootLogPageDto.getLevel()) + .between(StringUtils.isNotEmpty(bootLogPageDto.getEndTime()) && StringUtils.isNotEmpty(bootLogPageDto.getStartTime()), BootLog::getTime, bootLogPageDto.getStartTime(), bootLogPageDto.getEndTime()); + return Result.success(baseMapper.selectPage(bootLogPageDto, queryWrapper)); + } + + @Override + @Transactional + public Result bootLogDelete(BootLogDeleteDto bootLogDeleteDto) { + // 1. 统一校验:时间范围为空 且 无ID/批量ID/日志级别筛选时,直接返回参数错误 + boolean hasNoIdFilter = bootLogDeleteDto.getId() == null + && (bootLogDeleteDto.getListId() == null || bootLogDeleteDto.getListId().isEmpty()); + boolean hasNoLevelFilter = StringUtils.isEmpty(bootLogDeleteDto.getLevel()); + boolean hasNoTimeRange = StringUtils.isEmpty(bootLogDeleteDto.getStartTime()) + || StringUtils.isEmpty(bootLogDeleteDto.getEndTime()); + + // 核心校验逻辑:无任何筛选条件(ID/批量ID/级别 都没有,且时间范围也没有)→ 不允许删除 + if (hasNoIdFilter && hasNoLevelFilter && hasNoTimeRange) { + return Result.error("请选择要删除的日志(需指定ID/批量ID/日志级别或时间范围)"); + } + + // 2. 构建查询条件:使用LambdaQueryWrapper链式调用,条件判断更清晰,增加空集合安全校验 + LambdaQueryWrapper deleteQueryWrapper = new LambdaQueryWrapper() + // 单ID筛选:ID不为null时生效 + .eq(bootLogDeleteDto.getId() != null, BootLog::getId, bootLogDeleteDto.getId()) + // 批量ID筛选:集合不为null且非空时生效(避免空集合导致SQL语法错误) + .in(bootLogDeleteDto.getListId() != null && !bootLogDeleteDto.getListId().isEmpty(), + BootLog::getId, bootLogDeleteDto.getListId()) + // 日志级别筛选:级别不为空时生效 + .eq(StringUtils.isNotEmpty(bootLogDeleteDto.getLevel()), + BootLog::getLevel, bootLogDeleteDto.getLevel()) + // 时间范围筛选:开始/结束时间都不为空时生效 + .between(!hasNoTimeRange, BootLog::getTime, + bootLogDeleteDto.getStartTime(), bootLogDeleteDto.getEndTime()); + + // 3. 执行删除并返回结果:直接用变量接收删除行数,结果判断更简洁 + int deletedCount = baseMapper.delete(deleteQueryWrapper); + return deletedCount > 0 + ? Result.success("共删除:" + deletedCount + "条数据") + : Result.error("删除失败(无匹配日志或已被删除)"); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ChannelDataHitServiceImpl.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ChannelDataHitServiceImpl.java new file mode 100644 index 0000000..3338ae1 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ChannelDataHitServiceImpl.java @@ -0,0 +1,22 @@ +package com.cbsd.universaltestsoftware_client.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.cbsd.universaltestsoftware_client.entity.ChannelDataHit; +import com.cbsd.universaltestsoftware_client.service.ChannelDataHitService; +import com.cbsd.universaltestsoftware_client.mapper.ChannelDataHitMapper; +import org.springframework.stereotype.Service; + +/** +* @author SanJie +* @description 针对表【channel_data_hit(通道协议命中次数)】的数据库操作Service实现 +* @createDate 2026-01-23 16:24:09 +*/ +@Service +public class ChannelDataHitServiceImpl extends ServiceImpl + implements ChannelDataHitService{ + +} + + + + diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ChannelDataServiceImpl.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ChannelDataServiceImpl.java new file mode 100644 index 0000000..f2a8651 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ChannelDataServiceImpl.java @@ -0,0 +1,491 @@ +package com.cbsd.universaltestsoftware_client.service.impl; + +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.cbsd.universaltestsoftware_client.dto.ChannelDataHistoryDto; +import com.cbsd.universaltestsoftware_client.dto.ChannelDataStatisticsDto; +import com.cbsd.universaltestsoftware_client.dto.PageChannelDataDto; +import com.cbsd.universaltestsoftware_client.dto.ServerPageChannelDataDto; +import com.cbsd.universaltestsoftware_client.entity.*; +import com.cbsd.universaltestsoftware_client.exception.BaseException; +import com.cbsd.universaltestsoftware_client.mapper.*; +import com.cbsd.universaltestsoftware_client.nettyclient.connection.ConnectionManager; +import com.cbsd.universaltestsoftware_client.parser.model.FiledParseData; +import com.cbsd.universaltestsoftware_client.service.ChannelDataService; +import com.cbsd.universaltestsoftware_client.service.SystemConfigService; +import com.cbsd.universaltestsoftware_client.util.DateUtils; +import com.cbsd.universaltestsoftware_client.util.Result; +import com.cbsd.universaltestsoftware_client.vo.ChannelDataStatisticsVo; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.StringUtils; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +@Service +@Log4j2 +public class ChannelDataServiceImpl extends ServiceImpl implements ChannelDataService { + + @Resource + private ParserMapper parserMapper; + + @Resource + private ParserProtocolMapper parserProtocolMapper; + + @Resource + private InstructMapper instructMapper; + + @Resource + private DataBoardChannelMapper dataBoardChannelMapper; + + @Resource + private ServerMapper serverMapper; + + @Resource + private ConnectionManager connectionManager; + + @Resource + private ChannelDataTrafficMapper channelDataTrafficMapper; + + @Resource + private ChannelDataHitMapper channelDataHitMapper; + + @Resource + private SystemConfigService systemConfigService; + + @Override + @Async + public void start(PageChannelDataDto pageChannelDataDto) { + + HashMap serverByDataBoardId = getServerByDataBoardId(pageChannelDataDto.getDataBoardId()); + List serverId = (List) serverByDataBoardId.get("serverInfos"); + + if (CollectionUtils.isNotEmpty(serverId)) { + //远程服务代码 + + for (ServerInfo serverInfo : serverId) { + try { + ServerInfo server = serverMapper.selectById(serverInfo.getServerId()); + if (server == null) continue; + ServerPageChannelDataDto serverPageChannelDataDto = new ServerPageChannelDataDto(); + List channelIds = (List) serverByDataBoardId.get("channelIds"); + serverPageChannelDataDto.setChannelIds(channelIds); + serverPageChannelDataDto.setDataBoardId(pageChannelDataDto.getDataBoardId()); + serverPageChannelDataDto.setPageSize(pageChannelDataDto.getPageSize()); + if (StringUtils.isNotEmpty(pageChannelDataDto.getStartTime())) { + serverPageChannelDataDto.setStartTime(DateUtils.stringToTimestamp(pageChannelDataDto.getStartTime())); + } + if (StringUtils.isNotEmpty(pageChannelDataDto.getEndTime())) { + serverPageChannelDataDto.setEndTime(DateUtils.stringToTimestamp(pageChannelDataDto.getEndTime())); + } + + String url = String.format("http://%s:%s/universaltestsoftware_server_api/outer/client/start", + server.getIpAddress(), server.getHttpPort()); + String result = HttpRequest.post(url) + .header("Content-Type", "application/json") + .body(JSONUtil.toJsonStr(serverPageChannelDataDto)) + .execute() + .body(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + } + + + } + + @Override + @Async + public void stop(String dataBoardId) { + //远程调用 + HashMap serverByDataBoardId = getServerByDataBoardId(dataBoardId); + List serverId = (List) serverByDataBoardId.get("serverInfos"); + if (CollectionUtils.isNotEmpty(serverId)) { + try { + HashMap hashMap = new HashMap<>(); + hashMap.put("dataBoardId", dataBoardId); + for (ServerInfo serverInfo : serverId) { + ServerInfo server = serverMapper.selectById(serverInfo.getServerId()); + if (server == null) continue; + String url = String.format("http://%s:%s/universaltestsoftware_server_api/outer/client/stop", + server.getIpAddress(), server.getHttpPort()); + String s = HttpUtil.get(url, hashMap); + + } + connectionManager.removeOriginalData(dataBoardId); + + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @Override + public void updateData(PageChannelDataDto pageChannelDataDto) { + // 更新分页参数 + HashMap serverByDataBoardId = getServerByDataBoardId(pageChannelDataDto.getDataBoardId()); + List serverId = (List) serverByDataBoardId.get("serverInfos"); + + if (CollectionUtils.isNotEmpty(serverId)) { + //远程服务代码 + try { + for (ServerInfo serverInfo : serverId) { + ServerInfo server = serverMapper.selectById(serverInfo.getServerId()); + if (server == null) continue; + ServerPageChannelDataDto serverPageChannelDataDto = new ServerPageChannelDataDto(); + List channelIds = (List) serverByDataBoardId.get("channelIds"); + serverPageChannelDataDto.setChannelIds(channelIds); + serverPageChannelDataDto.setPageSize(pageChannelDataDto.getPageSize()); + serverPageChannelDataDto.setDataBoardId(pageChannelDataDto.getDataBoardId()); + if (StringUtils.isNotEmpty(pageChannelDataDto.getStartTime())) { + serverPageChannelDataDto.setStartTime(DateUtils.stringToTimestamp(pageChannelDataDto.getStartTime())); + } + if (StringUtils.isNotEmpty(pageChannelDataDto.getEndTime())) { + serverPageChannelDataDto.setEndTime(DateUtils.stringToTimestamp(pageChannelDataDto.getEndTime())); + } + String url = String.format("http://%s:%s/universaltestsoftware_server_api/outer/client/updateData", + server.getIpAddress(), server.getHttpPort()); + String result = HttpRequest.post(url) + .header("Content-Type", "application/json") + .body(JSONUtil.toJsonStr(serverPageChannelDataDto)) + .execute() + .body(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + } + + @Override + public String channelDataQuery(ChannelDataHistoryDto channelDataHistoryDto) { + ServerInfo server = serverMapper.selectById(channelDataHistoryDto.getServerId()); + if (server == null) { + return JSON.toJSONString(Result.success()); + } + if (server.getConnectStatus() == 0) { + return JSON.toJSONString(Result.success(new ArrayList<>())); + } + return postToServer(server, "/universaltestsoftware_server_api/outer/client/channelDataQuery", channelDataHistoryDto); + + } + + @Override + public String channelDataDelete(ChannelDataHistoryDto channelDataHistoryDto) { + ServerInfo server = serverMapper.selectById(channelDataHistoryDto.getServerId()); + if (server == null) { + return JSON.toJSONString(Result.success()); + } + if (server.getConnectStatus() == 0) { + return JSON.toJSONString(Result.success(new ArrayList<>())); + } + return postToServer(server, "/universaltestsoftware_server_api/outer/client/channelDataDelete", channelDataHistoryDto); + } + + /** + * 构造服务端请求基础URL + */ + private String buildUrl(ServerInfo server, String path) { + return String.format("http://%s:%s%s", server.getIpAddress(), server.getHttpPort(), path); + } + + /** + * 获取指定channelId的最新数据 + * + * @param channelId 通道id + */ + public Map getChannelLatestData(String channelId) { + Map map = new HashMap<>(); + if (StringUtils.isNotBlank(channelId)) { + List parserList = parserMapper.selectList(new LambdaQueryWrapper().eq(Parser::getChannelId, channelId)); + if (parserList != null && !parserList.isEmpty()) { + List parserIdList = parserList.stream().map(Parser::getParserId).collect(Collectors.toList()); + List protocolList = parserProtocolMapper.selectList(new LambdaQueryWrapper().in(ParserProtocol::getParserId, parserIdList)); + if (protocolList != null && !protocolList.isEmpty()) { + List protocolIdList = new ArrayList<>(); + for (ParserProtocol parserProtocol : protocolList) { + if (!protocolIdList.contains(parserProtocol.getProtocolId())) { + protocolIdList.add(parserProtocol.getProtocolId()); + } + } + for (String protocolId : protocolIdList) { + ChannelData data = baseMapper.selectOne(new LambdaQueryWrapper() + .eq(ChannelData::getChannelId, channelId) + .eq(ChannelData::getProtocolId, protocolId) + .orderByDesc(ChannelData::getTime) + .last("LIMIT 1")); + if (data != null && StringUtils.isNotBlank(data.getData())) { + try { + List filedParseDataList = JSON.parseArray(data.getData(), FiledParseData.class); + for (FiledParseData filedParseData : filedParseDataList) { + map.computeIfAbsent(filedParseData.getLabel(), k -> filedParseData.getPhyValue()); + } + } catch (Exception e) { + + } + } + } + } + } + } + return map; + } + + /** + * 通用POST请求封装 + */ + private String postToServer(ServerInfo server, String path, Object body) { + try { + String url = buildUrl(server, path); + String result = HttpRequest.post(url) + .header("Content-Type", "application/json") + .body(JSONUtil.toJsonStr(body)) + .execute() + .body(); + if (com.baomidou.mybatisplus.core.toolkit.StringUtils.isBlank(result)) { + throw new RuntimeException("服务端返回为空"); + } + return result; + } catch (Exception e) { + log.error("请求服务端失败, path={}, error={}", path, e.getMessage()); + throw new RuntimeException("服务端连接失败"); + } + } + + + //通过看板id获取服务端 + public HashMap getServerByDataBoardId(String dataBoardId) { + HashMap map = new HashMap<>(); + //查询这个看板有哪些通道id + LambdaQueryWrapper dataBoardChannelLambdaQueryWrapper = new LambdaQueryWrapper<>(); + dataBoardChannelLambdaQueryWrapper.eq(DataBoardChannel::getDataBoardId, dataBoardId) + .groupBy(DataBoardChannel::getChannelId) + ; + List dataBoardChannelList = dataBoardChannelMapper.selectList(dataBoardChannelLambdaQueryWrapper); + List channelIds = dataBoardChannelList.stream().map(DataBoardChannel::getChannelId).collect(Collectors.toList()); + if (CollectionUtils.isNotEmpty(channelIds)) { + //查询这个通道有那些服务端 + LambdaQueryWrapper instructLambdaQueryWrapper = new LambdaQueryWrapper<>(); + instructLambdaQueryWrapper.in(Instruct::getChannelId, channelIds) + .groupBy(Instruct::getServerId) + ; + List instructList = instructMapper.selectList(instructLambdaQueryWrapper); + + List serverIds = instructList.stream().map(Instruct::getServerId).collect(Collectors.toList()); + if (CollectionUtils.isEmpty(serverIds)) { + return map; + } + List serverInfos = serverMapper.selectList(new LambdaQueryWrapper().in(ServerInfo::getServerId, serverIds)); + map.put("serverInfos", serverInfos); + map.put("channelIds", channelIds); + + } + return map; + } + + @Override + public Result> getChannelDataStatistics(ChannelDataStatisticsDto dto) { + if (StringUtils.isBlank(dto.getDataBoardId())) { + return Result.error("数据看板id不能为空"); + } + + List channelIdList = dataBoardChannelMapper.getChannelList(dto.getDataBoardId()); + if (channelIdList == null || channelIdList.isEmpty()) { + return Result.success(new ArrayList<>()); + } + List resultList = new ArrayList<>(); + channelIdList.forEach(channelId -> { + ChannelDataStatisticsVo vo = new ChannelDataStatisticsVo(); + vo.setChannelId(channelId); + resultList.add(vo); + }); + + //通道数据流量 + List channelDataTrafficList = channelDataTrafficMapper.selectList(new LambdaQueryWrapper().in(ChannelDataTraffic::getChannelId, channelIdList)); + Map channelDataTrafficMap = null; + if (channelDataTrafficList != null && !channelDataTrafficList.isEmpty()) { + channelDataTrafficMap = channelDataTrafficList.stream() + .collect(Collectors.toMap(ChannelDataTraffic::getChannelId, channelDataTraffic -> channelDataTraffic)); + } + //查询通道的解析器 + List parserList = parserMapper.selectList(new LambdaQueryWrapper().in(Parser::getChannelId, channelIdList)); + //通道协议map + Map> channelProtocolMap = new HashMap<>(); + if (parserList != null && !parserList.isEmpty()) { + //通道解析器map + Map> channelParserMap = parserList.stream().collect(Collectors.groupingBy(Parser::getChannelId)); + List parserIdList = parserList.stream().map(Parser::getParserId).collect(Collectors.toList()); + List allParserProtocolList = parserProtocolMapper.selectList(new LambdaQueryWrapper() + .in(ParserProtocol::getParserId, parserIdList) + .orderByAsc(ParserProtocol::getCreateTime)); + //解析器协议map + Map> parserProtocolMap = allParserProtocolList.stream().collect(Collectors.groupingBy(ParserProtocol::getParserId)); + + for (String channelId : channelIdList) { + List channelParserList = channelParserMap.get(channelId); + if (channelParserList != null && !channelParserList.isEmpty()) { + List protocolIdList = new ArrayList<>(); + for (Parser parser : channelParserList) { + List parserProtocolList = parserProtocolMap.get(parser.getParserId()); + if (parserProtocolList != null && !parserProtocolList.isEmpty()) { + for (ParserProtocol parserProtocol : parserProtocolList) { + protocolIdList.add(parserProtocol.getProtocolId()); + } + } + } + channelProtocolMap.put(channelId, protocolIdList); + } + } + + } + + for (ChannelDataStatisticsVo vo : resultList) { + //通道数据流量 + if (channelDataTrafficMap != null && channelDataTrafficMap.get(vo.getChannelId()) != null) { + vo.setDataByteCount(channelDataTrafficMap.get(vo.getChannelId()).getByteCount()); + } else { + vo.setDataByteCount(0); + } + List protocolStatisticsList = new ArrayList<>(); + + List channelProtocolIdList = channelProtocolMap.get(vo.getChannelId()); + if (channelProtocolIdList != null && !channelProtocolIdList.isEmpty()) { + for (String protocolId : channelProtocolIdList) { + ChannelDataStatisticsVo.ProtocolStatisticsVo protocolStatisticsVo = new ChannelDataStatisticsVo.ProtocolStatisticsVo(); + protocolStatisticsVo.setProtocolId(protocolId); + ChannelDataHit dataHit = channelDataHitMapper.selectOne(new LambdaQueryWrapper() + .eq(ChannelDataHit::getChannelId, vo.getChannelId()) + .eq(ChannelDataHit::getProtocolId, protocolId) + .last("LIMIT 1")); + protocolStatisticsVo.setParseCount(dataHit != null ? dataHit.getHitCount() : 0); + protocolStatisticsList.add(protocolStatisticsVo); + } + } + vo.setProtocolStatisticsList(protocolStatisticsList); + } + return Result.success(resultList); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public Result clearChannelDataStatistics(ChannelDataStatisticsDto dto) { + if (StringUtils.isBlank(dto.getChannelId())) { + return Result.error("通道id不能为空"); + } + try { + channelDataTrafficMapper.update(null, new LambdaUpdateWrapper() + .set(ChannelDataTraffic::getByteCount, 0) + .eq(ChannelDataTraffic::getChannelId, dto.getChannelId())); + //查询通道的解析器 + List parserList = parserMapper.selectList(new LambdaQueryWrapper().eq(Parser::getChannelId, dto.getChannelId())); + if (parserList != null && !parserList.isEmpty()) { + List parserIdList = parserList.stream().map(Parser::getParserId).collect(Collectors.toList()); + List allParserProtocolList = parserProtocolMapper.selectList(new LambdaQueryWrapper() + .in(ParserProtocol::getParserId, parserIdList)); + if (allParserProtocolList != null && !allParserProtocolList.isEmpty()) { + List protocolIdList = allParserProtocolList.stream().map(ParserProtocol::getProtocolId).collect(Collectors.toList()); + channelDataHitMapper.update(null, new LambdaUpdateWrapper() + .set(ChannelDataHit::getHitCount, 0) + .eq(ChannelDataHit::getChannelId, dto.getChannelId()) + .in(ChannelDataHit::getProtocolId, protocolIdList)); + } + } + } catch (Exception e) { + e.printStackTrace(); + throw new BaseException("清除失败"); + } + + return Result.success(1); + } + + @Override + public void clearChannelData() { + // 异步通知服务端清理数据(不阻塞主线程) + CompletableFuture.runAsync(() -> { + System.out.println("-----------开始清理数据----------"); + try { + String dataClearLimitStr = systemConfigService.getSystemConfig(2); + if (StringUtils.isNotBlank(dataClearLimitStr)) { + int dataClearLimit = Integer.parseInt(dataClearLimitStr); + if (dataClearLimit > 0) { + List serverInfos = serverMapper.selectList( + new LambdaQueryWrapper() + .eq(ServerInfo::getConnectStatus, 1) + .eq(ServerInfo::getIsDelete, "0") + ); + for (ServerInfo serverInfo : serverInfos) { + try { + // 只有在线的服务端才通知 + if (serverInfo.getConnectStatus() == 1) { + System.out.println("【异步】通知服务端清理数据: " + serverInfo.getIpAddress()); + HashMap hashMap = new HashMap<>(); + hashMap.put("clearCount", dataClearLimit); + String url = String.format( + "http://%s:%s/universaltestsoftware_server_api/outer/client/channelDataClear", + serverInfo.getIpAddress(), + serverInfo.getHttpPort() + ); + String s = HttpUtil.get(url, hashMap); // 注意:确保 HttpUtil.get 是线程安全的 + } + } catch (Exception e) { + e.printStackTrace(); + } + } + checkDataLimitToClear(dataClearLimit); + log.info("【channel_data】数据清理完成"); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + }); + } + + private void checkDataLimitToClear(int dataClearLimit) { + long count = count(); + long deleteCount = count - dataClearLimit; + System.out.println("【异步】数据超出限制,需要清理数据数量: " + deleteCount); + if (deleteCount > 0) { + // 1. 分批删除,每批1000条 + long batchSize = 1000; + long totalDeleted = 0; + //清除数据 + while (totalDeleted < deleteCount) { + try { + long currentBatchSize = Math.min(batchSize, deleteCount - totalDeleted); + + // 方法1:直接删除(小批量) + baseMapper.delete(new LambdaQueryWrapper() + .orderByAsc(ChannelData::getTime) + .last("LIMIT " + currentBatchSize)); + + totalDeleted += currentBatchSize; + + // 可选:每次批次后短暂休眠,减少对数据库的压力 + if (totalDeleted < deleteCount) { + Thread.sleep(10); // 10毫秒 + } + } catch (Exception ignored) { + + } + } + } + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ChannelDataTrafficServiceImpl.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ChannelDataTrafficServiceImpl.java new file mode 100644 index 0000000..a536e1b --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ChannelDataTrafficServiceImpl.java @@ -0,0 +1,22 @@ +package com.cbsd.universaltestsoftware_client.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.cbsd.universaltestsoftware_client.entity.ChannelDataTraffic; +import com.cbsd.universaltestsoftware_client.service.ChannelDataTrafficService; +import com.cbsd.universaltestsoftware_client.mapper.ChannelDataTrafficMapper; +import org.springframework.stereotype.Service; + +/** +* @author SanJie +* @description 针对表【channel_data_traffic(通道流量统计)】的数据库操作Service实现 +* @createDate 2026-01-23 10:07:19 +*/ +@Service +public class ChannelDataTrafficServiceImpl extends ServiceImpl + implements ChannelDataTrafficService{ + +} + + + + diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/DataBoardChannelServiceImpl.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/DataBoardChannelServiceImpl.java new file mode 100644 index 0000000..6f50ded --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/DataBoardChannelServiceImpl.java @@ -0,0 +1,20 @@ +package com.cbsd.universaltestsoftware_client.service.impl; + +import com.cbsd.universaltestsoftware_client.entity.DataBoardChannel; +import com.cbsd.universaltestsoftware_client.mapper.DataBoardChannelMapper; +import com.cbsd.universaltestsoftware_client.service.DataBoardChannelService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author admin + * @since 2025-09-10 + */ +@Service +public class DataBoardChannelServiceImpl extends ServiceImpl implements DataBoardChannelService { + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/DataBoardServiceImpl.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/DataBoardServiceImpl.java new file mode 100644 index 0000000..75b012e --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/DataBoardServiceImpl.java @@ -0,0 +1,759 @@ +package com.cbsd.universaltestsoftware_client.service.impl; + +import cn.hutool.http.HttpRequest; +import cn.hutool.json.JSONUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.cbsd.universaltestsoftware_client.dto.*; +import com.cbsd.universaltestsoftware_client.entity.*; +import com.cbsd.universaltestsoftware_client.exception.BaseException; +import com.cbsd.universaltestsoftware_client.mapper.*; +import com.cbsd.universaltestsoftware_client.parser.model.Filed; +import com.cbsd.universaltestsoftware_client.parser.model.FiledParseData; +import com.cbsd.universaltestsoftware_client.parser.model.ProtocolXml; +import com.cbsd.universaltestsoftware_client.service.DataBoardChannelService; +import com.cbsd.universaltestsoftware_client.service.DataBoardService; +import com.cbsd.universaltestsoftware_client.util.DateUtils; +import com.cbsd.universaltestsoftware_client.util.Result; +import com.cbsd.universaltestsoftware_client.util.XmlToObjectConverter; +import com.cbsd.universaltestsoftware_client.vo.ChannelDataHistoryVo; +import com.opencsv.CSVWriter; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +/** + *

+ * 数据看板 服务实现类 + *

+ * + * @author admin + * @since 2025-09-10 + */ +@Service +public class DataBoardServiceImpl extends ServiceImpl implements DataBoardService { + + @Resource + private ParserMapper parserMapper; + + @Resource + private ParserProtocolMapper parserProtocolMapper; + + @Resource + private ProtocolMapper protocolMapper; + + @Resource + private DataBoardChannelMapper dataBoardChannelMapper; + + + @Resource + private ChannelDataMapper channelDataMapper; + + + @Resource + private DataBoardChannelService dataBoardChannelService; + + @Resource + private ServerMapper serverMapper; + + + @Resource + private DownloadMapper downloadMapper; + + @Value("${cbsd.downloadUrl}") + private String downloadDir; + + + List colorList = Arrays.asList( + "#5470c6", "#91cc75", "#fac858", "#ee6666", "#73c0de", "#3ba272", "#fc8452", "#9a60b4", "#8798cc", + "#75ccc3", "#faa458", "#e87bec", "#55dc98", "#5db548", "#c39d70", "#b977f1", "#f7a5ef", "#c0c447", + "#f59472", "#f9868d", "#9cd8de", "#26c8cb", "#d5b9ff", "#39aae5", "#eda18a", "#24a5a7", "#9db9cd", + "#fe73d8", "#74d9ff", "#fbbead", "#eee965", "#c7dfe8" + ); + + /** + * 构造服务端请求基础URL + */ + private String buildUrl(ServerInfo server, String path) { + return String.format("http://%s:%s%s", server.getIpAddress(), server.getHttpPort(), path); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void saveOrUpdateDataBoard(InsertDataBoardDto insertDataBoardDto) throws Exception { + DataBoard dataBoard = new DataBoard(); + dataBoard.setDataBoardName(insertDataBoardDto.getDataBoardName()); + dataBoard.setDataBoardType(insertDataBoardDto.getDataBoardType()); + dataBoard.setCreateTime(DateUtils.dateToString()); + dataBoard.setDataType(insertDataBoardDto.getDataType()); + if (StringUtils.isNotBlank(insertDataBoardDto.getDataBoardId())) { + dataBoard.setDataBoardId(insertDataBoardDto.getDataBoardId()); + baseMapper.updateById(dataBoard); + //清除所有通道 + //查询所有通道 + if (CollectionUtils.isEmpty(insertDataBoardDto.getChannelList())) { + List dataBoardChannels = dataBoardChannelMapper.selectList(new LambdaUpdateWrapper().eq(DataBoardChannel::getDataBoardId, insertDataBoardDto.getDataBoardId()) + .groupBy(DataBoardChannel::getChannelId) + ); + ArrayList channelDtos = new ArrayList<>(); + for (DataBoardChannel dataBoardChannel : dataBoardChannels) { + ChannelDto channelDto = new ChannelDto(); + channelDto.setChannelId(dataBoardChannel.getChannelId()); + channelDto.setChannelName(dataBoardChannel.getChannelName()); + channelDtos.add(channelDto); + + } + insertDataBoardDto.setChannelList(channelDtos); + } + + dataBoardChannelMapper.update(null, new UpdateWrapper().set("protocol_id", null) + .set("protocol_name", null) + .eq("data_board_id", insertDataBoardDto.getDataBoardId()) + ); + + + } else { + + if (StringUtils.isBlank(insertDataBoardDto.getDataBoardName()) || insertDataBoardDto.getDataBoardType() == null) { + throw new Exception("数据看板名字和类型不能为空"); + } + if (CollectionUtils.isEmpty(insertDataBoardDto.getChannelList())) { + throw new Exception("数据看板通道不能为空"); + } + baseMapper.insert(dataBoard); + } + for (ChannelDto channelDto : insertDataBoardDto.getChannelList()) { + //参数列表 + List parsers = parserMapper.selectList(new LambdaUpdateWrapper().eq(Parser::getChannelId, channelDto.getChannelId())); + //查询这个解析器有那些协议 + if (CollectionUtils.isNotEmpty(parsers)) { + List parserIds = parsers.stream().map(Parser::getParserId).distinct().collect(Collectors.toList()); + List parserProtocols = parserProtocolMapper.selectList(new LambdaUpdateWrapper().in(ParserProtocol::getParserId, parserIds)); + if (CollectionUtils.isNotEmpty(parserProtocols)) { + List protocolIds = parserProtocols.stream().map(ParserProtocol::getProtocolId).distinct().collect(Collectors.toList()); + List protocols = protocolMapper.selectList(new LambdaUpdateWrapper().in(Protocol::getProtocolId, protocolIds)); + //解析协议内容的参数 + // List protocolContents = protocols.stream().map(Protocol::getProtocolContent).distinct().collect(Collectors.toList()); + for (Protocol protocol : protocols) { + HashMap parameter = new HashMap<>(); + List dataBoardChannelList = new ArrayList<>(); + if (StringUtils.isEmpty(protocol.getProtocolContent())) { + continue; + } + ProtocolXml protocolXml = XmlToObjectConverter.convertXmlToObject(protocol.getProtocolContent(), ProtocolXml.class); + + List filedList = protocolXml.getFileds(); + //根据sn排序升序 + filedList.sort(Comparator.comparingInt(Filed::getSnInt)); + for (Filed filed : filedList) { + //判断字段类型是否为解析字段 +// if (filed.getType().equalsIgnoreCase(ProtocolEnum.VAR.getValue())) { +// String readName = filed.getReadName(); +// if (StringUtils.isNotBlank(readName)) { +// parameter.put(readName, filed.getSnInt()); +// } +// +// } + + String readName = filed.getReadName(); + if (StringUtils.isNotBlank(readName)) { + parameter.put(readName, filed.getSnInt()); + } + } + //循环Map参数 并帮颜色循坏赋值,如果颜色不够则又从头循环 + int j = 0; + for (Map.Entry entry : parameter.entrySet()) { + if (j >= colorList.size()) { + j = 0; + } + String s = entry.getKey(); + int i = entry.getValue(); + DataBoardChannel dataBoardChannel = new DataBoardChannel(); + //查看是是否存在 + DataBoardChannel dataBoardChannel1 = dataBoardChannelMapper. + selectOne(new LambdaUpdateWrapper().eq(DataBoardChannel::getDataBoardId, dataBoard.getDataBoardId()) + .eq(DataBoardChannel::getChannelId, channelDto.getChannelId()) + .eq(DataBoardChannel::getProtocolId, protocol.getProtocolId()) +// .eq(DataBoardChannel::getParameter, s) + ); + if (dataBoardChannel1 != null) { + dataBoardChannel = dataBoardChannel1; + } + dataBoardChannel.setDataBoardId(dataBoard.getDataBoardId()); + dataBoardChannel.setChannelId(channelDto.getChannelId()); + dataBoardChannel.setChannelName(channelDto.getChannelName()); + dataBoardChannel.setParameter(s); + dataBoardChannel.setSn(i); + dataBoardChannel.setProtocolId(protocol.getProtocolId()); + dataBoardChannel.setProtocolName(protocol.getProtocolName()); + dataBoardChannel.setLegendColor(colorList.get(j)); + + dataBoardChannelList.add(dataBoardChannel); + j++; + } + + if (!dataBoardChannelList.isEmpty()) { + int batchSize = 1000; + int total = dataBoardChannelList.size(); + if (total > batchSize) { + int insertTimes = total / batchSize; + int lastSize = total % batchSize; + if (lastSize > 0) { + insertTimes += 1; + } + for (int i = 0; i < insertTimes; i++) { + List insertList; + if (lastSize > 0 && i == insertTimes - 1) { + insertList = dataBoardChannelList.subList(i * batchSize, i * batchSize + lastSize); + } else { + insertList = dataBoardChannelList.subList(i * batchSize, i * batchSize + batchSize); + } + dataBoardChannelService.saveOrUpdateBatch(insertList); + } + } else { + dataBoardChannelService.saveOrUpdateBatch(dataBoardChannelList); + } + } else { + throw new BaseException("通道无有效协议或协议中无参数字段"); + } + } + } + } + + } + + + } + + @Override + public void removeDataBoardById(String dataBoardId) { + baseMapper.deleteById(dataBoardId); + dataBoardChannelMapper.delete(new LambdaUpdateWrapper().eq(DataBoardChannel::getDataBoardId, dataBoardId)); + } + + @Override + public List listDataBoard(Integer dataType) { + List dataBoardList = baseMapper.selectList(new LambdaUpdateWrapper().eq(DataBoard::getDataType, dataType) + .orderByAsc(DataBoard::getCreateTime) + ); + for (DataBoard dataBoard : dataBoardList) { + List dataBoardChannelList = dataBoardChannelMapper.selectList(new LambdaUpdateWrapper() + .eq(DataBoardChannel::getDataBoardId, dataBoard.getDataBoardId()) + .groupBy(DataBoardChannel::getChannelId) + ); + + for (DataBoardChannel dataBoardChannel : dataBoardChannelList) { + //查询这个通道里有多少协议 + List protocolIds = dataBoardChannelMapper.selectList(new LambdaUpdateWrapper() + .eq(DataBoardChannel::getChannelId, dataBoardChannel.getChannelId()) + .eq(DataBoardChannel::getDataBoardId, dataBoard.getDataBoardId()) + .groupBy(DataBoardChannel::getProtocolId) + ); + ArrayList protocolNameListDtos = new ArrayList<>(); + + for (DataBoardChannel protocolId : protocolIds) { + if (StringUtils.isEmpty(protocolId.getProtocolId())) { + continue; + } + ProtocolNameListDto protocolNameListDto = new ProtocolNameListDto(); + List parameterList = dataBoardChannelMapper.selectList(new LambdaUpdateWrapper() + .eq(DataBoardChannel::getChannelId, dataBoardChannel.getChannelId()) + .eq(DataBoardChannel::getDataBoardId, dataBoard.getDataBoardId()) + .eq(DataBoardChannel::getProtocolId, protocolId.getProtocolId()) + .orderByAsc(DataBoardChannel::getSn) + ); + BeanUtils.copyProperties(dataBoardChannel, protocolNameListDto); + protocolNameListDto.setProtocolId(protocolId.getProtocolId()); + protocolNameListDto.setProtocolName(protocolId.getProtocolName()); + protocolNameListDto.setParameterList(parameterList); + protocolNameListDtos.add(protocolNameListDto); + } + dataBoardChannel.setProtocolNameList(protocolNameListDtos); + } + + dataBoard.setDataBoardChannelList(dataBoardChannelList); + } + return dataBoardList; + } + + @Override + public void updateDataBoardById(DataBoardChannel dataBoardChannel) { + dataBoardChannelMapper.updateById(dataBoardChannel); + } + + @Override + public Result exportDataBoard(ExportDataBoardDto dto, HttpServletResponse response) { + + // ====== 1. 基础参数校验 ====== + if (dto.getExportType() == null) { + return Result.error("请选择导出类型"); + } + DataBoard dataBoard = baseMapper.selectById(dto.getDataBoardId()); + if (dataBoard == null) { + return Result.error("看板不存在"); + } + String exportName = dataBoard.getDataBoardName(); + + // ====== 2. 初始化 Download 记录(不管有没有数据,先创建)====== + Download download = new Download(); + String downloadId = UUID.randomUUID().toString().replace("-", ""); + download.setDownloadId(downloadId); + download.setStartTime(dto.getStartTime()); + download.setEndTime(dto.getEndTime()); + download.setCreateTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + download.setState(1); // 初始状态:-1 表示“待处理/校验中” + download.setType(dto.getExportType()); + download.setChannelName(""); // 先空着,后面有数据再填 + download.setProtocolName(""); + download.setDownloadUrl(""); + downloadMapper.insert(download); + + // ====== 3. 异步执行整个流程(包括校验 + 导出)====== + new Thread(() -> { + try { + String channelName = ""; + String protocolName = ""; + boolean hasData = false; + + if (dto.getExportType() == 1) { + List channels = dataBoardChannelMapper.selectList(new LambdaQueryWrapper() + .eq(DataBoardChannel::getDataBoardId, dto.getDataBoardId()) + .in(CollectionUtils.isNotEmpty(dto.getParam()), DataBoardChannel::getParameter, dto.getParam()) + .in(CollectionUtils.isNotEmpty(dto.getProtocolIds()), DataBoardChannel::getProtocolId, dto.getProtocolIds()) + ); + + if (CollectionUtils.isNotEmpty(channels)) { + List channelIds = channels.stream().map(DataBoardChannel::getChannelId).collect(Collectors.toList()); + List protocolIds = channels.stream().map(DataBoardChannel::getProtocolId).collect(Collectors.toList()); + + List dataList = channelDataMapper.selectList(new LambdaQueryWrapper() + .between(ChannelData::getTime, dto.getStartTime(), dto.getEndTime()) + .in(CollectionUtils.isNotEmpty(channelIds), ChannelData::getChannelId, channelIds) + .in(CollectionUtils.isNotEmpty(protocolIds), ChannelData::getProtocolId, protocolIds) + ); + + if (CollectionUtils.isNotEmpty(dataList)) { + // 检查是否有匹配字段的有效数据 + for (ChannelData cd : dataList) { + if (StringUtils.isNotBlank(cd.getData())) { + List list = JSON.parseArray(cd.getData(), FiledParseData.class); + for (FiledParseData fpd : list) { + if (CollectionUtils.isEmpty(dto.getParam()) || dto.getParam().contains(fpd.getReadName())) { + hasData = true; + break; + } + } + if (hasData) break; + } + } + + if (hasData) { + channelName = channels.stream().map(DataBoardChannel::getChannelName).distinct().collect(Collectors.joining(",")); + protocolName = channels.stream().map(DataBoardChannel::getProtocolName).distinct().collect(Collectors.joining(",")); + } + } + } + + } else if (dto.getExportType() == 2) { + List channels = dataBoardChannelMapper.selectList(new LambdaQueryWrapper() + .eq(DataBoardChannel::getDataBoardId, dto.getDataBoardId()) + .in(CollectionUtils.isNotEmpty(dto.getParam()), DataBoardChannel::getParameter, dto.getParam()) + .in(CollectionUtils.isNotEmpty(dto.getProtocolIds()), DataBoardChannel::getProtocolId, dto.getProtocolIds()) + ); + + if (CollectionUtils.isNotEmpty(channels)) { + List channelIds = new ArrayList<>(); + + for (DataBoardChannel channel : channels) { + if (!channelIds.contains(channel.getChannelId())) { + channelIds.add(channel.getChannelId()); + } + } + if (CollectionUtils.isNotEmpty(channelIds)) { + ChannelDataQueryAll query = new ChannelDataQueryAll(); + query.setChannelIds(channelIds); + query.setStartTime(dto.getStartTime()); + query.setEndTime(dto.getEndTime()); + + List servers = serverMapper.selectList(new LambdaQueryWrapper() + .eq(ServerInfo::getConnectStatus, 1) + .eq(ServerInfo::getIsDelete, "0") + ); + + for (ServerInfo server : servers) { + try { + String resp = postToServer(server, "/universaltestsoftware_server_api/outer/client/channelDataQueryAll", query); + if (StringUtils.isNotBlank(resp) && !resp.equals("[]")) { + hasData = true; + break; + } + } catch (Exception ignored) { + } + } + + if (hasData) { + channelName = channels.stream().map(DataBoardChannel::getChannelName).distinct().collect(Collectors.joining(",")); + protocolName = channels.stream().map(DataBoardChannel::getProtocolName).distinct().collect(Collectors.joining(",")); + } + } + } + } + + // ====== 4. 根据是否有数据,决定后续操作 ====== + if (hasData) { + // 更新通道/协议名 + Download updateNames = new Download(); + updateNames.setDownloadId(downloadId); + updateNames.setChannelName(channelName); + updateNames.setProtocolName(protocolName); + downloadMapper.updateById(updateNames); + + // 执行导出 + LocalDateTime dt = LocalDateTime.parse(download.getCreateTime(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + String timeStr = dt.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + String finalExportName = exportName + "_" + timeStr; + + doExportInBackground(finalExportName, downloadId, dto); + // doExportInBackground 内部会把 state 更新为 2 + } else { + // 无数据:直接标记为“无数据”状态(比如 state = -1) + Download noDataUpdate = new Download(); + noDataUpdate.setDownloadId(downloadId); + noDataUpdate.setState(-1); // -1 表示“无数据” + downloadMapper.updateById(noDataUpdate); + } + + } catch (Exception e) { + // 异常:标记为失败 + e.printStackTrace(); + Download failUpdate = new Download(); + failUpdate.setDownloadId(downloadId); + failUpdate.setState(-2); // -2 表示“系统错误” + downloadMapper.updateById(failUpdate); + } + }).start(); + + // ====== 5. 立即返回 downloadId 给前端 ====== + return Result.success(); + + + } + + @Override + public void clear(ExportDataBoardDto exportDataBoardDto) { + List dataBoardChannels = dataBoardChannelMapper.selectList(new LambdaQueryWrapper() + .eq(DataBoardChannel::getDataBoardId, exportDataBoardDto.getDataBoardId()) + .in(CollectionUtils.isNotEmpty(exportDataBoardDto.getParam()), DataBoardChannel::getParameter, exportDataBoardDto.getParam()) + .in(CollectionUtils.isNotEmpty(exportDataBoardDto.getProtocolIds()), DataBoardChannel::getProtocolId, exportDataBoardDto.getProtocolIds()) + + ); + if (CollectionUtils.isNotEmpty(dataBoardChannels)) { + List channelIds = new ArrayList<>(); + List protocolIds = new ArrayList<>(); + for (DataBoardChannel dataBoardChannel : dataBoardChannels) { + if (!channelIds.contains(dataBoardChannel.getChannelId())) { + channelIds.add(dataBoardChannel.getChannelId()); + } + if (!protocolIds.contains(dataBoardChannel.getProtocolId())) { + protocolIds.add(dataBoardChannel.getProtocolId()); + } + } + + channelDataMapper.delete(new LambdaQueryWrapper() + .in(CollectionUtils.isNotEmpty(channelIds), ChannelData::getChannelId, channelIds) + .in(CollectionUtils.isNotEmpty(protocolIds), ChannelData::getProtocolId, protocolIds) + .between(ChannelData::getTime, exportDataBoardDto.getStartTime(), exportDataBoardDto.getEndTime())); + + //异步远程调用清理服务端数据 + CompletableFuture.runAsync(() -> { + List servers = serverMapper.selectList(new LambdaQueryWrapper() + .eq(ServerInfo::getConnectStatus, 1) + .eq(ServerInfo::getIsDelete, "0") + ); + DeleteChannelDto deleteChannelDto = new DeleteChannelDto(); + deleteChannelDto.setChannelIds(channelIds); + deleteChannelDto.setStartTime(exportDataBoardDto.getStartTime()); + deleteChannelDto.setEndTime(exportDataBoardDto.getEndTime()); + for (ServerInfo server : servers) { + try { + String resp = postToServer(server, "/universaltestsoftware_server_api/outer/client/deleteByIds", deleteChannelDto); + System.out.println("清除服务端数据结果:" + resp); + } catch (Exception ignored) { + } + } + }); + } + + } + + @Override + public Result> exportHistory(DownloadDto downloadDto) { + LambdaUpdateWrapper dataBoardLambdaUpdateWrapper = new LambdaUpdateWrapper<>(); + dataBoardLambdaUpdateWrapper.between(StringUtils.isNotBlank(downloadDto.getStartTime()) && StringUtils.isNotBlank(downloadDto.getEndTime()), Download::getCreateTime, + downloadDto.getStartTime(), downloadDto.getEndTime()) + .eq(downloadDto.getState() != null, Download::getState, downloadDto.getState()) + .eq(downloadDto.getType() != null, Download::getType, downloadDto.getType()) + .and(StringUtils.isNotBlank(downloadDto.getKeyword()), + e -> e + .like(Download::getChannelName, downloadDto.getKeyword()) + .or() + .like(Download::getProtocolName, downloadDto.getKeyword()) + ) + .orderByDesc(Download::getCreateTime) + ; + DownloadDto downloadDto1 = downloadMapper.selectPage(downloadDto, dataBoardLambdaUpdateWrapper); + + return Result.success(downloadDto1); + + } + + private String postToServer(ServerInfo server, String path, Object body) { + try { + String url = buildUrl(server, path); + String result = HttpRequest.post(url) + .timeout(30 * 1000) + .setConnectionTimeout(30 * 1000) + .setReadTimeout(60 * 1000) + .charset(StandardCharsets.UTF_8) // ⭐ 核心:统一 UTF-8 + .header("Content-Type", "application/json;charset=UTF-8") + .body(JSONUtil.toJsonStr(body)) + .execute() + .body(); + if (StringUtils.isBlank(result)) { + throw new RuntimeException("服务端返回为空"); + } + return result; + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("服务端连接失败"); + } + } + + /** + * 后台真实执行导出的方法(异步调用) + */ + private void doExportInBackground(String exportName, String downloadId, ExportDataBoardDto dto) { + File dir = new File(downloadDir); + if (!dir.exists()) { + dir.mkdirs(); + } + + String exportFileName = ""; + + boolean exportSuccess = false; + + //20260208 需求:导出原数据时,将原数据拼接保存为.dat文件 + if (dto.getExportType() == 2) { + //原数据 + exportFileName = exportName + ".dat"; + File file = new File(dir, exportFileName); + try { + handleExportSourceData(dto, file); + exportSuccess = true; + } catch (Exception e) { + + } + } else if (dto.getExportType() == 1) { + //解析数据 + exportFileName = exportName + ".csv"; + File file = new File(dir, exportFileName); + + try (FileOutputStream fos = new FileOutputStream(file); + OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8)) { + // 写入 BOM,避免 Excel 打开中文乱码 + osw.write('\ufeff'); + try (CSVWriter writer = new CSVWriter(osw)) { + handleExportParseData(dto, writer); + } + exportSuccess = true; + } catch (Exception e) { + // 记录错误(实际项目建议用 log.error) + e.printStackTrace(); + // 注意:当前设计只在成功时更新状态;失败时 state 仍为 1(生成中) + } + } + + // 导出成功,更新 Download 记录为“已完成” + Download update = new Download(); + update.setDownloadId(downloadId); + update.setState(exportSuccess ? 2 : -1); // 2 = 已完成 + update.setDownloadUrl(exportSuccess ? exportFileName : null); // 前端可通过此路径下载 + downloadMapper.updateById(update); + } + + /** + * 处理导出类型1:解析字段导出 + */ + private void handleExportParseData(ExportDataBoardDto dto, CSVWriter writer) { + List channels = dataBoardChannelMapper.selectList(new LambdaQueryWrapper() + .eq(DataBoardChannel::getDataBoardId, dto.getDataBoardId()) + .in(CollectionUtils.isNotEmpty(dto.getParam()), DataBoardChannel::getParameter, dto.getParam()) + .in(CollectionUtils.isNotEmpty(dto.getProtocolIds()), DataBoardChannel::getProtocolId, dto.getProtocolIds()) + ); + + List channelIds = channels.stream().map(DataBoardChannel::getChannelId).collect(Collectors.toList()); + List protocolIds = channels.stream().map(DataBoardChannel::getProtocolId).collect(Collectors.toList()); + + List dataList = channelDataMapper.selectList(new LambdaQueryWrapper() + .between(ChannelData::getTime, dto.getStartTime(), dto.getEndTime()) + .in(CollectionUtils.isNotEmpty(channelIds), ChannelData::getChannelId, channelIds) + .in(CollectionUtils.isNotEmpty(protocolIds), ChannelData::getProtocolId, protocolIds) + .orderByAsc(ChannelData::getTime) + ); + + // 构建映射关系 + Map channelIdToName = new HashMap<>(); + Map protocolIdToName = new HashMap<>(); + for (DataBoardChannel c : channels) { + channelIdToName.put(c.getChannelId(), c.getChannelName()); + protocolIdToName.put(c.getProtocolId(), c.getProtocolName()); + } + + // 写表头 +// writer.writeNext(new String[]{"通道", "协议", "数据字段", "源数据", "物理解析值", "物理单位", "时间"}); + + boolean headerHasWritten = false; + + List header = new ArrayList<>(); + List content = new ArrayList<>(); + // 写数据行 + for (ChannelData cd : dataList) { +// if (StringUtils.isBlank(cd.getData())) continue; +// List parseList = JSON.parseArray(cd.getData(), FiledParseData.class); +// for (FiledParseData fpd : parseList) { +// if (CollectionUtils.isNotEmpty(dto.getParam()) && !dto.getParam().contains(fpd.getReadName())) { +// continue; +// } +// String[] row = { +// orEmpty(channelIdToName.get(cd.getChannelId())), +// orEmpty(protocolIdToName.get(cd.getProtocolId())), +// orEmpty(fpd.getReadName()), +// orEmpty(fpd.getSourceValue()), +// orEmpty(fpd.getPhyValue()), +// orEmpty(fpd.getPhyUnit()), +// orEmpty(cd.getTime()) +// }; +// writer.writeNext(row); +// } + if (!headerHasWritten) { + header.add("时间"); + header.add("协议"); + header.add("通道"); + } + content.add(orEmpty(cd.getTime())); + content.add(orEmpty(protocolIdToName.get(cd.getProtocolId()))); + content.add(orEmpty(channelIdToName.get(cd.getChannelId()))); + + if (StringUtils.isBlank(cd.getData())) continue; + List parseList = JSON.parseArray(cd.getData(), FiledParseData.class); + for (FiledParseData fpd : parseList) { + if (CollectionUtils.isNotEmpty(dto.getParam()) && !dto.getParam().contains(fpd.getReadName())) { + continue; + } + if (!headerHasWritten) { + if (dto.getParseDataType() == 1 || dto.getParseDataType() == 3) { + header.add(fpd.getReadName() + "(源码)"); + } + if (dto.getParseDataType() == 2 || dto.getParseDataType() == 3) { + header.add(fpd.getReadName() + "(解析值)"); + } + } + if (dto.getParseDataType() == 1 || dto.getParseDataType() == 3) { + content.add(orEmpty(fpd.getSourceValue())); + } + if (dto.getParseDataType() == 2 || dto.getParseDataType() == 3) { + content.add(orEmpty(fpd.getPhyValue())); + } + } + + if (!headerHasWritten) { + headerHasWritten = true; + String[] headerArray = header.toArray(new String[0]); + writer.writeNext(headerArray); + header.clear(); + } + String[] contentArray = content.toArray(new String[0]); + writer.writeNext(contentArray); + content.clear(); + } + } + + /** + * 处理导出类型2:原始内容导出 + */ + private void handleExportSourceData(ExportDataBoardDto dto, File exportFile) throws IOException { + List channels = dataBoardChannelMapper.selectList(new LambdaQueryWrapper() + .eq(DataBoardChannel::getDataBoardId, dto.getDataBoardId()) + .in(CollectionUtils.isNotEmpty(dto.getParam()), DataBoardChannel::getParameter, dto.getParam()) + .in(CollectionUtils.isNotEmpty(dto.getProtocolIds()), DataBoardChannel::getProtocolId, dto.getProtocolIds()) + ); + + List channelIds = new ArrayList<>(); + + for (DataBoardChannel channel : channels) { + if (!channelIds.contains(channel.getChannelId())) { + channelIds.add(channel.getChannelId()); + } + } + + // 查询远程服务器数据 + List allData = new ArrayList<>(); + ChannelDataQueryAll query = new ChannelDataQueryAll(); + query.setChannelIds(channelIds); + query.setStartTime(dto.getStartTime()); + query.setEndTime(dto.getEndTime()); + + List servers = serverMapper.selectList(new LambdaQueryWrapper() + .eq(ServerInfo::getConnectStatus, 1) + .eq(ServerInfo::getIsDelete, "0") + ); + + for (ServerInfo server : servers) { + try { + String resp = postToServer(server, "/universaltestsoftware_server_api/outer/client/channelDataQueryAll", query); + if (StringUtils.isNotBlank(resp)) { + List list = JSON.parseArray(resp, ChannelDataHistoryVo.class); + allData.addAll(list); + } + } catch (Exception e) { + e.printStackTrace(); // 或记录日志 + } + } + + StringBuilder sb = new StringBuilder(); + for (ChannelDataHistoryVo vo : allData) { + sb.append(vo.getContentString()); + } + Files.write(Paths.get(exportFile.getPath()), sb.toString().getBytes(StandardCharsets.UTF_8)); + } + + /** + * 工具方法:null 转空字符串 + */ + private String orEmpty(String str) { + return str == null ? "" : str; + } + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/DownloadServiceImpl.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/DownloadServiceImpl.java new file mode 100644 index 0000000..9ce7007 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/DownloadServiceImpl.java @@ -0,0 +1,20 @@ +package com.cbsd.universaltestsoftware_client.service.impl; + +import com.cbsd.universaltestsoftware_client.entity.Download; +import com.cbsd.universaltestsoftware_client.mapper.DownloadMapper; +import com.cbsd.universaltestsoftware_client.service.DownloadService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author admin + * @since 2026-02-03 + */ +@Service +public class DownloadServiceImpl extends ServiceImpl implements DownloadService { + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/InstructServiceImpl.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/InstructServiceImpl.java new file mode 100644 index 0000000..7b82814 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/InstructServiceImpl.java @@ -0,0 +1,1557 @@ +package com.cbsd.universaltestsoftware_client.service.impl; + +import cn.hutool.http.HttpUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.cbsd.client.check.ICheckInterface; +import com.cbsd.client.pretreatment.CalculateUtils; +import com.cbsd.client.pretreatment.IPreTreatmentInterface; +import com.cbsd.universaltestsoftware_client.dto.*; +import com.cbsd.universaltestsoftware_client.entity.Instruct; +import com.cbsd.universaltestsoftware_client.entity.Invocation; +import com.cbsd.universaltestsoftware_client.entity.Protocol; +import com.cbsd.universaltestsoftware_client.entity.ServerInfo; +import com.cbsd.universaltestsoftware_client.exception.BaseException; +import com.cbsd.universaltestsoftware_client.mapper.InstructMapper; +import com.cbsd.universaltestsoftware_client.mapper.InvocationMapper; +import com.cbsd.universaltestsoftware_client.mapper.ProtocolMapper; +import com.cbsd.universaltestsoftware_client.mapper.ServerMapper; +import com.cbsd.universaltestsoftware_client.nettyclient.connection.ConnectionManager; +import com.cbsd.universaltestsoftware_client.nettyclient.entity.SendTcpContent; +import com.cbsd.universaltestsoftware_client.parser.model.Filed; +import com.cbsd.universaltestsoftware_client.parser.model.ProtocolXml; +import com.cbsd.universaltestsoftware_client.script.ScriptManager; +import com.cbsd.universaltestsoftware_client.service.InstructService; +import com.cbsd.universaltestsoftware_client.util.*; +import com.cbsd.universaltestsoftware_client.vo.BoardListVo; +import com.cbsd.universaltestsoftware_client.vo.ChannelListVo; +import com.cbsd.universaltestsoftware_client.vo.ServerListVo; +import com.cbsd.universaltestsoftware_client.websocket.command.CommandRunMessage; +import com.cbsd.universaltestsoftware_client.websocket.command.CommandWebSocketHandler; +import io.reactivex.rxjava3.core.Observable; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 指令服务实现类 + * 提供指令的增删改查、执行、排序、树形结构构建等功能 + */ +@Service +@Slf4j +public class InstructServiceImpl extends ServiceImpl implements InstructService { + + @Resource + private ServerMapper serverMapper; + + @Resource + private ConnectionManager connectionManager; + + @Resource + private ProtocolMapper protocolMapper; + + @Resource + private InvocationMapper invocationMapper; + + @Resource + private ChannelDataServiceImpl channelDataService; + + /** + * 指令文件路径 + */ + @Value("${cbsd.commandRootPath}") + private String commandRootPath; + + + /** + * 分页查询指令树 + */ + @Override + public Page pageList(InstructPageDto dto) { + Page page = baseMapper.selectPage(dto, new LambdaQueryWrapper() + .select(Instruct.class, i -> !i.getProperty().equals("protocolContent")) + .like(StringUtils.isNotBlank(dto.getKeyword()), Instruct::getInstructName, dto.getKeyword()) + .eq(Instruct::getInvocationId, dto.getInvocationId()) + .orderByAsc(Instruct::getSort)); + + // 构建树形结构 + List tree = buildTree(page.getRecords(), "0"); + page.setRecords(tree); + + return page; + } + + /** + * 执行指令:异步发送 TCP 报文 + */ + public Result execute(String instructId, CommandRunMessage runMessage) { + Instruct instruct = baseMapper.selectById(instructId); + if (instruct == null) return Result.error("指令不存在"); + + ServerInfo server = serverMapper.selectById(instruct.getServerId()); + if (server == null) { + log.info("指令执行失败, 指令名称: {}, 原因: 服务端不存在", instruct.getInstructName()); + return Result.error("服务端不存在"); + } + if (server.getConnectStatus() != 1) { + log.info("指令执行失败, 指令名称: {}, 原因: 服务端未连接", instruct.getInstructName()); + return Result.error("服务端未连接"); + } + //获取状态 + Map serverConnectionStatus = connectionManager.getServerConnectionStatus(); + + Boolean b = serverConnectionStatus.get(server.getServerId()); + if (b == null) { + log.info("指令执行失败, 指令名称: {}, 原因: 服务端以已断线", instruct.getInstructName()); + return Result.error("服务端以已断线"); + } + + String protocolXml = Optional.ofNullable(instruct.getProtocolId()) + .map(pid -> protocolMapper.selectById(pid)) + .map(Protocol::getProtocolContent) + .orElse(null); + + SendTcpContent.OperationType opType = + instruct.getOperationType() == 0 ? SendTcpContent.OperationType.DATA : + instruct.getOperationType() == 1 ? SendTcpContent.OperationType.REGISTER : null; + + // 异步发送 + doSend(instruct, server, protocolXml, opType, runMessage); + + return Result.success("指令已提交执行"); + } + + /** + * 真正发送逻辑,捕获异常并记录 + */ + private void doSend(Instruct instruct, ServerInfo server, String protocolXml, + SendTcpContent.OperationType opType, CommandRunMessage runMessage) { + try { + String command = instruct.getInstructText(); + String extraData = instruct.getExtraData(); + if (opType == SendTcpContent.OperationType.DATA) { + //操作类型为数据时 才处理指令数据文件和预处理脚本 + if (StringUtils.isBlank(command)) { + //判断指令文件是否为空 + if (StringUtils.isNotBlank(instruct.getInstructDataFile())) { + String dataFileContent = readInstructDataFile(instruct.getInstructDataFile()); + if (StringUtils.isNotBlank(dataFileContent)) { + command = dataFileContent; + } + } + } + //预处理脚本 + if (StringUtils.isNotBlank(command) && StringUtils.isNotBlank(instruct.getPreProcessScriptName())) { + String pretreatmentCommand = preTreatmentInstructCommand(command, instruct); + if (StringUtils.isNotBlank(pretreatmentCommand)) { + command = pretreatmentCommand; + } + } + } else if (opType == SendTcpContent.OperationType.REGISTER) { + //操作类型为寄存器 + // 20260304 特殊处理1553B板卡 添加消息和更新消息时 读取指令数据文件且预处理 复制给 额外参数中的data + if ("WT_PCI642_1553B".equals(instruct.getBoardTypeName()) && StringUtils.isNotBlank(instruct.getInstructDataFile())) { + //messageType=0&messageId=11&cmd1RtAddress=2&cmd1SubAddress=11&gapTime=3000000&reTry=true&isCycle=false&data=AAAA + Map extraDataMap = ConverUtils.convertStringToMap(instruct.getExtraData()); + if ((command.equals("bc_add_message") || command.equals("bc_message_update"))) { + if (extraDataMap.containsKey("data")) { + String extraDataData = extraDataMap.get("data"); + //读取指令数据文件 + String dataFileContent = readInstructDataFile(instruct.getInstructDataFile()); + if (StringUtils.isNotBlank(dataFileContent)) { + extraDataData = dataFileContent; + } + //预处理 + if (StringUtils.isNotBlank(instruct.getPreProcessScriptName())) { + String pretreatmentExtraData = preTreatmentInstructCommand(extraDataData, instruct); + if (StringUtils.isNotBlank(pretreatmentExtraData)) { + extraDataData = pretreatmentExtraData; + } + } + extraDataMap.put("data", extraDataData); + + } + } else if (command.equals("rt_start_transceiver_measurement_data")) { + //收发信机测量数据,读取指令数据文件 + String measureData = readInstructDataFile(instruct.getInstructDataFile()); + //预处理 + if (StringUtils.isNotBlank(measureData) && StringUtils.isNotBlank(instruct.getPreProcessScriptName())) { + String pretreatmentMeasureData = preTreatmentInstructCommand(measureData, instruct); + if (StringUtils.isNotBlank(pretreatmentMeasureData)) { + measureData = pretreatmentMeasureData; + } + } + extraDataMap.put("measureData", measureData); + } + extraData = ConverUtils.convertMapToString(extraDataMap); + } + + } + if (StringUtils.isNotBlank(command)) { + //操作类型为数据时判断指令数据是否需要拆分 + if (opType == SendTcpContent.OperationType.DATA && instruct.getDataSplittingLength() != null && instruct.getDataSplittingLength() > 0) { + List splitDataList = SplitUtils.splitStringByLength(command, instruct.getDataSplittingLength() * 2); + if (splitDataList.size() > 1) { + splitIndex = 0; + if (splitSend(splitDataList, instruct, server, protocolXml, opType, extraData)) { + afterSend(instruct, runMessage); + } + } else { + connectionManager.sendMessageToServer( + server.getServerId(), + command, + instruct.getChannelId(), + server.getTcpPort(), + opType, + protocolXml, + extraData); + afterSend(instruct, runMessage); + } + } else { + connectionManager.sendMessageToServer( + server.getServerId(), + command, + instruct.getChannelId(), + server.getTcpPort(), + opType, + protocolXml, + extraData); + afterSend(instruct, runMessage); + } + } else { + if (runMessage != null) { + //向前端推送 + runMessage.setInstruct(instruct); + runMessage.setCommandStatus(4); + broadcast(JSON.toJSONString(runMessage)); + } + } + + + } catch (Exception e) { + log.error("发送指令失败, 指令名称:{}, reason: {}", instruct.getInstructName(), e.getMessage()); + } + } + + /** + * 发送完成 + */ + private void afterSend(Instruct instruct, CommandRunMessage runMessage) { + try { + // 更新命中次数 + String hintTime = DateUtils.dateToString(); + UpdateWrapper uw = new UpdateWrapper<>(); + uw.eq("instruct_id", instruct.getInstructId()) + // 数据库层面的原子累加,避免丢失更新 + .setSql("hit_count = hit_count + 1") + .set("last_hit_time", hintTime); + baseMapper.update(null, uw); + + if (runMessage != null) { + //向前端推送 + instruct.setHitCount(instruct.getHitCount() + 1); + instruct.setLastHitTime(hintTime); + runMessage.setInstruct(instruct); + runMessage.setCommandStatus(3); + broadcast(JSON.toJSONString(runMessage)); + } + + //执行检查脚本 + if (StringUtils.isNotBlank(instruct.getCheckScriptName())) { + String originalCommand = instruct.getInstructText(); + if (StringUtils.isBlank(originalCommand)) { + //判断指令文件是否为空 + if (StringUtils.isNotBlank(instruct.getInstructDataFile())) { + File commandFile = new File(commandRootPath, instruct.getInstructDataFile()); + if (commandFile.exists()) { + try { + originalCommand = String.join("", Files.readAllLines(commandFile.toPath())); + } catch (Exception e) { + //按照二进制读取 + byte[] bytes = Files.readAllBytes(commandFile.toPath()); + originalCommand = DataUtils.byteToHex(bytes); + } + } + } + } + + Map latestDataMap = channelDataService.getChannelLatestData(instruct.getChannelId()); + + String preTreatmentCommand = originalCommand; + //获取预处理脚本 + if (StringUtils.isNotBlank(instruct.getPreProcessScriptName())) { + IPreTreatmentInterface preTreatmentInterface = (IPreTreatmentInterface) ScriptManager.getScriptClass(ScriptManager.ScriptType.IPreTreatmentInterface.getValue(), instruct.getPreProcessScriptName()).newInstance(); + String pretreatmentResult = preTreatmentInterface.pretreatment(originalCommand, instruct.getRepeatCount(), instruct.getHitCount(), instruct.getLastHitTime(), latestDataMap, new CalculateUtils()); + if (StringUtils.isNotBlank(pretreatmentResult)) { + preTreatmentCommand = pretreatmentResult; + } + } + + ICheckInterface checkInterface = (ICheckInterface) ScriptManager.getScriptClass(ScriptManager.ScriptType.ICheckInterface.getValue(), instruct.getCheckScriptName()).newInstance(); + List logList = checkInterface.check(originalCommand, preTreatmentCommand, instruct.getRepeatCount(), instruct.getHitCount(), instruct.getLastHitTime(), latestDataMap); + if (logList != null && !logList.isEmpty()) { + for (String logInfo : logList) { + log.info(logInfo); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + private int splitIndex = 0; + + /** + * 拆分发送指令 + */ + private boolean splitSend(List splitDataList, Instruct instruct, ServerInfo server, String protocolXml, + SendTcpContent.OperationType opType, String extraData) { + String hexMessage = splitDataList.get(splitIndex); + connectionManager.sendMessageToServer( + server.getServerId(), + hexMessage, + instruct.getChannelId(), + server.getTcpPort(), + opType, + protocolXml, + extraData); + if (instruct.getTailTime() != null && instruct.getTailTime() > 0) { + CountDownLatch latch = new CountDownLatch(1); + Observable.timer(instruct.getTailTime(), TimeUnit.MILLISECONDS).doOnComplete(latch::countDown).subscribe(); + try { + latch.await(); // 等待直到计时器完成或超时(如果有的话) + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + if (splitIndex < splitDataList.size() - 1) { + splitIndex++; + return splitSend(splitDataList, instruct, server, protocolXml, opType, extraData); + } + return true; + } + + /** + * 读取指令数据文件内容 + * + * @param instructDataFile 指令数据文件 + * @return 文件内容 + */ + private String readInstructDataFile(String instructDataFile) { + String fileContent = null; + File commandFile = new File(commandRootPath, instructDataFile); + if (commandFile.exists()) { + try { + fileContent = String.join("", Files.readAllLines(commandFile.toPath())); + } catch (Exception e) { + //按照二进制读取 + try { + byte[] bytes = Files.readAllBytes(commandFile.toPath()); + fileContent = DataUtils.byteToHex(bytes); + } catch (Exception ignored) { + + } + } + } + return fileContent; + } + + /** + * 预处理指令 + * + * @param originalCommand 原指令 + * @param instruct 指令对象 + * @return 处理后的内容 + */ + private String preTreatmentInstructCommand(String originalCommand, Instruct instruct) { + try { + IPreTreatmentInterface preTreatmentInterface = (IPreTreatmentInterface) ScriptManager.getScriptClass(ScriptManager.ScriptType.IPreTreatmentInterface.getValue(), instruct.getPreProcessScriptName()).newInstance(); + //查询通道数据 + Map latestDataMap = channelDataService.getChannelLatestData(instruct.getChannelId()); + return preTreatmentInterface.pretreatment(originalCommand, instruct.getRepeatCount(), instruct.getHitCount(), instruct.getLastHitTime(), latestDataMap, new CalculateUtils()); + } catch (Exception e) { + return null; + } + } + + /** + * 获取所有已连接服务端及其板卡通道 + */ + @Override + public Result> getChannelList(String keyword) { + List servers = serverMapper.selectList( + new LambdaQueryWrapper() + .eq(ServerInfo::getIsDelete, "0") + .eq(ServerInfo::getConnectStatus, 1)); + ArrayList serverInfos = new ArrayList<>(); + //获取状态 + Map serverConnectionStatus = connectionManager.getServerConnectionStatus(); + servers.forEach(server -> { + //获取这个服务端是否连接成功,成功则加入 + if (CollectionUtils.isNotEmpty(serverConnectionStatus)) { + Boolean b = serverConnectionStatus.get(server.getServerId()); + if (b != null) { + if (b) { + serverInfos.add(server); + } + } + } + }); + + + List vos = serverInfos.parallelStream() + .map(server -> fetchBoardList(server, keyword)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + //取出所有板卡 + List serverListVos = new ArrayList<>(); + + for (ServerListVo vo : vos) { + ServerListVo serverListVo = new ServerListVo(); + serverListVo.setServerId(vo.getServerId()); + serverListVo.setServerName(vo.getServerName()); + serverListVo.setIpAddress(vo.getIpAddress()); + serverListVo.setTcpPort(vo.getTcpPort()); + serverListVo.setHttpPort(vo.getHttpPort()); + ArrayList newBoardListVo = new ArrayList<>(); + List boardListVos = vo.getBoardListVos(); + for (BoardListVo boardListVo : boardListVos) { + List boardListVos1 = boardListVo.getBoardListVos(); + if (CollectionUtils.isNotEmpty(boardListVos1)) { + newBoardListVo.add(boardListVo); + } + } + serverListVo.setBoardListVos(newBoardListVo); + serverListVos.add(serverListVo); + + } + return Result.success(serverListVos); + } + + /** + * 远程调用服务端获取板卡列表 + */ + private ServerListVo fetchBoardList(ServerInfo server, String keyword) { + try { + String url = String.format("http://%s:%s/universaltestsoftware_server_api/outer/client/pageListBoardAndChannel", + server.getIpAddress(), server.getHttpPort()); + Map hashMap = new HashMap<>(); + hashMap.put("keyWord", keyword); + hashMap.put("onlineState", 1); + + String resp = HttpUtil.get(url, hashMap); + if (StringUtils.isBlank(resp)) return null; + + Result result = JSON.parseObject(resp, Result.class); + if (result.getCode() == 1) { + ServerListVo vo = new ServerListVo(); + BeanUtils.copyProperties(server, vo); + vo.setBoardListVos(JSON.parseArray(result.getData().toString(), BoardListVo.class)); + return vo; + } + } catch (Exception e) { + log.warn("获取板卡列表异常,serverId={}", server.getServerId(), e); + } + return null; + } + + /** + * 获取通道驱动函数名 + */ + @Override + public String getChannelFunctionName(String channelId, String serverId) { + ServerInfo server = serverMapper.selectById(serverId); + if (server == null) throw new RuntimeException("服务端不存在"); + + String url = String.format("http://%s:%s/universaltestsoftware_server_api/common/getBoardDriverRegisterFunctions", + server.getIpAddress(), server.getHttpPort()); + Map param = Collections.singletonMap("channelId", channelId); + try { + String resp = HttpUtil.get(url, param); + if (StringUtils.isBlank(resp)) throw new RuntimeException("连接失败"); + return resp; + } catch (Exception e) { + log.error("获取驱动函数名异常", e); + throw new RuntimeException("连接失败"); + } + } + + /** + * 保存或更新指令组(树形编码) + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Result saveOrUpdateInstructGroup(InstructGroupDto dto) { + if (StringUtils.isBlank(dto.getInstructId())) { + return createInstructGroup(dto); + } else { + Instruct exist = baseMapper.selectById(dto.getInstructId()); + if (exist == null) return Result.error("指令组不存在"); + exist.setInstructName(dto.getInstructName()); + exist.setSort(dto.getSort()); + try { + baseMapper.updateById(exist); + } catch (Exception e) { + if (e instanceof DuplicateKeyException) { + return Result.error("同一用例内指令名称重复"); + } + return Result.error("保存失败"); + } + return Result.success(); + } + } + + /** + * 新建指令组:自动生成编码、排序 + */ + private Result createInstructGroup(InstructGroupDto dto) { + Instruct instruct = new Instruct(); + instruct.setInstructName(dto.getInstructName()); + instruct.setSort(dto.getSort()); + instruct.setInvocationId(dto.getInvocationId()); + instruct.setInstructType(dto.getInstructType()); + Instruct olInstruct = null; + //最后一条数据的sort + int sort = nextRootSort(dto.getInvocationId()); + //判断是否新增在某一条指令的后面 + if (StringUtils.isNotBlank(dto.getInferiorId())) { + //查询当前得sort + olInstruct = baseMapper.selectById(dto.getInferiorId()); + if (olInstruct != null) { + instruct.setSort(olInstruct.getSort() + 1); + if (StringUtils.isEmpty(dto.getParentEncoding())) { + if (!olInstruct.getParentEncoding().equals("0")) { + dto.setParentEncoding(olInstruct.getParentEncoding()); + } + + } + + } else { + instruct.setSort(sort); + } + } else { + instruct.setSort(sort); + } + + if (StringUtils.isBlank(dto.getParentEncoding())) { + // 根节点 + String maxCode = getMaxEncoding("0", dto.getInvocationId()); + instruct.setEncoding(nextCode(maxCode)); + instruct.setParentEncoding("0"); + + if (instruct.getInstructType() == 1) { + instruct.setInstructName("新指令" + instruct.getEncoding()); + } else { + instruct.setInstructName("新指令组" + instruct.getEncoding()); + } + instruct.setInstructTag(instruct.getEncoding()); + } else { + // 子节点 + if (!existsEncoding(dto.getParentEncoding(), dto.getInvocationId())) { + return Result.error("父级编码不存在"); + } + String parent = dto.getParentEncoding(); + String maxCode = getMaxEncoding(parent, dto.getInvocationId()); + //001001 + instruct.setEncoding(parent + nextCode(maxCode)); + instruct.setParentEncoding(parent); + instruct.setParentName(getNameByEncoding(parent, dto.getInvocationId())); + + if (instruct.getInstructType() == 1) { + instruct.setInstructName("新指令" + instruct.getEncoding()); + } else { + instruct.setInstructName("新指令组" + instruct.getEncoding()); + } + instruct.setInstructTag(instruct.getEncoding()); + } + + try { + baseMapper.insert(instruct); + if (olInstruct != null) { + //大于等于它得sort+1,并且不等他它本身。 + baseMapper.shiftSortAfter(instruct.getInvocationId(), instruct.getSort(), instruct.getInstructId()); + } + //如果是新增指令组,在后面新增一条指令 + if (instruct.getInstructType() == 2) { + InstructGroupDto instructGroupDto = new InstructGroupDto(); + instructGroupDto.setInvocationId(instruct.getInvocationId()); + instructGroupDto.setInstructType(1); + instructGroupDto.setInferiorId(instruct.getInstructId()); + instructGroupDto.setParentEncoding(instruct.getEncoding()); + createInstructGroup(instructGroupDto); + } + } catch (Exception e) { + if (e instanceof DuplicateKeyException) { + return Result.error("同一用例内指令名称重复"); + } + return Result.error("保存失败"); + } + reOrder(dto.getInvocationId()); + return Result.success(); + } + + /** + * 更新指令 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public Result saveOrUpdateInstruct(Instruct instruct) { + if (StringUtils.isBlank(instruct.getInstructId())) { + throw new BaseException("指令id不能为空"); + } + if (StringUtils.isNotBlank(instruct.getProtocolId())) { + Protocol protocol = protocolMapper.selectById(instruct.getProtocolId()); + if (protocol != null) { + instruct.setProtocolName(protocol.getProtocolName()); + } + } + try { + baseMapper.updateById(instruct); + if (StringUtils.isNotBlank(instruct.getInstructName())) { + instruct = baseMapper.selectById(instruct.getInstructId()); + if (instruct.getInstructType() == 2) { + //指令组修改时,将组内的父级名字更改 + baseMapper.update(null, new LambdaUpdateWrapper() + .set(Instruct::getParentName, instruct.getInstructName()) + .eq(Instruct::getInvocationId, instruct.getInvocationId()) + .eq(Instruct::getParentEncoding, instruct.getEncoding()) + ); + } + } + + Integer status = instruct.getStatus(); + if (status != null) { + //修改的状态 + instruct = baseMapper.selectById(instruct.getInstructId()); + if (instruct.getInstructType() == 2) { + //指令组修改状态时,将组内的指令状态同步修改 + baseMapper.update(null, new LambdaUpdateWrapper() + .set(Instruct::getStatus, status) + .eq(Instruct::getInvocationId, instruct.getInvocationId()) + .likeRight(Instruct::getParentEncoding, instruct.getEncoding())); + } + } + + } catch (Exception e) { + if (e instanceof DuplicateKeyException) { + return Result.error("同一用例内指令名称重复"); + } + throw new BaseException("保存失败"); + } + reOrder(instruct.getInvocationId()); + return Result.success(); + } + + @Override + public Result moveUp(String instructId) { + Instruct current = baseMapper.selectById(instructId); + if (current == null || current.getSort() == null) { + return Result.error("指令不存在"); + } + Instruct prev = baseMapper.findPrevSibling(current.getSort(), current.getParentEncoding(), current.getInvocationId()); + if (prev == null) { + return Result.error("已经是第一个不能上移"); + } + + // 交换 sort + int tempSort = current.getSort(); + current.setSort(prev.getSort()); + prev.setSort(tempSort); + + baseMapper.updateSort(current); + baseMapper.updateSort(prev); + + reOrder(current.getInvocationId()); + + return Result.success(); + + } + + @Override + public Result moveDown(String instructId) { + Instruct current = baseMapper.selectById(instructId); + if (current == null || current.getSort() == null) { + return Result.error("指令不存在"); + } + + Instruct next = baseMapper.findNextSibling(current.getSort(), current.getParentEncoding(), current.getInvocationId()); + if (next == null) { + return Result.error("已经是最后一个不能下移"); // 已经是最后一个 + } + + // 交换 sort + int tempSort = current.getSort(); + current.setSort(next.getSort()); + next.setSort(tempSort); + + baseMapper.updateSort(current); + baseMapper.updateSort(next); + + reOrder(current.getInvocationId()); + + return Result.success(); + } + + /** + * 删除指令及其子节点 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Result deleteInstructById(InstructDeleteDto dto) { + if (dto == null) { + return Result.error("参数不能为空"); + } + if (StringUtils.isBlank(dto.getInvocationId())) { + return Result.error("用例id不能为空"); + } + if (CollectionUtils.isEmpty(dto.getInstructIds())) { + return Result.error("请选择要删除的指令"); + } + + // 2. 删除 + List instructs = baseMapper.selectList(new LambdaQueryWrapper() + .eq(Instruct::getInvocationId, dto.getInvocationId()) + .in(Instruct::getInstructId, dto.getInstructIds())); + baseMapper.deleteBatchIds(dto.getInstructIds()); + for (Instruct instruct : instructs) { + if (instruct.getInstructType() == 2) { + baseMapper.delete(new LambdaQueryWrapper() + .eq(Instruct::getInvocationId, dto.getInvocationId()) + .likeRight(Instruct::getParentEncoding, instruct.getEncoding())); + } else { + Long l = baseMapper.selectCount(new LambdaQueryWrapper() + .eq(Instruct::getInvocationId, dto.getInvocationId()) + .eq(Instruct::getParentEncoding, instruct.getParentEncoding())); + if (l == 0) { + baseMapper.delete(new LambdaQueryWrapper() + .eq(Instruct::getInvocationId, dto.getInvocationId()) + .eq(Instruct::getEncoding, instruct.getParentEncoding())); + } + } + } + // 3. 该 invocationId 下剩余节点全局重排 1,2,3... + baseMapper.reorderGlobalSortContinuous(dto.getInvocationId()); + return Result.success(); + } + + /** + * 获取板卡驱动额外数据 + */ + @Override + public String getBoardDriverExtraData(String channelId, String serverId) { + ServerInfo server = serverMapper.selectById(serverId); + if (server == null) throw new BaseException("服务端不存在"); + + String url = String.format("http://%s:%s/universaltestsoftware_server_api/common/getBoardDriverExtraData", + server.getIpAddress(), server.getHttpPort()); + try { + String resp = HttpUtil.get(url, Collections.singletonMap("channelId", channelId)); + if (StringUtils.isBlank(resp)) throw new RuntimeException("连接失败"); + return resp; + } catch (Exception e) { + log.error("获取驱动额外数据异常", e); + throw new RuntimeException("连接失败"); + } + } + + /** + * 解析协议参数 + */ + @Override + public Result> getInstructProtocolParam(String instructId) { + Instruct instruct = baseMapper.selectById(instructId); + if (instruct == null) throw new BaseException("指令不存在"); + + Protocol protocol = protocolMapper.selectById(instruct.getProtocolId()); + if (protocol == null || StringUtils.isBlank(protocol.getProtocolContent())) { + return Result.success(Collections.emptyList()); + } + + try { + ProtocolXml xml = XmlToObjectConverter.convertXmlToObject(protocol.getProtocolContent(), ProtocolXml.class); + if (xml.getFileds() != null && !xml.getFileds().isEmpty()) { + List list = xml.getFileds(); + list.sort(Comparator.comparingInt(Filed::getSnInt)); + return Result.success(list); + } + } catch (Exception e) { + log.error("协议解析失败,instructId={}", instructId, e); + } + return Result.success(Collections.emptyList()); + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public void createGroupFromList(CreateGroupDTO dto) { + /* 1. 校验:存在 + 同 invocationId */ + List roots = baseMapper.selectByIdsAndInvocation(dto.getInstructIds(), dto.getInvocationId()); + if (roots.size() < 2) throw new BaseException("至少选择 2 个节点"); + //如果他们是同级 + String parentEncoding = roots.get(0).getParentEncoding(); + boolean TJ = false; + long count = roots.stream().filter(e -> e.getParentEncoding().equals(parentEncoding)).count(); + if (count == roots.size()) { + TJ = true; + } + //查询所有的组id。 + List collect = roots.stream().filter(e -> e.getInstructType() == 2).map(Instruct::getEncoding).distinct().collect(Collectors.toList()); + //存在这个的组id编码移除 + roots.removeIf(e -> collect.contains(e.getParentEncoding())); + + /* 2. 创建新组(根级) */ + String parentEnc; + if (TJ) { + parentEnc = parentEncoding; + } else { + parentEnc = "0"; + } + String newGrpCode; + if (parentEnc.equals("0")) { + newGrpCode = nextCode(getMaxEncoding(parentEnc, dto.getInvocationId())); + } else { + newGrpCode = parentEnc + nextCode(getMaxEncoding(parentEnc, dto.getInvocationId())); + } + + int newGrpSort = getMaxSort(parentEnc, dto.getInvocationId()) + 1; + + Instruct group = new Instruct(); + group.setInstructType(2); + group.setInstructName("新组" + newGrpSort); + group.setInvocationId(dto.getInvocationId()); + group.setParentEncoding(parentEnc); + group.setParentName("无"); + group.setEncoding(newGrpCode); + group.setInstructTag(newGrpCode); + group.setSort(newGrpSort); + group.setStatus(1); + baseMapper.insert(group); + + for (Instruct root : roots) { + root.setParentEncoding(newGrpCode); + root.setParentName(group.getInstructName()); + baseMapper.updateById(root); + } + reOrder(dto.getInvocationId()); + } + + /** + * 重新排序 + * + * @param invocationId 用例id + */ + private void reOrder(String invocationId) { + List instructList = baseMapper.selectList(new LambdaQueryWrapper() + .eq(Instruct::getInvocationId, invocationId)); + // 构建树形结构 + List tree = buildTree(instructList, "0"); + + List saveList = new ArrayList<>(); + for (Instruct instruct : tree) { + saveList.add(instruct); + saveList.addAll(getInstructChildren(instruct)); + } + for (int i = 0; i < saveList.size(); i++) { + saveList.get(i).setSort(i + 1); + } + //保存 + if (!saveList.isEmpty()) { + saveOrUpdateBatch(saveList); + } + } + + private List getInstructChildren(Instruct instruct) { + List resultList = new ArrayList<>(); + List childList = instruct.getChildren(); + if (childList != null && !childList.isEmpty()) { + for (Instruct child : childList) { + resultList.add(child); + resultList.addAll(getInstructChildren(child)); + } + } + return resultList; + } + + + /** + * 批量复制 + * + * @param dto + */ + @Override + public void batchCopyBelow(BatchCopyBelowDTO dto) { + /* 1. 校验存在 & 同 invocationId */ + List roots = baseMapper.selectByIdsAndInvocation(dto.getInstructIds(), dto.getInvocationId()); + if (roots.size() != dto.getInstructIds().size()) + throw new BaseException("存在无效节点或跨用例"); + + //查询所有的组id。 + List collect = roots.stream().filter(e -> e.getInstructType() == 2).map(Instruct::getEncoding).distinct().collect(Collectors.toList()); + //存在这个的组id编码移除 + roots.removeIf(e -> collect.contains(e.getParentEncoding())); + + /* 3. 逐节点复制(单指令 or 整棵组) */ + for (Instruct src : roots) { + if (src.getInstructType() == 1) { + // 单指令:复制自身 + copySingleBelow(src, dto.getInvocationId()); + } else { + // 整组:复制根 + 完整子树 + copyWholeTreeBelow(src, dto.getInvocationId()); + } + } + reOrder(dto.getInvocationId()); + /* 4. 整个 invocationId 全局重排 1,2,3… */ +// baseMapper.reorderGlobalSortContinuous(dto.getInvocationId()); + + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void batchMoveBelow(BatchMoveBelowDTO dto) { + if (CollectionUtils.isEmpty(dto.getInstructs())) { + throw new BaseException("移动数据不能为空"); + } + //取出他的id + List ids = dto.getInstructs().stream().map(SortDto::getInstructId).collect(Collectors.toList()); + List roots = baseMapper.selectByIdsAndInvocation(ids, dto.getInvocationId()); + if (roots.size() != ids.size()) + throw new BaseException("存在无效节点或跨用例"); + //查询所有的组id。 + List collect = roots.stream().filter(e -> e.getInstructType() == 2).map(Instruct::getEncoding).distinct().collect(Collectors.toList()); + //存在这个的组id编码移除 + roots.removeIf(e -> collect.contains(e.getParentEncoding())); + + for (Instruct root : roots) { + Integer oldSort = root.getSort(); + //查询出他移动的序号 + int sort = dto.getInstructs().stream().filter(e -> e.getInstructId().equals(root.getInstructId())).findFirst().get().getSort(); + //查出他原来的序号是什么 + Instruct oldInstruct = baseMapper.selectOne(new LambdaQueryWrapper().eq(Instruct::getInvocationId, dto.getInvocationId()).eq(Instruct::getSort, sort).last("limit 1")); + if (oldInstruct != null) { + String oldEncoding = root.getEncoding(); + //更换他的父级 + root.setParentName(oldInstruct.getParentName()); + root.setParentEncoding(oldInstruct.getParentEncoding()); + //重新生成编码 + if (root.getParentEncoding().equals("0")) { + root.setEncoding(nextCode(getMaxEncoding(root.getParentEncoding(), dto.getInvocationId()))); + } else { + root.setEncoding(root.getParentEncoding() + nextCode(getMaxEncoding(root.getParentEncoding(), dto.getInvocationId()))); + } + //他的子集更换的父级 + UpdateWrapper instructUpdateWrapper = new UpdateWrapper<>(); + instructUpdateWrapper.set("parent_encoding", root.getEncoding()) + .set("parent_name", root.getInstructName()) + .eq("parent_encoding", oldEncoding); + + baseMapper.update(null, instructUpdateWrapper); + + + root.setSort(sort); + baseMapper.updateById(root); + //往后移,区间进一个不含本身 + if (sort > oldSort) { + baseMapper.minusSort(dto.getInvocationId(), oldSort, sort, root.getInstructId()); + } + //往前移动,移动到的位置后的人全部+1 + if (sort < oldSort) { + //区间的sort +1 + baseMapper.plusSort(dto.getInvocationId(), oldSort, sort, root.getInstructId()); + } + } + } + + reOrder(dto.getInvocationId()); + } + + private void copySingleBelow(Instruct src, String invocationId) { + Instruct copy = cloneNode(src); + //查询一边移动后sort + Instruct instruct = baseMapper.selectById(src.getInstructId()); + if (instruct == null) { + return; + } + copy.setSort(instruct.getSort() + 1); // 紧挨下方 + copy.setParentEncoding(src.getParentEncoding()); + copy.setParentName(src.getParentName()); + if (copy.getParentEncoding().equals("0")) { + copy.setEncoding(nextCode(getMaxEncoding(src.getParentEncoding(), invocationId))); + } else { + copy.setEncoding(copy.getParentEncoding() + nextCode(getMaxEncoding(src.getParentEncoding(), invocationId))); + } + copy.setInstructTag(copy.getEncoding()); + baseMapper.insert(copy); + + baseMapper.shiftSortAfter(copy.getInvocationId(), copy.getSort(), copy.getInstructId()); + } + + private void copyWholeTreeBelow(Instruct root, String invocationId) { + /* 1) 先复制根 */ + Instruct rootCopy = cloneNode(root); + //查询一边移动后sort + Instruct instruct = baseMapper.selectById(root.getInstructId()); + if (instruct == null) { + return; + } + rootCopy.setSort(instruct.getSort() + 1); + + rootCopy.setParentEncoding(root.getParentEncoding()); + rootCopy.setParentName(root.getParentName()); + + if (rootCopy.getParentEncoding().equals("0")) { + rootCopy.setEncoding(nextCode(getMaxEncoding(rootCopy.getParentEncoding(), invocationId))); + } else { + rootCopy.setEncoding(rootCopy.getParentEncoding() + nextCode(getMaxEncoding(rootCopy.getParentEncoding(), invocationId))); + } + rootCopy.setInstructTag(rootCopy.getEncoding()); + baseMapper.insert(rootCopy); + + baseMapper.shiftSortAfter(rootCopy.getInvocationId(), rootCopy.getSort(), rootCopy.getInstructId()); + /* 2) 递归复制子树(保留相对顺序) */ + copySubTreeRecursively(root.getEncoding(), rootCopy.getEncoding(), invocationId, rootCopy.getInstructName()); + } + + /** + * 递归复制子树:只改 parent 头缀 & 编码头缀,保留 sort 相对值 + * + * @param oldPrefix 原编码头缀(如 040) + * @param newPrefix 新编码头缀(如 007) + */ + private void copySubTreeRecursively(String oldPrefix, String newPrefix, String invocationId, String parentName) { + // 直接子孙:parentEncoding == oldPrefix + List children = baseMapper.listByParentEncodingAndInvocation(invocationId, oldPrefix); + for (Instruct child : children) { + Instruct copy = cloneNode(child); + copy.setParentEncoding(newPrefix); + copy.setSort(nextRootSort(invocationId)); + if (copy.getParentEncoding().equals("0")) { + copy.setEncoding(nextCode(getMaxEncoding(copy.getParentEncoding(), invocationId))); + } else { + copy.setEncoding(copy.getParentEncoding() + nextCode(getMaxEncoding(copy.getParentEncoding(), invocationId))); + } + copy.setInstructTag(copy.getEncoding()); + copy.setParentName(parentName); + baseMapper.insert(copy); + + // 递归复制下一层 + copySubTreeRecursively(child.getEncoding(), copy.getEncoding(), invocationId, copy.getInstructName()); + } + } + + + @Override + public Result batchChangeParam(BatchChangeInstructParamDto dto) { + if (dto == null) { + return Result.error("参数不能为空"); + } + if (StringUtils.isBlank(dto.getInvocationId()) && (dto.getInstructIdList() == null || dto.getInstructIdList().isEmpty())) { + return Result.error("用例id或指令id不能为空"); + } + if (dto.getStatus() != null && dto.getStatus() != 1 && dto.getStatus() != 0) { + return Result.error("状态错误"); + } + if (dto.getIsDebug() != null && dto.getIsDebug() != 1 && dto.getIsDebug() != 0) { + return Result.error("断点状态错误"); + } + if (dto.getLeadTime() != null && dto.getLeadTime() < 0) { + return Result.error("前置等待时间不能小于0"); + } + if (dto.getTailTime() != null && dto.getTailTime() < 0) { + return Result.error("后置等待时间不能小于0"); + } + + if (dto.getStatus() == null && dto.getIsDebug() == null && dto.getLeadTime() == null && dto.getTailTime() == null) { + return Result.error("改变参数不能为空"); + } + try { + if (StringUtils.isNotBlank(dto.getInvocationId())) { + //根据用例id批量修改 + baseMapper.update(null, new LambdaUpdateWrapper() + .set(dto.getStatus() != null, Instruct::getStatus, dto.getStatus()) + .set(dto.getIsDebug() != null, Instruct::getBreakPoint, dto.getIsDebug()) + .set(dto.getLeadTime() != null, Instruct::getLeadTime, dto.getLeadTime()) + .set(dto.getTailTime() != null, Instruct::getTailTime, dto.getTailTime()) + .eq(Instruct::getInvocationId, dto.getInvocationId())); + } else { + //根据指令id批量修改 + baseMapper.update(null, new LambdaUpdateWrapper() + .set(dto.getStatus() != null, Instruct::getStatus, dto.getStatus()) + .set(dto.getIsDebug() != null, Instruct::getBreakPoint, dto.getIsDebug()) + .set(dto.getLeadTime() != null, Instruct::getLeadTime, dto.getLeadTime()) + .set(dto.getTailTime() != null, Instruct::getTailTime, dto.getTailTime()) + .in(Instruct::getInstructId, dto.getInstructIdList())); + } + } catch (Exception e) { + throw new BaseException("修改失败"); + } + return Result.success(); + } + + /** + * 获取用例列表 + * + * @param invocationIdList 用例id列表 + */ + public List getInvocationListByIds(List invocationIdList) { + if (invocationIdList == null || invocationIdList.isEmpty()) { + return Collections.emptyList(); + } + return invocationMapper.selectList(new LambdaQueryWrapper().in(Invocation::getInvocationId, invocationIdList) + .orderByAsc(Invocation::getSort)); + } + + /** + * 根据用例id获取指令列表 + * + * @param invocationId 用例id + */ + public List getInstructListByInvocationId(String invocationId) { + if (StringUtils.isBlank(invocationId)) { + return null; + } + return baseMapper.selectList(new LambdaQueryWrapper().eq(Instruct::getInvocationId, invocationId) + .orderByAsc(Instruct::getSort)); + } + + /** + * 根据id列表获取指令列表 + * + * @param instructIdList 指令id列表 + */ + public List getInstructListByIds(List instructIdList) { + return baseMapper.selectList(new LambdaQueryWrapper().in(Instruct::getInstructId, instructIdList) + .orderByAsc(Instruct::getSort)); + } + + /** + * 重置指令命中次数 + * + * @param invocationIdList 指令id列表 + */ + public void resetHitCountByInstructIdList(List invocationIdList) { + if (invocationIdList == null || invocationIdList.isEmpty()) { + return; + } + baseMapper.update(null, new LambdaUpdateWrapper() + .set(Instruct::getHitCount, 0) + .in(Instruct::getInstructId, invocationIdList)); + } + + /** + * 重置指令命中次数 + * + * @param invocationId 用例id + */ + public void resetHitCountByInvocation(String invocationId) { + if (StringUtils.isBlank(invocationId)) { + return; + } + baseMapper.update(null, new LambdaUpdateWrapper() + .set(Instruct::getHitCount, 0) + .eq(Instruct::getInvocationId, invocationId)); + } + + @Override + public Result uploadCommandFile(String instructId, MultipartFile file) { + if (StringUtils.isBlank(instructId)) { + return Result.error("指令id不能为空"); + } + if (file == null) { + return Result.error("文件不能为空"); + } + String fileName = file.getOriginalFilename(); + if (StringUtils.isBlank(fileName)) { + return Result.error("文件名不能为空"); + } + Instruct instruct = baseMapper.selectById(instructId); + if (instruct == null) { + return Result.error("指令不存在"); + } + File parentFile = new File(commandRootPath); + if (!parentFile.exists()) { + parentFile.mkdirs(); + } + String suffix = fileName.substring(fileName.lastIndexOf(".")); + if (!".txt".equals(suffix) && !".dat".equals(suffix)) { + return Result.error("请上传文本文件(.txt)或dat(.dat)文件"); + } + if (instruct.getInstructType() == 1) { + //常规指令 + String saveFileName = instruct.getInstructId() + suffix; + File existsFile = new File(parentFile, saveFileName); + if (existsFile.exists()) { + existsFile.delete(); + } + try { + Path savePath = Paths.get(parentFile.getPath(), saveFileName); + Files.write(savePath, file.getBytes()); + } catch (Exception e) { + throw new BaseException(e.getMessage()); + } + try { + baseMapper.update(null, new LambdaUpdateWrapper() + .set(Instruct::getInstructDataFile, saveFileName) + .eq(Instruct::getInstructId, instructId)); + } catch (Exception e) { + if (existsFile.exists()) { + existsFile.delete(); + } + return Result.error("上传失败 " + e.getMessage()); + } + } else if (instruct.getInstructType() == 2) { + //指令组,指令组下所有指令统一处理 + List childInstructList = baseMapper.selectList(new LambdaQueryWrapper() + .eq(Instruct::getInvocationId, instruct.getInvocationId()) + .eq(Instruct::getInstructType, 1) + .likeRight(Instruct::getParentEncoding, instruct.getEncoding())); + if (childInstructList != null && !childInstructList.isEmpty()) { + for (Instruct childInstruct : childInstructList) { + String saveFileName = instruct.getInstructId() + suffix; + File existsFile = new File(parentFile, saveFileName); + if (existsFile.exists()) { + existsFile.delete(); + } + try { + Path savePath = Paths.get(parentFile.getPath(), saveFileName); + Files.write(savePath, file.getBytes()); + } catch (Exception ignored) { + + } + try { + baseMapper.update(null, new LambdaUpdateWrapper() + .set(Instruct::getInstructDataFile, saveFileName) + .eq(Instruct::getInstructId, childInstruct.getInstructId())); + } catch (Exception e) { + if (existsFile.exists()) { + existsFile.delete(); + } + } + } + } else { + return Result.error("该指令组下无指令"); + } + } + return Result.success(); + } + + @Override + public void uploadScript(String instructId, String scriptType, String scriptFile) { + if (StringUtils.isBlank(instructId)) { + throw new BaseException("指令id不能为空"); + } + if (StringUtils.isBlank(scriptType)) { + throw new BaseException("脚本类型不能为空"); + } + if (!ScriptManager.ScriptType.getValues().contains(scriptType)) { + throw new BaseException("脚本类型错误"); + } + if (StringUtils.isBlank(scriptFile)) { + throw new BaseException("脚本文件不能为空"); + } + Instruct instruct = baseMapper.selectById(instructId); + if (instruct == null) { + throw new BaseException("指令不存在"); + } + String suffix = scriptFile.substring(scriptFile.lastIndexOf(".")); + String scriptName = scriptFile.replace(suffix, ""); + try { + baseMapper.update(null, new LambdaUpdateWrapper() + .set(scriptType.equals(ScriptManager.ScriptType.IPreTreatmentInterface.getValue()), Instruct::getPreProcessScriptFile, scriptFile) + .set(StringUtils.isBlank(instruct.getPreProcessScript()) && scriptType.equals(ScriptManager.ScriptType.IPreTreatmentInterface.getValue()), Instruct::getPreProcessScriptName, scriptName) + .set(scriptType.equals(ScriptManager.ScriptType.IBranchConditionInterface.getValue()), Instruct::getBranchConditionScriptFile, scriptFile) + .set(StringUtils.isBlank(instruct.getBranchConditionScript()) && scriptType.equals(ScriptManager.ScriptType.IBranchConditionInterface.getValue()), Instruct::getBranchConditionScriptName, scriptName) + .set(scriptType.equals(ScriptManager.ScriptType.ICheckInterface.getValue()), Instruct::getCheckScriptFile, scriptFile) + .set(StringUtils.isBlank(instruct.getCheckScript()) && scriptType.equals(ScriptManager.ScriptType.ICheckInterface.getValue()), Instruct::getCheckScriptName, scriptName) + .eq(Instruct::getInstructId, instructId)); + } catch (Exception e) { + throw new BaseException(e.getMessage()); + } + } + + @Override + public void uploadScriptContent(String instructId, String scriptType, String scriptFileName, String scriptContent) { + if (StringUtils.isBlank(instructId)) { + throw new BaseException("指令id不能为空"); + } + if (StringUtils.isBlank(scriptType)) { + throw new BaseException("脚本类型不能为空"); + } + if (!ScriptManager.ScriptType.getValues().contains(scriptType)) { + throw new BaseException("脚本类型错误"); + } + Instruct instruct = baseMapper.selectById(instructId); + if (instruct == null) { + throw new BaseException("指令不存在"); + } + try { + if (scriptType.equals(ScriptManager.ScriptType.IPreTreatmentInterface.getValue())) { + String preProcessScriptFile = instruct.getPreProcessScriptFile(); + String tempPreProcessScriptFileName = StringUtils.isNotBlank(preProcessScriptFile) ? preProcessScriptFile.replace(".java", "") : null; + baseMapper.update(null, new LambdaUpdateWrapper() + .set(Instruct::getPreProcessScript, StringUtils.isNotBlank(scriptContent) ? scriptContent : null) + .set(StringUtils.isNotBlank(scriptContent), Instruct::getPreProcessScriptName, scriptFileName) + .set(StringUtils.isBlank(scriptContent), Instruct::getPreProcessScriptName, tempPreProcessScriptFileName) + .eq(Instruct::getInstructId, instructId)); + } else if (scriptType.equals(ScriptManager.ScriptType.IBranchConditionInterface.getValue())) { + String branchConditionScriptFile = instruct.getBranchConditionScriptFile(); + String tempBranchConditionScriptFileName = StringUtils.isNotBlank(branchConditionScriptFile) ? branchConditionScriptFile.replace(".java", "") : null; + baseMapper.update(null, new LambdaUpdateWrapper() + .set(Instruct::getBranchConditionScript, StringUtils.isNotBlank(scriptContent) ? scriptContent : null) + .set(StringUtils.isNotBlank(scriptContent), Instruct::getBranchConditionScriptName, scriptFileName) + .set(StringUtils.isBlank(scriptContent), Instruct::getBranchConditionScriptName, tempBranchConditionScriptFileName) + .eq(Instruct::getInstructId, instructId)); + } else if (scriptType.equals(ScriptManager.ScriptType.ICheckInterface.getValue())) { + String checkScriptFile = instruct.getCheckScriptFile(); + String tempCheckScriptFileName = StringUtils.isNotBlank(checkScriptFile) ? checkScriptFile.replace(".java", "") : null; + baseMapper.update(null, new LambdaUpdateWrapper() + .set(Instruct::getCheckScript, StringUtils.isNotBlank(scriptContent) ? scriptContent : null) + .set(StringUtils.isNotBlank(scriptContent), Instruct::getCheckScriptName, scriptFileName) + .set(StringUtils.isBlank(scriptContent), Instruct::getCheckScriptName, tempCheckScriptFileName) + .eq(Instruct::getInstructId, instructId)); + } + } catch (Exception e) { + throw new BaseException(e.getMessage()); + } + } + + @Override + public Result deleteInstructFile(InstructDeleteFileDto dto) { + if (dto == null) { + return Result.error("参数不能为空"); + } + if (StringUtils.isBlank(dto.getInstructId())) { + return Result.error("指令id不能为空"); + } + if (dto.getDeleteFileType() == null) { + return Result.error("删除文件类型不能为空"); + } + try { + baseMapper.update(null, new LambdaUpdateWrapper() + .set(dto.getDeleteFileType() == 1, Instruct::getPreProcessScriptFile, null) + .set(dto.getDeleteFileType() == 2, Instruct::getBranchConditionScriptFile, null) + .set(dto.getDeleteFileType() == 3, Instruct::getCheckScriptFile, null) + .set(dto.getDeleteFileType() == 4, Instruct::getInstructDataFile, null) + .set(dto.getDeleteFileType() == 5, Instruct::getProtocolId, null) + .set(dto.getDeleteFileType() == 5, Instruct::getProtocolName, null) + .eq(Instruct::getInstructId, dto.getInstructId())); + } catch (Exception e) { + return Result.error("删除失败"); + } + return Result.success("删除成功"); + } + + @Override + public String getInstructDataRemark(String instructId) { + if (StringUtils.isBlank(instructId)) { + throw new BaseException("指令id不能为空"); + } + Instruct instruct = baseMapper.selectById(instructId); + if (instruct == null) { + throw new BaseException("指令不存在"); + } + if (StringUtils.isBlank(instruct.getChannelId())) { + throw new BaseException("该指令为选择通道"); + } + ServerInfo server = serverMapper.selectById(instruct.getServerId()); + String url = String.format("http://%s:%s/universaltestsoftware_server_api/common/getBoardDriverSendDataAnnotation", + server.getIpAddress(), server.getHttpPort()); + Map param = Collections.singletonMap("channelId", instruct.getChannelId()); + return HttpUtil.get(url, param); + } + + /** + * 将扁平的节点列表转换为树形结构,并按sort字段排序 + * + * @param nodes 所有节点的列表 + * @param rootParentEncoding 根节点的父编码标识,通常为空字符串或特定标识 + * @return 树形结构的根节点列表 + */ + public List buildTree(List nodes, String rootParentEncoding) { + // 存储所有节点的编码与节点对象的映射,便于快速查找 + Map nodeMap = new HashMap<>(); + + // 首先将所有节点放入map中 + for (Instruct node : nodes) { + nodeMap.put(node.getEncoding(), node); + } + + // 根节点列表 + List rootNodes = new ArrayList<>(); + + // 遍历所有节点,构建树形结构 + for (Instruct node : nodes) { + String parentEncoding = node.getParentEncoding(); + + // 如果是根节点 + if (rootParentEncoding.equals(parentEncoding)) { + rootNodes.add(node); + } else { + // 查找父节点并添加到父节点的子节点列表中 + Instruct parentNode = nodeMap.get(parentEncoding); + if (parentNode != null) { + parentNode.getChildren().add(node); + } else { + // 如果父节点不存在,也将其作为根节点处理 + rootNodes.add(node); + } + } + } + + // 对整棵树进行排序 + sortTree(rootNodes); + + return rootNodes; + } + + /** + * 递归对树形结构进行排序 + * + * @param nodes 节点列表 + */ + private void sortTree(List nodes) { + // 先对当前层级的节点按sort字段排序 + nodes.sort(Comparator.comparingInt(Instruct::getSort)); + + // 递归对每个节点的子节点进行排序 + for (Instruct node : nodes) { + if (!node.getChildren().isEmpty()) { + sortTree(node.getChildren()); + } + } + } + + /* ---------- 以下为辅助方法 ---------- */ + + + private boolean existsEncoding(String encoding, String invocationId) { + return baseMapper.selectCount( + new LambdaQueryWrapper() + .eq(Instruct::getEncoding, encoding) + .eq(Instruct::getInvocationId, invocationId)) > 0; + } + + private String getMaxEncoding(String parentEncoding, String invocationId) { + Instruct last = baseMapper.selectOne( + new LambdaQueryWrapper() + .eq(Instruct::getParentEncoding, parentEncoding) + .eq(Instruct::getInvocationId, invocationId) + .orderByDesc(Instruct::getEncoding) + .last("limit 1")); + return last == null ? null : last.getEncoding(); + } + + /** + * 根据同级最大编码,生成下一个编码 + * 自动根据提取出的数字长度确定格式化宽度 + * + * @param maxCode 最大编码,如 "001001" 或 "ABC001009" + * @return 下一个编码,如 "001002" + */ + private String nextCode(String maxCode) { + if (maxCode == null || maxCode.trim().isEmpty()) { + return "001"; + } + + // 1. 提取字符串中的所有数字 + String digits = maxCode.replaceAll("\\D+", ""); + + if (digits.isEmpty()) { + throw new IllegalArgumentException("编码中不包含数字: " + maxCode); + } + + // 2. 解析为 long 并加 1 + long nextNumber; + try { + long currentNumber = Long.parseLong(digits); + nextNumber = currentNumber + 1; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("无法解析编码中的数字: " + digits, e); + } + + // 3. 转换为字符串并截取或补充至三位数 + String nextCode = String.format("%03d", nextNumber); + + // 4. 确保总是返回三位数 + return nextCode.substring(nextCode.length() - Math.min(nextCode.length(), 3)); + } + + private int nextRootSort(String invocationId) { + Instruct last = baseMapper.selectOne( + new LambdaQueryWrapper() +// .eq(Instruct::getParentEncoding, "0") + .eq(Instruct::getInvocationId, invocationId) + .orderByDesc(Instruct::getSort) + .last("limit 1")); + return last == null ? 1 : last.getSort() + 1; + } + + private String getNameByEncoding(String encoding, String invocationId) { + return baseMapper.selectOne( + new LambdaQueryWrapper() + .eq(Instruct::getEncoding, encoding) + .eq(Instruct::getInvocationId, invocationId) + .select(Instruct::getInstructName)).getInstructName(); + } + + private int getMaxSort(String parentEncoding, String invocationId) { + Instruct last = baseMapper.selectOne(new LambdaQueryWrapper() +// .eq(Instruct::getParentEncoding, parentEncoding) + .eq(Instruct::getInvocationId, invocationId) + .orderByDesc(Instruct::getSort).last("limit 1")); + return last == null ? 0 : last.getSort(); + } + + + private Instruct cloneNode(Instruct src) { + Instruct tar = new Instruct(); + BeanUtils.copyProperties(src, tar); + tar.setInstructId(null); // 自增 + tar.setInstructName(src.getInstructName() + "_副本"); + tar.setHitCount(0); + tar.setLastHitTime(null); + return tar; + } + + + // 方案运行开始 + + @Resource + private CommandWebSocketHandler commandWebSocketHandler; + + /** + * 发送指令 + * + * @param runMessage 运行指令消息 + */ + public Result sendInstruct(CommandRunMessage runMessage) { + return execute(runMessage.getInstruct().getInstructId(), runMessage); + } + + public void broadcast(String message) { + commandWebSocketHandler.broadcast(message); + } + + // 方案运行结束 +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/InvocationServiceImpl.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/InvocationServiceImpl.java new file mode 100644 index 0000000..2501fa7 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/InvocationServiceImpl.java @@ -0,0 +1,367 @@ +package com.cbsd.universaltestsoftware_client.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.cbsd.universaltestsoftware_client.dto.InstructItem; +import com.cbsd.universaltestsoftware_client.dto.InstructXmlDTO; +import com.cbsd.universaltestsoftware_client.dto.InvocationPageDto; +import com.cbsd.universaltestsoftware_client.dto.InvocationSortDto; +import com.cbsd.universaltestsoftware_client.entity.Instruct; +import com.cbsd.universaltestsoftware_client.entity.Invocation; +import com.cbsd.universaltestsoftware_client.exception.BaseException; +import com.cbsd.universaltestsoftware_client.mapper.InstructMapper; +import com.cbsd.universaltestsoftware_client.mapper.InvocationMapper; +import com.cbsd.universaltestsoftware_client.script.ScriptManager; +import com.cbsd.universaltestsoftware_client.service.InstructService; +import com.cbsd.universaltestsoftware_client.service.InvocationService; +import com.cbsd.universaltestsoftware_client.util.DateUtils; +import com.cbsd.universaltestsoftware_client.util.Result; +import com.cbsd.universaltestsoftware_client.util.UUIDUtils; +import com.cbsd.universaltestsoftware_client.vo.InstructScriptVo; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + *

+ * 用列 服务实现类 + *

+ * + * @author admin + * @since 2025-09-01 + */ +@Service +public class InvocationServiceImpl extends ServiceImpl implements InvocationService { + + @Resource + private InstructMapper instructMapper; + + @Resource + private ScriptManager scriptManager; + + @Resource + private InstructService instructService; + + @Override + public Page pageList(InvocationPageDto invocationPageDto) { + Page serveBasePage = baseMapper.selectPage(invocationPageDto, new LambdaQueryWrapper() + .like(StringUtils.isNotBlank(invocationPageDto.getKeyword()), Invocation::getInvocationName, invocationPageDto.getKeyword()) + .eq(Invocation::getSchemeId, invocationPageDto.getSchemeId()) + .orderByAsc(Invocation::getSort) + ); + return serveBasePage; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public Result sort(InvocationSortDto dto) { + if (dto == null) { + return Result.error("参数不能为空"); + } + if (StringUtils.isBlank(dto.getSchemeId())) { + return Result.error("方案id不能为空"); + } + if (StringUtils.isBlank(dto.getInvocationId())) { + return Result.error("用例id不能为空"); + } + if (dto.getSort() == null) { + return Result.error("排序不能为空"); + } + Invocation invocation = baseMapper.selectById(dto.getInvocationId()); + if (invocation == null) { + return Result.error("用例不存在"); + } + int oldSort = invocation.getSort(); + int newSort = dto.getSort(); + long count = baseMapper.selectCount(new LambdaQueryWrapper() + .eq(Invocation::getSchemeId, dto.getSchemeId())); + if (newSort < 1) { + newSort = 1; + } + if (newSort > count) { + newSort = Math.toIntExact(count); + } + try { + if (newSort > oldSort) { + //小于等于newSort 大于oldSort 的sort - 1 + baseMapper.subtractSort(dto.getSchemeId(), newSort, oldSort); + } else if (newSort < oldSort) { + //大于等于newSort 小于oldSort的sort+1 + baseMapper.addSort(dto.getSchemeId(), newSort, oldSort); + } + baseMapper.update(null, new LambdaUpdateWrapper() + .set(Invocation::getSort, newSort) + .eq(Invocation::getInvocationId, dto.getInvocationId())); + } catch (Exception e) { + throw new BaseException("移动失败"); + } + return Result.success(); + } + + @Override + public void exportInvocation(String invocationId, HttpServletResponse response) throws IOException { + Invocation invocation = baseMapper.selectById(invocationId); + if (invocation == null) { + throw new BaseException("用例不存在"); + } + //查询下面得所有指令 + List instructs = instructMapper.selectList(new LambdaQueryWrapper().eq(Instruct::getInvocationId, invocationId)); + //组装数据 + ArrayList instructItems = new ArrayList(); + for (Instruct instruct : instructs) { + InstructItem instructItem = new InstructItem(); + BeanUtils.copyProperties(instruct, instructItem); + instructItem.setInvocationName(invocation.getInvocationName()); + instructItems.add(instructItem); + } + + + try { + InstructXmlDTO instructXmlDTO = new InstructXmlDTO(instructItems); + + + String xmlContent = marshalToXml(instructXmlDTO); + + // 🔽 安全处理文件名(防止中文乱码) + String encodedFilename = URLEncoder.encode(invocation.getInvocationName(), "UTF-8"); + // 兼容 IE/Edge(可选) + String headerValue = String.format("attachment; filename=\"%s\"; filename*=UTF-8''%s", + encodedFilename, encodedFilename); + + // 🔽 设置响应头 + response.setContentType("application/xml;charset=UTF-8"); + response.setHeader("Content-Disposition", headerValue); + response.setContentLength(xmlContent.getBytes("UTF-8").length); + + // 输出 XML 字符串 + response.getWriter().write(xmlContent); + response.getWriter().flush(); + + } catch (Exception e) { + e.printStackTrace(); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + response.getWriter().write("Server error: " + e.getMessage() + ""); + response.getWriter().flush(); + } + + + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void importXml(MultipartFile file, String schemeId) throws Exception { + if (file.isEmpty()) { + throw new IllegalArgumentException("上传文件为空"); + } + + if (!Objects.requireNonNull(file.getOriginalFilename()).toLowerCase().endsWith(".xml")) { + throw new IllegalArgumentException("仅支持 XML 文件"); + } + + List items; + try (InputStream is = file.getInputStream()) { + InstructXmlDTO dto = unmarshal(is); + if (dto == null || dto.getInstructs() == null || dto.getInstructs().isEmpty()) { + throw new IllegalArgumentException("XML 中未找到有效指令数据"); + } + items = dto.getInstructs(); + } + + // 获取所有指令中的用例名(应一致) + String invocationName = file.getOriginalFilename(); + if (StringUtils.isBlank(invocationName)) { + throw new BaseException("用例文件名称不能为空"); + } + String suffix = invocationName.substring(invocationName.lastIndexOf(".")); + invocationName = invocationName.replace(suffix, ""); + + // 查询或创建 Invocation(用例) + Invocation invocation = baseMapper.selectOne(new LambdaQueryWrapper().eq(Invocation::getInvocationName, invocationName) + .eq(Invocation::getSchemeId, schemeId)); + + String invocationId; + if (invocation == null) { + // 新建用例 + long count = baseMapper.selectCount(new LambdaQueryWrapper().eq(Invocation::getSchemeId, schemeId)); + invocation = new Invocation(); + invocation.setInvocationId(UUIDUtils.getUUID()); + invocation.setInvocationName(invocationName); + invocation.setSchemeId(schemeId); + invocation.setSort(Math.toIntExact(count + 1)); + invocation.setCreateTime(DateUtils.dateToString()); + baseMapper.insert(invocation); + invocationId = invocation.getInvocationId(); + } else { + invocationId = invocation.getInvocationId(); + + //删除原来的指令 + instructMapper.delete(new LambdaQueryWrapper() + .eq(Instruct::getInvocationId, invocationId)); + } + + // 批量保存指令 + List instructList = new ArrayList<>(); + for (InstructItem item : items) { + Instruct instruct = new Instruct(); + BeanUtils.copyProperties(item, instruct); + instruct.setInstructId(UUIDUtils.getUUID()); + instruct.setInvocationId(invocationId); // 关联到当前用例 + + //处理脚本 + String fixed = "public class "; + String implementsStr = "implements"; + //1.预处理脚本 + String preProcessScript = item.getPreProcessScript(); + if (StringUtils.isNotBlank(preProcessScript)) { + //获取原类名 + try { + int fixedIndex = preProcessScript.indexOf(fixed) + fixed.length(); + int implementsIndex = preProcessScript.indexOf(implementsStr); + String originalClassName = preProcessScript.substring(fixedIndex, implementsIndex - 1); + preProcessScript = preProcessScript.replace(originalClassName, "Pretreatment_" + instruct.getInstructId()); + InstructScriptVo vo = scriptManager.saveScriptContent(instruct.getInstructId(), "pretreatment", preProcessScript); + + instruct.setPreProcessScript(preProcessScript); + instruct.setPreProcessScriptName(vo.getScriptName()); + } catch (Exception e) { + instruct.setPreProcessScript(null); + instruct.setPreProcessScriptName(null); + } + } else { + instruct.setPreProcessScript(null); + instruct.setPreProcessScriptName(null); + } + //2.分支条件脚本 + String branchConditionScript = item.getBranchConditionScript(); + if (StringUtils.isNotBlank(branchConditionScript)) { + //获取原类名 + try { + int fixedIndex = branchConditionScript.indexOf(fixed) + fixed.length(); + int implementsIndex = branchConditionScript.indexOf(implementsStr); + String originalClassName = branchConditionScript.substring(fixedIndex, implementsIndex - 1); + branchConditionScript = branchConditionScript.replace(originalClassName, "Branch_" + instruct.getInstructId()); + InstructScriptVo vo = scriptManager.saveScriptContent(instruct.getInstructId(), "branch", branchConditionScript); + + instruct.setBranchConditionScript(branchConditionScript); + instruct.setBranchConditionScriptName(vo.getScriptName()); + } catch (Exception e) { + instruct.setBranchConditionScript(null); + instruct.setBranchConditionScriptName(null); + } + } else { + instruct.setBranchConditionScript(null); + instruct.setBranchConditionScriptName(null); + } + //3.检查脚本 + String checkScript = item.getCheckScript(); + if (StringUtils.isNotBlank(checkScript)) { + //获取原类名 + try { + int fixedIndex = checkScript.indexOf(fixed) + fixed.length(); + int implementsIndex = checkScript.indexOf(implementsStr); + String originalClassName = checkScript.substring(fixedIndex, implementsIndex - 1); + checkScript = checkScript.replace(originalClassName, "Check_" + instruct.getInstructId()); + InstructScriptVo vo = scriptManager.saveScriptContent(instruct.getInstructId(), "check", checkScript); + + instruct.setCheckScript(checkScript); + instruct.setCheckScriptName(vo.getScriptName()); + } catch (Exception e) { + instruct.setCheckScript(null); + instruct.setCheckScriptName(null); + } + } else { + instruct.setCheckScript(null); + instruct.setCheckScriptName(null); + } + + if (instruct.getOperationType() == null) { + instruct.setOperationType(0); + } + if (instruct.getLeadTime() == null) { + instruct.setLeadTime(0); + } + if (instruct.getTailTime() == null) { + instruct.setTailTime(1000); + } + if (instruct.getBreakPoint() == null) { + instruct.setBreakPoint(0); + } + if (instruct.getDataSplittingLength() == null) { + instruct.setDataSplittingLength(0); + } + instructList.add(instruct); + } + // 批量插入或更新(根据主键判断) + if (!instructList.isEmpty()) { + int batchSize = 1000; + int total = instructList.size(); + if (total > batchSize) { + int insertTimes = total / batchSize; + int lastSize = total % batchSize; + if (lastSize > 0) { + insertTimes += 1; + } + for (int i = 0; i < insertTimes; i++) { + List insertList; + if (lastSize > 0 && i == insertTimes - 1) { + insertList = instructList.subList(i * batchSize, i * batchSize + lastSize); + } else { + insertList = instructList.subList(i * batchSize, i * batchSize + batchSize); + } + instructMapper.insertBatchSomeColumn(insertList); + } + } else { + instructMapper.insertBatchSomeColumn(instructList); + } + } + } + + @Override + public Result deleteById(String invocationId) { + Invocation invocation = baseMapper.selectById(invocationId); + if (invocation == null) { + return Result.error("用例不存在"); + } + baseMapper.deleteById(invocationId); + instructMapper.delete(new LambdaQueryWrapper().eq(Instruct::getInvocationId, invocationId)); + //将该用例后的用例序号减一 + baseMapper.unificationSubtractSort(invocation.getSchemeId(), invocation.getSort()); + return Result.success(); + } + + private String marshalToXml(InstructXmlDTO dto) throws JAXBException { + JAXBContext context = JAXBContext.newInstance(InstructXmlDTO.class); + Marshaller marshaller = context.createMarshaller(); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8"); + + StringWriter writer = new StringWriter(); + marshaller.marshal(dto, writer); + return writer.toString(); + } + + private InstructXmlDTO unmarshal(InputStream inputStream) throws JAXBException { + JAXBContext context = JAXBContext.newInstance(InstructXmlDTO.class); + Unmarshaller unmarshaller = context.createUnmarshaller(); + return (InstructXmlDTO) unmarshaller.unmarshal(inputStream); + } + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ParserProtocolServiceImpl.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ParserProtocolServiceImpl.java new file mode 100644 index 0000000..c347226 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ParserProtocolServiceImpl.java @@ -0,0 +1,22 @@ +package com.cbsd.universaltestsoftware_client.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.cbsd.universaltestsoftware_client.entity.ParserProtocol; +import com.cbsd.universaltestsoftware_client.service.ParserProtocolService; +import com.cbsd.universaltestsoftware_client.mapper.ParserProtocolMapper; +import org.springframework.stereotype.Service; + +/** +* @author SanJie +* @description 针对表【parser_protocol(解析器协议)】的数据库操作Service实现 +* @createDate 2025-09-04 15:56:52 +*/ +@Service +public class ParserProtocolServiceImpl extends ServiceImpl + implements ParserProtocolService{ + +} + + + + diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ParserServiceImpl.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ParserServiceImpl.java new file mode 100644 index 0000000..b7e69b7 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ParserServiceImpl.java @@ -0,0 +1,176 @@ +package com.cbsd.universaltestsoftware_client.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.cbsd.universaltestsoftware_client.entity.*; +import com.cbsd.universaltestsoftware_client.mapper.ParserMapper; +import com.cbsd.universaltestsoftware_client.mapper.ParserProtocolMapper; +import com.cbsd.universaltestsoftware_client.mapper.ProtocolMapper; +import com.cbsd.universaltestsoftware_client.parser.ParserManager; +import com.cbsd.universaltestsoftware_client.service.ParserService; +import com.cbsd.universaltestsoftware_client.util.DateUtils; +import com.cbsd.universaltestsoftware_client.util.Result; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author SanJie + * @description 针对表【parser(解析器)】的数据库操作Service实现 + * @createDate 2025-09-04 15:55:48 + */ +@Service +public class ParserServiceImpl extends ServiceImpl implements ParserService { + + @Resource + private ParserProtocolMapper parserProtocolMapper; + + @Resource + private ParserManager parserManager; + + @Resource + private ProtocolMapper protocolMapper; + + @Resource + private ProtocolServiceImpl protocolService; + + @Override + public Result> getParserList(String channelId) { + LambdaQueryWrapper eq = new LambdaQueryWrapper().eq(Parser::getChannelId, channelId) + .orderByAsc(Parser::getParserSort); + List parserList = this.list(eq); + //查询有几个协议 + for (Parser parser : parserList) { + Long l = parserProtocolMapper.selectCount(new LambdaQueryWrapper().eq(ParserProtocol::getParserId, parser.getParserId())); + + parser.setProtocolCount(l); + } + return Result.success(parserList); + } + + @Override + public Result saveOrUpdateParser(Parser parser) { + if (StringUtils.isBlank(parser.getParserId())) { + parser.setCreateTime(DateUtils.dateToString()); + long count = baseMapper.selectCount(new LambdaQueryWrapper().eq(Parser::getChannelId, parser.getChannelId())); + parser.setParserSort((int) (count + 1)); + boolean save = this.save(parser); + if (save) { + return Result.success("保存成功"); + } else { + return Result.error("保存失败"); + } + } else { + boolean update = this.updateById(parser); + if (update) { + return Result.success("修改成功"); + } else { + return Result.error("修改失败"); + } + } + } + + @Override + public Result deleteParserById(String parserId) { + //查询通道 + Parser parser = this.getById(parserId); + if (parser != null) { + boolean remove = this.removeById(parserId); + if (remove) { + parserManager.refresh(); + } + parserProtocolMapper.delete(new LambdaQueryWrapper().eq(ParserProtocol::getParserId, parserId)); + + //对该通道解析器重新排序 + List parserList = baseMapper.selectList(new LambdaQueryWrapper().eq(Parser::getChannelId, parser.getChannelId()) + .orderByAsc(Parser::getParserSort)); + if (parserList != null && !parserList.isEmpty()) { + for (int i = 0; i < parserList.size(); i++) { + parserList.get(i).setParserSort(i + 1); + } + saveOrUpdateBatch(parserList); + } + updateDataBoard(parser); + } + return Result.success(); + } + + @Override + public Result addProtocol(List parserProtocol) { + if (CollectionUtils.isNotEmpty(parserProtocol)) { + //先删除 + String parserId = parserProtocol.get(0).getParserId(); + + parserProtocolMapper.delete(new LambdaQueryWrapper().eq(ParserProtocol::getParserId, parserId)); + for (ParserProtocol protocol : parserProtocol) { + protocol.setCreateTime(DateUtils.dateToString()); + parserProtocolMapper.insert(protocol); + } + Parser parser = baseMapper.selectById(parserId); + if (parser != null) { + parserManager.refresh(); + updateDataBoard(parser); + } + } + + return Result.success("添加成功"); + } + + @Override + public Result> getProtocolList(String parserId) { + List parserProtocolList = parserProtocolMapper.selectList(new LambdaQueryWrapper().eq(ParserProtocol::getParserId, parserId)); + List collect = parserProtocolList.stream().map(ParserProtocol::getProtocolId).distinct().collect(Collectors.toList()); + if (CollectionUtils.isNotEmpty(collect)) { + List protocols = protocolMapper.selectList(new LambdaQueryWrapper().in(Protocol::getProtocolId, collect)); + for (ParserProtocol parserProtocol : parserProtocolList) { + //设置协议名字 + parserProtocol.setProtocolName(protocols.stream().filter(protocol -> protocol.getProtocolId().equals(parserProtocol.getProtocolId())).findFirst().get().getProtocolName()); + + + } + } + return Result.success(parserProtocolList); + } + + @Override + public Result deleteParserProtocolById(String parserProtocolId) { + ParserProtocol parserProtocol = parserProtocolMapper.selectById(parserProtocolId); + if (parserProtocol != null) { + int delete = parserProtocolMapper.deleteById(parserProtocolId); + if (delete != 0) { + Parser parser = baseMapper.selectById(parserProtocol.getParserId()); + if (parser != null) { + parserManager.refresh(); + updateDataBoard(parser); + } + } + + } + return Result.success("删除成功"); + } + + //更新数据看板参数 + private void updateDataBoard(Parser parser) { + /* 3. 找到这些通道关联的看板-通道关系 & 看板 */ + List channelIds = new ArrayList(); + channelIds.add(parser.getChannelId()); + List boardChannels = protocolService.listBoardChannelsByChannelIds(channelIds); + List dataBoards = protocolService.listDataBoardsByChannels(boardChannels); + if (dataBoards.isEmpty()) { + return; + } + /* 4. 逐一看板同步到遥测 */ + for (DataBoard board : dataBoards) { + protocolService.syncSingleBoard(board, boardChannels); + } + } +} + + + + diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ProtocolServiceImpl.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ProtocolServiceImpl.java new file mode 100644 index 0000000..003e424 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ProtocolServiceImpl.java @@ -0,0 +1,399 @@ +package com.cbsd.universaltestsoftware_client.service.impl; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelReader; +import com.alibaba.excel.read.listener.PageReadListener; +import com.alibaba.excel.read.metadata.ReadSheet; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.cbsd.universaltestsoftware_client.config.BasePage; +import com.cbsd.universaltestsoftware_client.dto.ChannelDto; +import com.cbsd.universaltestsoftware_client.dto.InsertDataBoardDto; +import com.cbsd.universaltestsoftware_client.dto.ProtocolImport; +import com.cbsd.universaltestsoftware_client.dto.ProtocolPageDto; +import com.cbsd.universaltestsoftware_client.entity.*; +import com.cbsd.universaltestsoftware_client.mapper.*; +import com.cbsd.universaltestsoftware_client.parser.ParserManager; +import com.cbsd.universaltestsoftware_client.parser.enums.ProtocolEnum; +import com.cbsd.universaltestsoftware_client.parser.model.Filed; +import com.cbsd.universaltestsoftware_client.parser.model.Meta; +import com.cbsd.universaltestsoftware_client.parser.model.ProtocolXml; +import com.cbsd.universaltestsoftware_client.parser.model.VariableLengthDesc; +import com.cbsd.universaltestsoftware_client.service.DataBoardService; +import com.cbsd.universaltestsoftware_client.service.ProtocolService; +import com.cbsd.universaltestsoftware_client.util.DateUtils; +import com.cbsd.universaltestsoftware_client.util.Result; +import com.cbsd.universaltestsoftware_client.util.UUIDUtils; +import com.cbsd.universaltestsoftware_client.util.XmlToObjectConverter; +import com.cbsd.universaltestsoftware_client.vo.ProtocolSaveVo; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.xml.bind.JAXBException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + *

+ * 协议 服务实现类 + *

+ * + * @author admin + * @since 2025-09-02 + */ +@Service +public class ProtocolServiceImpl extends ServiceImpl implements ProtocolService { + + @Resource + private ParserProtocolMapper parserProtocolMapper; + + @Resource + private ParserMapper parserMapper; + + @Resource + private ParserManager parserManager; + + @Resource + private DataBoardChannelMapper dataBoardChannelMapper; + + @Resource + private DataBoardMapper dataBoardMapper; + + @Resource + private DataBoardService dataBoardService; + + private String defaultProtocol = "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + @Override + public Result> pageListProtocol(ProtocolPageDto dto) { + BasePage protocolBasePage = baseMapper.selectPage(dto, new LambdaQueryWrapper() + .like(StringUtils.isNotBlank(dto.getKeyword()), Protocol::getProtocolName, dto.getKeyword()) + .eq(dto.getProtocolType() != null, Protocol::getProtocolType, dto.getProtocolType()) + .orderByAsc(Protocol::getCreateTime) + ); + return Result.success(protocolBasePage); + } + + @Override + public Result deleteProtocolById(String protocolId) { + if (removeById(protocolId)) { + refreshParserManager(); + parserProtocolMapper.delete(new LambdaQueryWrapper().eq(ParserProtocol::getProtocolId, protocolId)); + //查询通道通道 + return Result.success(); + } + return Result.error("删除失败"); + } + + @Override + public Result saveOrUpdateProtocol(Protocol protocol) { + if (StringUtils.isNotBlank(protocol.getProtocolId())) { + try { + ProtocolXml protocolXml = XmlToObjectConverter.convertXmlToObject(protocol.getProtocolContent(), ProtocolXml.class); + if (protocolXml.getFileds() != null) { + int totalBitsLen = 0; + for (Filed filed : protocolXml.getFileds()) { + totalBitsLen += filed.getBitsLen(); + } + if (totalBitsLen % 8 != 0) { + return Result.error("协议字段bitslen总长度必须为8的倍数"); + } + } + } catch (Exception e) { + return Result.error("协议内容不合法"); + } + protocol.setUpdateTime(DateUtils.dateToString()); + baseMapper.updateById(protocol); + //修改遥测那边参数 + updateTelemetry(protocol.getProtocolId()); + } else { + protocol.setCreateTime(DateUtils.dateToString()); + protocol.setProtocolContent(defaultProtocol); + baseMapper.insert(protocol); + } + protocol = baseMapper.selectById(protocol.getProtocolId()); + ProtocolSaveVo vo = new ProtocolSaveVo(); + vo.setProtocol(protocol); + try { + vo.setParse(XmlToObjectConverter.convertXmlToObject(protocol.getProtocolContent(), ProtocolXml.class)); + } catch (JAXBException e) { + e.printStackTrace(); + } + refreshParserManager(); + return Result.success(vo); + } + + private void refreshParserManager() { + parserManager.refresh(); + } + + /** + * 协议参数发生变更后,把受影响的数据看板及其通道同步到遥测系统 + * 注意:本方法不处理事务,由调用方决定是否需要事务 + * + * @param protocolId 协议主键 + */ + private void updateTelemetry(String protocolId) { + /* 1. 找到所有关联的解析器 ID */ + List parserIds = getParserIdsByProtocol(protocolId); + if (parserIds.isEmpty()) { + return; + } + + /* 2. 找到解析器对应的所有通道 ID */ + List channelIds = getChannelIdsByParsers(parserIds); + if (channelIds.isEmpty()) { + return; + } + + /* 3. 找到这些通道关联的看板-通道关系 & 看板 */ + List boardChannels = listBoardChannelsByChannelIds(channelIds); + List dataBoards = listDataBoardsByChannels(boardChannels); + if (dataBoards.isEmpty()) { + return; + } + + /* 4. 逐一看板同步到遥测 */ + for (DataBoard board : dataBoards) { + syncSingleBoard(board, boardChannels); + } + } + + /* ====================== 抽取的私有方法 ====================== */ + + /** + * 根据协议 ID 拿到所有解析器 ID(已去重) + */ + private List getParserIdsByProtocol(String protocolId) { + return parserProtocolMapper + .selectList(new LambdaQueryWrapper() + .eq(ParserProtocol::getProtocolId, protocolId)) + .stream() + .map(ParserProtocol::getParserId) + .distinct() + .collect(Collectors.toList()); + } + + /** + * 根据解析器 ID 列表拿到所有通道 ID(已去重) + */ + private List getChannelIdsByParsers(List parserIds) { + return parserMapper + .selectList(new LambdaQueryWrapper() + .in(Parser::getParserId, parserIds)) + .stream() + .map(Parser::getChannelId) + .distinct() + .collect(Collectors.toList()); + } + + /** + * 根据通道 ID 列表拿到看板-通道关联列表 + */ + public List listBoardChannelsByChannelIds(List channelIds) { + return dataBoardChannelMapper + .selectList(new LambdaQueryWrapper() + .in(DataBoardChannel::getChannelId, channelIds)); + } + + /** + * 根据看板-通道关联列表拿到需要去重的看板列表 + */ + public List listDataBoardsByChannels(List boardChannels) { + List boardIds = boardChannels + .stream() + .map(DataBoardChannel::getDataBoardId) + .distinct() + .collect(Collectors.toList()); + if (boardIds.isEmpty()) { + return Collections.emptyList(); + } + return dataBoardMapper + .selectList(new LambdaQueryWrapper() + .in(DataBoard::getDataBoardId, boardIds)); + } + + /** + * 同步单块看板及其通道到遥测系统 + */ + public void syncSingleBoard(DataBoard board, List boardChannels) { + try { + // 属于当前看板的通道 +// List channelsOfBoard = boardChannels +// .stream() +// .filter(bc -> bc.getDataBoardId().equals(board.getDataBoardId())) +// .collect(Collectors.toList()); + //通过数据看板id查询所有通道 + List channelsOfBoard = dataBoardChannelMapper.selectList(new LambdaQueryWrapper(). + eq(DataBoardChannel::getDataBoardId, board.getDataBoardId()) + .groupBy(DataBoardChannel::getChannelId) + ); + + // 按 channelId 去重,并直接收集成 channelList + List channelDtos = channelsOfBoard + .stream() + .map(channel -> { + ChannelDto channelDto = new ChannelDto(); + BeanUtils.copyProperties(channel, channelDto); + return channelDto; + }) + .distinct() + .collect(Collectors.toList()); + + InsertDataBoardDto dto = new InsertDataBoardDto(); + BeanUtils.copyProperties(board, dto); + dto.setChannelList(channelDtos); + + // 调用远程遥测服务 + dataBoardService.saveOrUpdateDataBoard(dto); + } catch (Exception e) { + // 打印日志 & 继续同步下一块看板,也可换成抛业务异常 + } + } + + @Override + public Result importProtocol(MultipartFile file) { + try (ExcelReader excelReader = EasyExcel.read(file.getInputStream()).build()) { + //基本信息 + List basicInfoList = new ArrayList<>(); + //字段信息 + List filedList = new ArrayList<>(); + //可变长度字段信息 + List variableLengthList = new ArrayList<>(); + + ReadSheet basicInfoSheet = + EasyExcel.readSheet(0).head(ProtocolImport.BasicInfo.class).registerReadListener(new PageReadListener(basicInfoList::addAll)).headRowNumber(2).build(); + ReadSheet filedSheet = + EasyExcel.readSheet(1).head(ProtocolImport.Filed.class).registerReadListener(new PageReadListener(filedList::addAll)).headRowNumber(2).build(); + ReadSheet variableLengthSheet = + EasyExcel.readSheet(2).head(ProtocolImport.VariableLength.class).registerReadListener(new PageReadListener(variableLengthList::addAll)).headRowNumber(2).build(); + excelReader.read(basicInfoSheet, filedSheet, variableLengthSheet); + + if (basicInfoList.isEmpty()) { + return Result.error("请填写协议基本信息"); + } + if (basicInfoList.size() > 1) { + return Result.error("请填写一个协议基本信息"); + } + if (filedList.isEmpty()) { + return Result.error("请填写字段信息"); + } + + ProtocolXml protocolXml = new ProtocolXml(); + ProtocolImport.BasicInfo basicInfo = basicInfoList.get(0); + if (StringUtils.isBlank(basicInfo.getReadName())) { + return Result.error("基本信息readName为空"); + } + protocolXml.setReadName(basicInfo.getReadName()); + Meta meta = new Meta(); + meta.setFrameHeadValue(basicInfo.getFrameHeadValue()); + meta.setFrameTailValue(basicInfo.getFrameTailValue()); + meta.setCheckStartSn(basicInfo.getCheckStartSn()); + meta.setCheckEndSn(basicInfo.getCheckEndSn()); + meta.setVerifyScript(basicInfo.getVerifyScript()); + if (!variableLengthList.isEmpty()) { + List variableLengthDescList = variableLengthList.stream().map(it -> { + VariableLengthDesc variableLengthDesc = new VariableLengthDesc(); + variableLengthDesc.setValue(it.getValue()); + variableLengthDesc.setLength(it.getLength()); + return variableLengthDesc; + }).collect(Collectors.toList()); + meta.setVariableLengthDescList(variableLengthDescList); + } + protocolXml.setMeta(meta); + String[] phyTypeArr = new String[]{"int", "unsigned int", "float", "double", "hex", "time"}; + List fileds = new ArrayList<>(); + for (int i = 0; i < filedList.size(); i++) { + int row = i + 2; + ProtocolImport.Filed importFiled = filedList.get(i); + importFiled.setSn(String.valueOf(i)); + importFiled.setLabel("filed" + i); + if (StringUtils.isBlank(importFiled.getReadName())) { + return Result.error("第" + row + "行readName为空"); + } + if (StringUtils.isBlank(importFiled.getBitslen())) { + return Result.error("第" + row + "行bitslen为空"); + } + if(StringUtils.isBlank(importFiled.getPhyType())) { + importFiled.setPhyType("hex"); + } + if (StringUtils.isNotBlank(importFiled.getPhyType()) && !Arrays.asList(phyTypeArr).contains(importFiled.getPhyType())) { + return Result.error("第" + row + "行phyType错误"); + } + if (StringUtils.isNotBlank(importFiled.getEndian()) && ProtocolEnum.getByValue(importFiled.getEndian()) == null) { + return Result.error("第" + row + "行endian错误"); + } + if(StringUtils.isBlank(importFiled.getType())) { + importFiled.setType(ProtocolEnum.VAR.getValue()); + } + if (ProtocolEnum.getByValue(importFiled.getType()) == null) { + return Result.error("第" + row + "行type错误"); + } + if(StringUtils.isBlank(importFiled.getEndian())) { + importFiled.setEndian(ProtocolEnum.ENDIAN_BIG.getValue()); + } + Filed filed = new Filed(); + BeanUtils.copyProperties(importFiled, filed); + fileds.add(filed); + } + protocolXml.setFileds(fileds); + + Protocol protocol = new Protocol(); + protocol.setProtocolId(UUIDUtils.getUUID()); + protocol.setProtocolName(basicInfo.getReadName()); + protocol.setProtocolContent(XmlToObjectConverter.convertObjectToXml(protocolXml)); + protocol.setProtocolType(1); + protocol.setCreateTime(DateUtils.dateToString()); + protocol.setUpdateTime(DateUtils.dateToString()); + + baseMapper.insert(protocol); + + return Result.success(); + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/SchemeServiceImpl.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/SchemeServiceImpl.java new file mode 100644 index 0000000..68b9382 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/SchemeServiceImpl.java @@ -0,0 +1,83 @@ +package com.cbsd.universaltestsoftware_client.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.cbsd.universaltestsoftware_client.config.BasePage; +import com.cbsd.universaltestsoftware_client.entity.Instruct; +import com.cbsd.universaltestsoftware_client.entity.Invocation; +import com.cbsd.universaltestsoftware_client.entity.Scheme; +import com.cbsd.universaltestsoftware_client.exception.BaseException; +import com.cbsd.universaltestsoftware_client.mapper.InstructMapper; +import com.cbsd.universaltestsoftware_client.mapper.InvocationMapper; +import com.cbsd.universaltestsoftware_client.mapper.SchemeMapper; +import com.cbsd.universaltestsoftware_client.service.InvocationService; +import com.cbsd.universaltestsoftware_client.service.SchemeService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.cbsd.universaltestsoftware_client.util.Result; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.List; +import java.util.stream.Collectors; + +/** + *

+ * 方案 服务实现类 + *

+ * + * @author admin + * @since 2025-09-01 + */ +@Service +public class SchemeServiceImpl extends ServiceImpl implements SchemeService { + + @Resource + private InvocationMapper invocationMapper; + + @Resource + private InstructMapper instructMapper; + + @Override + public Result> pageList(BasePage basePage) { + BasePage serveBasePage = baseMapper.selectPage(basePage, new LambdaQueryWrapper() + .like(StringUtils.isNotBlank(basePage.getKeyword()), Scheme::getSchemeName, basePage.getKeyword()) + .orderByAsc(Scheme::getCreateTime) + ); + return Result.success(serveBasePage); + + } + + @Transactional(rollbackFor = Exception.class) + @Override + public Result deleteScheme(String schemeId) { + if (StringUtils.isBlank(schemeId)){ + return Result.error("方案id不能为空"); + } + Scheme scheme = getById(schemeId); + if (scheme == null){ + return Result.error("方案不存在"); + } + try { + //查询方案下的用例 + List invocationList = invocationMapper.selectList(new LambdaQueryWrapper() + .eq(Invocation::getSchemeId, schemeId)); + if (invocationList != null && !invocationList.isEmpty()){ + List invocationIds = invocationList.stream().map(Invocation::getInvocationId).collect(Collectors.toList()); + //删除方案下的用例指令 + instructMapper.delete(new LambdaQueryWrapper() + .in(Instruct::getInvocationId, invocationIds)); + //删除方案下的用例 + invocationMapper.delete(new LambdaQueryWrapper() + .eq(Invocation::getSchemeId, schemeId)); + } + //删除方案 + baseMapper.deleteById(schemeId); + + }catch (Exception e){ + throw new BaseException("删除方案失败"); + } + return Result.success(); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/SensorEquipmentServiceImpl.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/SensorEquipmentServiceImpl.java new file mode 100644 index 0000000..603b89e --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/SensorEquipmentServiceImpl.java @@ -0,0 +1,98 @@ +package com.cbsd.universaltestsoftware_client.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.cbsd.universaltestsoftware_client.dto.SensorEquipmentDto; +import com.cbsd.universaltestsoftware_client.dto.SensorEquipmentPageDto; +import com.cbsd.universaltestsoftware_client.entity.SensorEquipment; +import com.cbsd.universaltestsoftware_client.mapper.SensorEquipmentMapper; +import com.cbsd.universaltestsoftware_client.service.SensorEquipmentService; +import com.cbsd.universaltestsoftware_client.util.DateUtils; +import com.cbsd.universaltestsoftware_client.util.Result; +import com.cbsd.universaltestsoftware_client.util.UUIDUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@Slf4j +public class SensorEquipmentServiceImpl extends ServiceImpl implements SensorEquipmentService { + + @Override + public Result> pageList(SensorEquipmentPageDto dto) { + Page page = new Page<>(dto.getCurrent(), dto.getSize()); + baseMapper.selectPage(page, new LambdaQueryWrapper() + .eq(StringUtils.isNotBlank(dto.getChannelId()), SensorEquipment::getChannelId, dto.getChannelId()) + .like(StringUtils.isNotBlank(dto.getEquipmentName()), SensorEquipment::getEquipmentName, dto.getEquipmentName()) + .orderByAsc(SensorEquipment::getCreateTime)); + return Result.success(page); + } + + @Override + public Result> getAllList(String channelId) { + List list = baseMapper.selectList(new LambdaQueryWrapper() + .eq(StringUtils.isNotBlank(channelId), SensorEquipment::getChannelId, channelId) + .orderByAsc(SensorEquipment::getCreateTime)); + return Result.success(list); + } + + @Override + public Result addOrUpdate(SensorEquipmentDto dto) { + if (StringUtils.isBlank(dto.getEquipmentName())){ + return Result.error("设备名称不能为空"); + } + if (StringUtils.isBlank(dto.getChannelId())){ + return Result.error("设备通道id不能为空"); + } + if (StringUtils.isBlank(dto.getChannelAddress())){ + return Result.error("设备通道地址不能为空"); + } + if (StringUtils.isNotBlank(dto.getEquipmentId())){ + SensorEquipment sensorEquipment = baseMapper.selectById(dto.getEquipmentId()); + if (sensorEquipment == null){ + return Result.error("设备不存在"); + } + sensorEquipment.setEquipmentName(dto.getEquipmentName()); + sensorEquipment.setEquipmentDesc(dto.getEquipmentDesc()); + sensorEquipment.setEquipmentParam(dto.getEquipmentParam()); + sensorEquipment.setChannelId(dto.getChannelId()); + sensorEquipment.setChannelAddress(dto.getChannelAddress()); + try { + baseMapper.updateById(sensorEquipment); + }catch (Exception e){ + e.printStackTrace(); + return Result.error("修改失败"); + } + } else { + SensorEquipment sensorEquipment = new SensorEquipment(); + sensorEquipment.setEquipmentId(UUIDUtils.getUUID()); + sensorEquipment.setEquipmentName(dto.getEquipmentName()); + sensorEquipment.setEquipmentDesc(dto.getEquipmentDesc()); + sensorEquipment.setEquipmentParam(dto.getEquipmentParam()); + sensorEquipment.setChannelId(dto.getChannelId()); + sensorEquipment.setChannelAddress(dto.getChannelAddress()); + sensorEquipment.setCreateTime(DateUtils.dateToString()); + try { + baseMapper.insert(sensorEquipment); + }catch (Exception e){ + e.printStackTrace(); + return Result.error("修改失败"); + } + } + return Result.success(); + } + + @Override + public Result delete(String equipmentId) { + try { + baseMapper.deleteById(equipmentId); + }catch (Exception e){ + e.printStackTrace(); + return Result.error("删除失败"); + } + return Result.success(); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ServeServiceImpl.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ServeServiceImpl.java new file mode 100644 index 0000000..b8d7226 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/ServeServiceImpl.java @@ -0,0 +1,127 @@ +package com.cbsd.universaltestsoftware_client.service.impl; + +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.cbsd.universaltestsoftware_client.config.BasePage; +import com.cbsd.universaltestsoftware_client.dto.PageServerInfoDto; +import com.cbsd.universaltestsoftware_client.entity.ServerInfo; +import com.cbsd.universaltestsoftware_client.exception.BaseException; +import com.cbsd.universaltestsoftware_client.mapper.ServerMapper; +//import com.cbsd.universaltestsoftware_client.netty.NettyClient; +import com.cbsd.universaltestsoftware_client.nettyclient.connection.ConnectionManager; +import com.cbsd.universaltestsoftware_client.service.ServeService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.cbsd.universaltestsoftware_client.util.DateUtils; +import com.cbsd.universaltestsoftware_client.util.Result; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + *

+ * 服务端 服务实现类 + *

+ * + * @author admin + * @since 2025-08-26 + */ +@Service +public class ServeServiceImpl extends ServiceImpl implements ServeService { + private final ConnectionManager connectionManager; + + @Autowired + public ServeServiceImpl(ConnectionManager connectionManager) { + this.connectionManager = connectionManager; + } + + @Override + public Result> pageList(PageServerInfoDto basePage) { + BasePage serveBasePage = baseMapper.selectPage(basePage, new LambdaQueryWrapper().eq(ServerInfo::getIsDelete, "0") + .like(StringUtils.isNotBlank(basePage.getKeyword()), ServerInfo::getServerName, basePage.getKeyword()) + .eq(basePage.getConnectStatus()!=null, ServerInfo::getConnectStatus, basePage.getConnectStatus()) + .orderByAsc(ServerInfo::getCreateTime) + ); + + List records = serveBasePage.getRecords(); + //获取状态 + Map serverConnectionStatus = connectionManager.getServerConnectionStatus(); + + for (ServerInfo record : records) { + if (CollectionUtils.isNotEmpty(serverConnectionStatus)){ + Boolean b = serverConnectionStatus.get(record.getServerId()); + if (b != null){ + record.setStatus(b ? 1 : 0); + } + } + + } + return Result.success(serveBasePage); + } + + @Override + public Result deleteById(String serverId) { +// UpdateWrapper serveUpdateWrapper = new UpdateWrapper<>(); +// serveUpdateWrapper.eq("server_id",serverId).set("is_delete",serverId) +// .set("update_time", DateUtils.dateToString()) +// ; +// baseMapper.update(null,serveUpdateWrapper); + baseMapper.deleteById(serverId); + connectionManager.removeServer(serverId); + + return Result.success(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Result connectOrDisconnect(String serverId) { + //查询服务端信息 + ServerInfo serverInfo = baseMapper.selectById(serverId); + if (serverInfo != null){ + serverInfo.setConnectStatus(1); + serverInfo.setUpdateTime(DateUtils.dateToString()); + baseMapper.updateById(serverInfo); + try { + String ipAddress = serverInfo.getIpAddress(); + Integer httpPort = serverInfo.getHttpPort(); + String url = "http://"+ipAddress+":"+httpPort+"/universaltestsoftware_server_api/outer/client/getNettyTcpPort"; + System.out.println(url); + String result = HttpUtil.post(url, new HashMap<>()); + if (StringUtils.isNotBlank(result)){ + Result bean = JSONUtil.toBean(result, Result.class); + if (bean.getCode() != 1){ + return Result.error("连接失败"); + } + serverInfo.setTcpPort(Integer.parseInt(bean.getData().toString())); + } + baseMapper.updateById(serverInfo); + + connectionManager.addAndConnectServer(serverId, serverInfo); + }catch (Exception e){ + e.printStackTrace(); + throw new BaseException("连接失败"); + } + } + + return Result.success(); + } + + @Override + public Result disconnectServer(String serverId) { + connectionManager.removeServer(serverId); + UpdateWrapper serveUpdateWrapper = new UpdateWrapper<>(); + serveUpdateWrapper.eq("server_id",serverId).set("connect_status",0) + .set("update_time", DateUtils.dateToString()) + ; + baseMapper.update(null,serveUpdateWrapper); + return Result.success(); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/SystemConfigServiceImpl.java b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/SystemConfigServiceImpl.java new file mode 100644 index 0000000..0f185eb --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/service/impl/SystemConfigServiceImpl.java @@ -0,0 +1,77 @@ +package com.cbsd.universaltestsoftware_client.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.cbsd.universaltestsoftware_client.entity.SystemConfig; +import com.cbsd.universaltestsoftware_client.exception.BaseException; +import com.cbsd.universaltestsoftware_client.mapper.SystemConfigMapper; +import com.cbsd.universaltestsoftware_client.service.SystemConfigService; +import com.cbsd.universaltestsoftware_client.util.Result; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author SanJie + * @description 针对表【system_config(系统配置(仅类型和内容))】的数据库操作Service实现 + * @createDate 2025-09-19 11:18:42 + */ +@Service +public class SystemConfigServiceImpl extends ServiceImpl + implements SystemConfigService { + + @Override + public Result> getShowColumnList() { + SystemConfig config = baseMapper.selectById(1); + if (config != null) { + if (StringUtils.isNotBlank(config.getContent())) { + return Result.success(Arrays.stream(config.getContent().split(",")).collect(Collectors.toList())); + } else { + return Result.success(new ArrayList<>()); + } + } + return Result.success(); + } + + @Override + public Result setShowColumnList(List columnList) { + SystemConfig config = baseMapper.selectById(1); + if (config == null) { + config = new SystemConfig(); + config.setType(1); + } + config.setContent(String.join(",", columnList)); + saveOrUpdate(config); + return Result.success(); + } + + @Override + public void setSystemConfig(int type, String value) { + try { + SystemConfig config = baseMapper.selectById(type); + if (config == null) { + config = new SystemConfig(); + config.setType(type); + baseMapper.insert(config); + } else { + config.setContent(value); + baseMapper.updateById(config); + } + } catch (Exception e) { + throw new BaseException(e.getMessage()); + } + } + + @Override + public String getSystemConfig(int type) { + SystemConfig config = baseMapper.selectById(type); + return config != null ? config.getContent() : ""; + } +} + + + + diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/timing/ChannelDataTiming.java b/src/main/java/com/cbsd/universaltestsoftware_client/timing/ChannelDataTiming.java new file mode 100644 index 0000000..95778da --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/timing/ChannelDataTiming.java @@ -0,0 +1,29 @@ +package com.cbsd.universaltestsoftware_client.timing; + +import com.cbsd.universaltestsoftware_client.service.ChannelDataService; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +@Component +@EnableScheduling +public class ChannelDataTiming { + + @Resource + private ChannelDataService channelDataService; + + /** + * 晚上12点定时清理数据 + */ + @Scheduled(cron = "0 0 0 * * ?") +// @Scheduled(cron = "0 0/1 * * * ?") + public void clearChannelData() { + channelDataService.clearChannelData(); + } + + + + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/util/CalcUtils.java b/src/main/java/com/cbsd/universaltestsoftware_client/util/CalcUtils.java new file mode 100644 index 0000000..f23d794 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/util/CalcUtils.java @@ -0,0 +1,122 @@ +package com.cbsd.universaltestsoftware_client.util; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.zip.Adler32; +import java.util.zip.CRC32; + +public class CalcUtils { + + // 1. 奇偶校验(返回偶校验位) + public static byte parityCheck(byte[] data) { + int count = 0; + for (byte b : data) { + // 统计每个字节中1的个数 + count += Integer.bitCount(b & 0xFF); + } + // 偶校验:如果1的总数是偶数,校验位为0,否则为1 + return (byte) (count % 2 == 0 ? 0 : 1); + } + + // 2. 简单校验和(8位) + public static int simpleChecksum(byte[] data) { + if (data == null || data.length == 0) { + return 0; + } + + int sum = 0; + for (byte b : data) { + sum += b & 0xFF; // 转换为无符号整数 + } + + // 取低16位 + return sum & 0xFFFF; + } + + + // 3. CRC16校验 (Modbus标准) + public static int crc16Checksum(byte[] data) { + int crc = 0xFFFF; // 初始值 + int polynomial = 0xA001; // 多项式反转 (0x8005) + + for (byte b : data) { + crc ^= (b & 0xFF); + for (int i = 0; i < 8; i++) { + if ((crc & 0x0001) != 0) { + crc >>= 1; + crc ^= polynomial; + } else { + crc >>= 1; + } + } + } + return crc; + } + + // 4. CRC32校验 + public static long crc32Checksum(byte[] data) { + CRC32 crc32 = new CRC32(); + crc32.update(data); + return crc32.getValue(); + } + + // 6. MD5校验 + public static String md5Checksum(byte[] data) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] digest = md.digest(data); + + // 转换为十六进制字符串 + StringBuilder sb = new StringBuilder(); + for (byte b : digest) { + sb.append(String.format("%02x", b & 0xFF)); + } + return sb.toString(); + } + + // 7. SHA-256校验 + public static String sha256Checksum(byte[] data) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] digest = md.digest(data); + + // 转换为十六进制字符串 + StringBuilder sb = new StringBuilder(); + for (byte b : digest) { + sb.append(String.format("%02x", b & 0xFF)); + } + return sb.toString(); + } + + /** + * 8. 单字节异或校验 + */ + public static String byteXOR(String hexData) { + // 将组合数据按字节进行异或运算 + int xorResult = 0; + for (int i = 0; i < hexData.length(); i += 2) { // 每次处理两个字符(一个字节) + String byteStr = hexData.substring(i, i + 2); + int byteValue = Integer.parseInt(byteStr, 16); // 将十六进制字符串转换为整数 + xorResult ^= byteValue; // 与当前字节进行异或运算 + } + // 将异或结果扩展为4位十六进制字符串(后面补两个0) + return String.format("%02X", xorResult); + } + + /** + * 根据指定字节数截取/补齐十六进制结果 + * 超长取低位(右截取),不足前面补0 + */ + public static String fitToByteCount(String hexResult, Integer byteCount) { + if (byteCount == null || byteCount <= 0) { + return hexResult; + } + int targetLen = byteCount * 2; + if (hexResult.length() > targetLen) { + // 超长,取低位(取右边 targetLen 个字符) + return hexResult.substring(hexResult.length() - targetLen); + } else if (hexResult.length() < targetLen) { + // 不足,前面补0 + return String.format("%" + targetLen + "s", hexResult).replace(' ', '0'); + } + return hexResult; + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/util/ConverUtils.java b/src/main/java/com/cbsd/universaltestsoftware_client/util/ConverUtils.java new file mode 100644 index 0000000..1741e8d --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/util/ConverUtils.java @@ -0,0 +1,79 @@ +package com.cbsd.universaltestsoftware_client.util; + +import java.util.HashMap; +import java.util.Map; +import java.util.StringJoiner; + +public class ConverUtils { + + /** + * 将 key=value&key=value 格式的字符串转换为 Map + * @param str 待转换的字符串 + * @return 转换后的 Map 集合 + */ + public static Map convertStringToMap(String str) { + Map resultMap = new HashMap<>(); + + // 空值校验 + if (str == null || str.trim().isEmpty()) { + return resultMap; + } + + // 按 & 分割成单个键值对 + String[] keyValuePairs = str.split("&"); + + // 遍历每个键值对 + for (String pair : keyValuePairs) { + // 按 = 分割键和值(限制分割次数为1,避免值中包含=的情况) + String[] keyValue = pair.split("=", 2); + + // 校验分割结果是否合法 + if (keyValue.length == 2) { + String key = keyValue[0].trim(); + String value = keyValue[1].trim(); + resultMap.put(key, value); + } else if (keyValue.length == 1) { + // 处理只有key没有value的情况(如:param=&name=test) + String key = keyValue[0].trim(); + resultMap.put(key, ""); + } + } + + return resultMap; + } + + /** + * 将 Map 还原为 key=value&key=value 格式的字符串 + * @param map 待转换的Map + * @return 还原后的字符串 + */ + public static String convertMapToString(Map map) { + // 空Map直接返回空字符串 + if (map == null || map.isEmpty()) { + return ""; + } + + // 使用StringJoiner简化拼接,分隔符为& + StringJoiner joiner = new StringJoiner("&"); + + // 遍历Map中的每个键值对 + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + + // 处理空键(避免出现=value的情况) + if (key == null || key.trim().isEmpty()) { + continue; + } + // 处理空值(保证key=的格式) + if (value == null) { + value = ""; + } + + // 拼接单个key=value并加入joiner + joiner.add(key.trim() + "=" + value.trim()); + } + + return joiner.toString(); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/util/DataUtils.java b/src/main/java/com/cbsd/universaltestsoftware_client/util/DataUtils.java new file mode 100644 index 0000000..e729363 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/util/DataUtils.java @@ -0,0 +1,274 @@ +package com.cbsd.universaltestsoftware_client.util; + +import java.nio.ByteBuffer; +import java.util.List; + +public class DataUtils { + + public static String byteToHex(byte data) { + int h = (data >>> 4) & 0xF; + int l = data & 0xF; + char ch = (char) ((h < 10) ? ('0' + h) : ('A' + h - 10)); + char cl = (char) ((l < 10) ? ('0' + l) : ('A' + l - 10)); + return String.valueOf(ch) + cl; + } + + public static String byteToHex(byte[] data) { + StringBuilder sb = new StringBuilder(); + for (byte b : data) { + sb.append(byteToHex(b)); + } + return sb.toString(); + } + + /** + * 将字节数组转换为int(大端序,高位在前) + * + * @param bytes 输入字节数组 + * @return 转换后的int值 + */ + public static long toIntBigEndian(byte[] bytes) { + int result = 0; + int length = Math.min(bytes.length, 4); // 只取前4个字节 + + for (int i = 0; i < length; i++) { + result = (result << 8) | (bytes[i] & 0xFF); + } + return result; + } + + /** + * 将字节数组转换为int(小端序,低位在前) + * + * @param bytes 输入字节数组 + * @return 转换后的int值 + */ + public static long toIntLittleEndian(byte[] bytes) { + int result = 0; + int length = Math.min(bytes.length, 4); // 只取前4个字节 + + for (int i = 0; i < length; i++) { + result |= (bytes[i] & 0xFF) << (8 * i); + } + return result; + } + + /** + * byte列表转字节数组 + * + * @param byteList + * @return + */ + public static byte[] byteListToBytes(List byteList) { + if (byteList == null) { + return null; + } + // 创建与列表大小相同的 byte 数组 + byte[] byteArray = new byte[byteList.size()]; + // 逐个将 Byte 对象转换为 byte 基本类型并填入数组 + for (int i = 0; i < byteList.size(); i++) { + // 使用 Byte 的 byteValue () 方法获取基本类型值 + byteArray[i] = byteList.get(i); + } + return byteArray; + } + + /** + * 将double数组转换为byte数组(不丢失精度) + * + * @param doubles 输入的double数组 + * @return 转换后的byte数组 + */ + public static byte[] doublesToBytes(double[] doubles) { + // 每个double占8个字节,计算所需的总字节数 + int byteLength = doubles.length * 8; + // 创建指定容量的ByteBuffer + ByteBuffer buffer = ByteBuffer.allocate(byteLength); + + // 将所有double值放入缓冲区 + for (double d : doubles) { + buffer.putDouble(d); + } + + // 返回缓冲区中的字节数组 + return buffer.array(); + } + + /** + * 将byte数组转换回double数组(还原原始数据) + * + * @param bytes 输入的byte数组 + * @return 转换后的double数组 + * @throws IllegalArgumentException 如果字节数组长度不是8的倍数 + */ + public static double[] bytesToDoubles(byte[] bytes) { + if (bytes.length % 8 != 0) { + throw new IllegalArgumentException("字节数组长度必须是8的倍数"); + } + + // 计算可以转换的double数量 + int doubleLength = bytes.length / 8; + // 创建ByteBuffer并放入字节数组 + ByteBuffer buffer = ByteBuffer.wrap(bytes); + double[] doubles = new double[doubleLength]; + + // 从缓冲区中读取所有double值 + for (int i = 0; i < doubleLength; i++) { + doubles[i] = buffer.getDouble(); + } + + return doubles; + } + + /** + * 将byte转换为8位二进制字符串 + * + * @param b 要转换的byte值 + * @return 8位二进制字符串 + */ + public static String byteToBinary(byte b) { + // 将byte转换为无符号整数,避免符号位扩展 + int unsignedByte = b & 0xFF; + // 转换为二进制字符串 + String binary = Integer.toBinaryString(unsignedByte); + // 确保结果是8位,不足的在前面补0 + while (binary.length() < 8) { + binary = "0" + binary; + } + return binary; + } + + /** + * 判断奇数或偶数,位运算,最后一位是1则为奇数,为0是偶数 + * + * @param num + * @return + */ + public static int isOdd(int num) { + return num & 1; + } + + /** + * 转hex字符串转字节数组 + * + * @param inHex + * @return + */ + public static byte[] HexToByteArr(String inHex) { + byte[] result; + int hexlen = inHex.length(); + if (isOdd(hexlen) == 1) { + hexlen++; + result = new byte[(hexlen / 2)]; + inHex = "0" + inHex; + } else { + result = new byte[(hexlen / 2)]; + } + int j = 0; + for (int i = 0; i < hexlen; i += 2) { + result[j] = HexToByte(inHex.substring(i, i + 2)); + j++; + } + return result; + } + + /** + * Hex字符串转byte + * + * @param inHex + * @return + */ + public static byte HexToByte(String inHex) { + return (byte) Integer.parseInt(inHex, 16); + } + + /** + * 将byte转换为byte类型的bit数组(8位,保留所有0) + * + * @param b 待转换的byte + * @return 长度为8的byte数组,元素为0或1(索引0对应最高位) + */ + public static byte[] byteToByteBitArray(byte b) { + byte[] bits = new byte[8]; + // 遍历8个比特位,从最高位(第7位)到最低位(第0位) + for (int i = 7; i >= 0; i--) { + // 关键修正:将int结果强转为byte,避免隐式转换警告 + bits[7 - i] = (byte) ((b >> i) & 1); + } + return bits; + } + + // 优化版:无需生成单个byte的中间bit数组,直接计算填充 + public static byte[] byteArrayToBitArray(byte[] byteArray) { + if (byteArray == null || byteArray.length == 0) { + return new byte[0]; + } + byte[] resultBits = new byte[byteArray.length * 8]; + int index = 0; + for (byte b : byteArray) { + // 直接遍历8位,计算后填充到结果数组,省去中间数组 + for (int i = 7; i >= 0; i--) { + resultBits[index++] = (byte) ((b >> i) & 1); + } + } + return resultBits; + } + + /** + * 将bit数组转换为byte数组, 填充0 + * + * @param bitArray 输入的bit数组, 如果长度不足8的倍数,则在前部填充0 + * @return 转换后的byte数组 + */ + public static byte[] bitArrayToByteArray(byte[] bitArray) { + if (bitArray == null || bitArray.length == 0) { + return new byte[0]; + } + if(bitArray.length % 8 != 0) { + // 创建一个与bitArray长度相同的byte数组,用于填充0 + byte[] paddedBitArray = new byte[bitArray.length + (8 - bitArray.length % 8)]; + // 将bitArray复制到paddedBitArray中,并填充0 + System.arraycopy(bitArray, 0, paddedBitArray, paddedBitArray.length - bitArray.length, bitArray.length); + bitArray = paddedBitArray; + } + int byteLength = bitArray.length / 8; + byte[] result = new byte[byteLength]; + for (int i = 0; i < byteLength; i++) { + result[i] = (byte) ((bitArray[i * 8] << 7) | + (bitArray[i * 8 + 1] << 6) | + (bitArray[i * 8 + 2] << 5) | + (bitArray[i * 8 + 3] << 4) | + (bitArray[i * 8 + 4] << 3) | + (bitArray[i * 8 + 5] << 2) | + (bitArray[i * 8 + 6] << 1) | + (bitArray[i * 8 + 7])); + } + return result; + } + + /** + * 将bit数组转化为二进制字符串,不足则补0 + * @param bits 比特数组 + * @return 二进制字符串 + */ + public static String bitsToBinaryString(byte[] bits) { + StringBuilder sb = new StringBuilder(); + for (byte bit : bits) { + sb.append(bit); + if (sb.length() % 8 == 0) { + sb.append(" "); + } + } + //不足八位,前面补0 +// while (sb.length() % 8 != 0) { +// sb.insert(0, "0"); +// } + return sb.toString(); + } + + public static void main(String[] args) { + byte[] bytes = new byte[]{1,0,1,0}; + String result = bitsToBinaryString(bytes); + System.out.println(result); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/util/DateUtils.java b/src/main/java/com/cbsd/universaltestsoftware_client/util/DateUtils.java new file mode 100644 index 0000000..8fae03e --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/util/DateUtils.java @@ -0,0 +1,51 @@ +package com.cbsd.universaltestsoftware_client.util; + +import com.baomidou.mybatisplus.core.toolkit.StringUtils; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +public class DateUtils { + + public static String dateToString() { + return dateToString(null, null); + } + + public static String dateToString(String pattern) { + return dateToString(null, pattern); + } + + public static String dateToString(Date date, String pattern) { + if (date == null) { + date = new Date(); + } + if (StringUtils.isBlank(pattern)) { + pattern = "yyyy-MM-dd HH:mm:ss"; + } + SimpleDateFormat normalFormat = new SimpleDateFormat(pattern, Locale.CHINA); + return normalFormat.format(date); + } + + public static String timestampToDateString(long timestamp) { + return timestampToDateString(timestamp, null); + } + + public static String timestampToDateString(long timestamp, String pattern) { + Date date = new Date(timestamp); + return dateToString(date, pattern); + } + + public static long stringToTimestamp(String dateString) { + if (StringUtils.isBlank(dateString)) { + return 0; + } + SimpleDateFormat normalFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA); + try { + Date date = normalFormat.parse(dateString); + return date.getTime(); + } catch (Exception e) { + return 0; + } + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/util/DynamicClassLoader.java b/src/main/java/com/cbsd/universaltestsoftware_client/util/DynamicClassLoader.java new file mode 100644 index 0000000..290f554 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/util/DynamicClassLoader.java @@ -0,0 +1,13 @@ +package com.cbsd.universaltestsoftware_client.util; + +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Paths; + +public class DynamicClassLoader extends URLClassLoader { + public DynamicClassLoader(String rootPath) throws Exception { + super(new URL[]{ Paths.get(rootPath).toUri().toURL() }, + Thread.currentThread().getContextClassLoader()); + } +} + diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/util/FileUtils.java b/src/main/java/com/cbsd/universaltestsoftware_client/util/FileUtils.java new file mode 100644 index 0000000..49b1677 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/util/FileUtils.java @@ -0,0 +1,37 @@ +package com.cbsd.universaltestsoftware_client.util; + +import org.springframework.boot.system.ApplicationHome; + +import java.io.File; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.security.CodeSource; +import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class FileUtils { + + /** + * 获取目录下的所有文件 + * + * @param path 目录地址 + */ + public static List getDirectoryFiles(String path) { + List fileList = new ArrayList<>(); + File dic = new File(path); + if (dic.exists() && dic.isDirectory() && dic.listFiles() != null) { + for (File file : Objects.requireNonNull(dic.listFiles())) { + if (file.isFile()) { + fileList.add(file); + } + } + } + return fileList; + } + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/util/MediaTypeUtils.java b/src/main/java/com/cbsd/universaltestsoftware_client/util/MediaTypeUtils.java new file mode 100644 index 0000000..bb2a33f --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/util/MediaTypeUtils.java @@ -0,0 +1,56 @@ +package com.cbsd.universaltestsoftware_client.util; + + +import com.baomidou.mybatisplus.core.toolkit.StringUtils; + +public class MediaTypeUtils { + + /** + * 根据文件后缀获取MIME 类型,默认:text/html + * @param suffix 文件后缀,包含点(.) + * @return MIME 类型 + */ + public static String getMediaType(String suffix) { + + if (StringUtils.isNotBlank(suffix)){ + switch (suffix.toLowerCase()) { + case ".jpg": + case ".jpeg": + return "image/jpeg"; + case ".png": + return "image/png"; + case ".gif": + return "image/gif"; + case ".aac": + return "audio/aac"; + case ".mp3": + return "audio/mpeg"; + case ".mp4": + return "video/mp4"; + case ".mpeg": + return "video/mpeg"; + case ".txt": + return "text/plain"; + case ".json": + return "application/json"; + case ".doc": + return "application/msword"; + case ".docx": + return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; + case ".xls": + return "application/vnd.ms-excel"; + case ".xlsx": + return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + case ".ppt": + return "application/vnd.ms-powerpoint"; + case ".pptx": + return "application/vnd.openxmlformats-officedocument.presentationml.presentation"; + case ".pdf": + return "application/pdf"; + case ".apk": + return "application/vnd.android.package-archive"; + } + } + return "text/html"; + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/util/ModbusCRC16Utils.java b/src/main/java/com/cbsd/universaltestsoftware_client/util/ModbusCRC16Utils.java new file mode 100644 index 0000000..baa59d6 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/util/ModbusCRC16Utils.java @@ -0,0 +1,89 @@ +package com.cbsd.universaltestsoftware_client.util; + + +public class ModbusCRC16Utils { + + /** + * 计算Modbus CRC16校验值 + * + * @param data 待校验的数据 + * @return CRC16校验值,2字节数组 + */ + public static byte[] calculateCRC(byte[] data) { + int crc = 0xFFFF; + for (byte b : data) { + crc ^= (b & 0xFF); + for (int i = 0; i < 8; i++) { + if ((crc & 0x0001) != 0) { + crc >>= 1; + crc ^= 0xA001; + } else { + crc >>= 1; + } + } + } + // 将CRC转换为字节数组(低字节在前,高字节在后) + byte[] result = new byte[2]; + result[0] = (byte) (crc & 0xFF); + result[1] = (byte) ((crc >> 8) & 0xFF); + + return result; + } + + /** + * 计算Modbus CRC16校验值并以十六进制字符串形式返回 + * + * @param data 待校验的数据 + * @return CRC16校验值的十六进制字符串 + */ + public static String calculateCRCToHexString(byte[] data) { + byte[] crcBytes = calculateCRC(data); + return String.format("%02X%02X", crcBytes[0] & 0xFF, crcBytes[1] & 0xFF); + } + + /** + * 获取源数据和验证码的组合byte数组 + * + * @param strings 可变长度的十六进制字符串 + */ +// public static byte[] getData(String... strings) { +// byte[] data = DataUtils.getData(strings); +// return getData(data); +// } + + /** + * 获取源数据和验证码的组合byte数组 + * + * @param bytes 字节数组 + */ + private static byte[] getData(byte[] bytes) { + byte[] bb = calculateCRC(bytes); + byte[] cc = new byte[bytes.length + bb.length]; + System.arraycopy(bytes, 0, cc, 0, bytes.length); + System.arraycopy(bb, 0, cc, bytes.length, bb.length); + return cc; + } + + /** + * 验证数据的CRC16校验值 + * + * @param data 包含CRC校验值的数据(CRC在数据末尾,2字节) + * @return 校验结果,true表示校验通过,false表示校验失败 + */ + public static boolean verifyCRC(byte[] data) { + if (data == null || data.length < 2) { + return false; + } + + // 提取数据部分(不包含CRC) + byte[] dataWithoutCRC = new byte[data.length - 2]; + System.arraycopy(data, 0, dataWithoutCRC, 0, data.length - 2); + + // 计算CRC + byte[] calculatedCRC = calculateCRC(dataWithoutCRC); + + // 比较计算得到的CRC与数据中包含的CRC + return calculatedCRC[0] == data[data.length - 2] && + calculatedCRC[1] == data[data.length - 1]; + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/util/ProtocolToolUtils.java b/src/main/java/com/cbsd/universaltestsoftware_client/util/ProtocolToolUtils.java new file mode 100644 index 0000000..761a96d --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/util/ProtocolToolUtils.java @@ -0,0 +1,37 @@ +package com.cbsd.universaltestsoftware_client.util; + +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.cbsd.universaltestsoftware_client.parser.model.Filed; +import com.cbsd.universaltestsoftware_client.parser.model.ProtocolXml; +import org.springframework.stereotype.Component; + +import javax.xml.bind.JAXBException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + + +public class ProtocolToolUtils { + + + + /** + * 解析协议参数 + * @param protocolContent + * @return + */ + public static List getProtocolParameter(String protocolContent) throws JAXBException { + List parameter = new ArrayList<>(); + ProtocolXml protocolXml = XmlToObjectConverter.convertXmlToObject(protocolContent, ProtocolXml.class); + if (protocolXml != null) { + List fileds = protocolXml.getFileds(); + fileds.stream().sorted(Comparator.comparingInt(Filed::getSnInt)); + if (CollectionUtils.isNotEmpty(fileds)) { + for (Filed filed : fileds) { + parameter.add(filed.getReadName()); + } + } + } + return parameter; + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/util/Result.java b/src/main/java/com/cbsd/universaltestsoftware_client/util/Result.java new file mode 100644 index 0000000..d92db8e --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/util/Result.java @@ -0,0 +1,105 @@ +package com.cbsd.universaltestsoftware_client.util; + +import com.alibaba.fastjson.JSON; +import com.cbsd.universaltestsoftware_client.exception.BaseException; +import com.cbsd.universaltestsoftware_client.exception.CustomizeException; + + +/** + * 统一API响应结果封装 + * + * @author LH @date 2018年11月14日 + */ +public class Result { + /** + * 编码 + */ + private Integer code; + /** + * 描述 + */ + private String msg; + + /** + * 业务数据 + */ + + + + private T data; + + public Integer getCode() { + return code; + } + + public Result setCode(Integer code) { + this.code = code; + return this; + } + + public String getMsg() { + return msg; + } + + public Result setMsg(String msg) { + this.msg = msg; + return this; + } + + public T getData() { + return data; + } + + public Result setData(T data) { + this.data = data; + return this; + } + + @Override + public String toString() { + return JSON.toJSONString(this); + } + + public static Result success() { + return new Result().setCode(ResultCode.OK).setMsg("操作成功"); + } + + public static Result success(T data) { + return new Result().setCode(ResultCode.OK).setMsg("操作成功").setData(data); + } + public static Result success(T data,String msg) { + return new Result().setCode(ResultCode.OK).setMsg(msg).setData(data); + } + + public static Result error() { + return new Result().setCode(ResultCode.SYSTEM_ERROR).setMsg("系统错误"); + } + + public static Result error(String message) { + return new Result().setCode(ResultCode.SYSTEM_ERROR).setMsg(message); + } + + public static Result error(Integer code, String message) { + return new Result().setCode(code).setMsg(message); + } + public static Result error(Integer code, String message,T data) { + return new Result().setCode(code).setMsg(message).setData(data); + } + + // added by qidongbo 20211210 直接传入异常对象 + public static Result error(CustomizeException exception) { + return new Result().setCode(Integer.parseInt(exception.getCode())).setMsg(exception.getMessage()); + } + + public static Result throwError() { + return throwError(ResultCode.SYSTEM_ERROR,"系统错误"); + } + + public static Result throwError(String message) { + return throwError(ResultCode.SYSTEM_ERROR,message); + } + + public static Result throwError(Integer code,String message) { + throw new BaseException(code,message); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/util/ResultCode.java b/src/main/java/com/cbsd/universaltestsoftware_client/util/ResultCode.java new file mode 100644 index 0000000..e15ebda --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/util/ResultCode.java @@ -0,0 +1,263 @@ +package com.cbsd.universaltestsoftware_client.util; + +/** + *

+ * 全局请求返回状态标识 + *

+ * + * @author chenlin + * @date 2019-10-29 + */ +public final class ResultCode { + + //========================全局请求返回状态标识开始======================= + /** + * 操作成功 + */ + public static final int OK = 1; + /** + * 用户未登陆 + */ + public static final int NOT_LOGIN = -1; + /** + * 不能为空 + */ + public static final int CAN_NOT_NULL = -2; + /** + * 参数类型错误 + */ + public static final int PARAMER_TYPE_ERROR = -3; + /** + * 未知错误 + */ + public static final int UNKNOW_ERROR = -4; + /** + * 系统错误 + */ + public static final int SYSTEM_ERROR = -5; +// /** +// * access_token 错误 +// */ +// public static final int ACCESS_TOKEN_ERROR = -6; + /** + * 参数超长 + */ + public static final int TOO_LANG = -7; + /** + * 暂无数据 + */ + public static final int NO_DATA = -8; + /** + * 前端不需要处理的错误 + */ + public static final int NOT_NEED_DEAL = -9; + /** + * 签名错误 + */ + public static final int SIGN_ERROR = -10; + + /** + * 枚举错误 + */ + public static final int ENUM_ERROR = -11; + + /** + *excel操作异常 + */ + public static final int EXCEL_ABNORMAL = -12; + + /** + * 商务部cookie过期 + */ + public static final int MINISTRY_COMMERCE_COOKIE = -13; + /** + * 用户权限不足 + */ + public static final int INSUFFCIENT_PERMISSIONS = -16; + + /** + * 重复提交 + */ + public static final int REPEAT_COMMIT = -17; + + /** + * 版本错误 + */ + public static final int VERTION_ERROR = -18; + + /** + * 公司信息未完善 + */ + public static final int NOT_PERFECT = -19; + /** + * 审核状态错误 + */ + public static final int STATE_ERROR = -21; + /** + * 已超限 + */ + public static final int OVERRUN = -22; + /** + * 重复数据 + */ + public static final int REPEATING_DATA = -23; + + + public static final int DATA_BASE_TIMEOUT = -24; + + public static final int DATA_BASE_ERROR = -25; + + /** + * 低于等于10 + */ + public static final int LOWER_ten=-26; + + /** + * 连续超过5次登录失败 + */ + public static final int ACOUNT_INVALID_FIVE = 100018; + /** + + /** + * 低于等于0 + */ + public static final int LOWER_zero=-27; + + //========================全局请求返回状态标识结束======================= + + //========================接口返回状态标识开始========================== + + /** + * 错误的手机号格式 + */ + public static final int NOT_TRUE_MOBILE = 100001; + /** + * 验证码错误 + */ + public static final int SMS_CODE_ERROR = 100002; + /** + * 用户不存在 + */ + public static final int USER_NOT_EXIST = 100003; + /** + * 密码错误 + */ + public static final int PASSWORD_ERROR = 100004; + /** + * appid或secret错误 + */ + public static final int TOKEN_USER_NOT_VALID = 100005; + /** + * 原始密码错误 + */ + public static final int OLD_PASSWORD_ERROR = 100006; + + /** + * 图形验证码错误 + */ + public static final int PIC_VERIFICATION_ERROR = 100008; + /** + * 错误的邮箱格式 + */ + public static final int NOT_TRUE_EMAIL = 100009; + /** + * 邮箱不存在 + */ + public static final int EMAIL_ISNOT_BIND = 100010; + /** + * 手机号已经注册 + */ + public static final int MOBILE_IS_REGIST = 100011; + /** + * 手机号未注册 + */ + public static final int MOBILE_ISNOT_REGIST = 100012; + /** + * 邮箱已经绑定 + */ + public static final int EMAIL_IS_BINDED = 100013; + /** + * 邮箱是当前用户已经绑定的邮箱 + */ + public static final int EMAIL_IS_CURRENT = 100015; + /** + * 账号被禁用 + */ + public static final int ACOUNT_INVALID = 100014; + /** + * 原密码和新密码相同 + */ + public static final int PASSWORD_SAME = 100016; + /** + * 名字重复 + */ + public static final int NAME_SAME = 100017; + /** + * 参数取值错误 + */ + public static final int PARAMETER_ERROR = 100031; + + + ///////////////上传资料错误码开始//////////////////////////// + /** + * 上传资料不是图片 + */ + public static final int IS_NOT_PIC = 110001; + /** + * 上传资料格式不对 + */ + public static final int FORMAT_IS_FALSE = 110002; + /** + * 上传头像尺寸不对不是图片 + */ + public static final int PIXEL_RATIO_EXCEPTION = 110003; + ///////////////上传资料错误码开始//////////////////////////// + + //========================接口返回状态标识结束========================== + /** + * 用户类型错误 + */ + public static final int USER_TYPE_ERROR = 100025; + /** + * 手机号未注册 + */ + public static final int NOT_PHONE = 100026; + + /** + * 测试版本 + */ + public static final int TEST_VERSION = 6666; + /** + * 过期版本 + */ + public static final int EXPIRED_VERSION = -9999; + /** + * OK版本 + */ + public static final int OK_VERSION = 1111; + /** + * 就餐失败 + */ + public static final int Not_jiuCan = 1001; + + /** + * REDIS服务器错误 + */ + public static final int REDIS_NOT = 50002; + + /** + * 托盘未绑定 + */ + public static final int PLATE_NOT_BIND = 50003; + + /** + * 租户Id未传 + */ + public static final int NOT_BIND_PLATE = 50004; + + /** + * 请求异常 + */ + public static final int REQUEST_ERROR = 50005; + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/util/SplitUtils.java b/src/main/java/com/cbsd/universaltestsoftware_client/util/SplitUtils.java new file mode 100644 index 0000000..b64ecb5 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/util/SplitUtils.java @@ -0,0 +1,25 @@ +package com.cbsd.universaltestsoftware_client.util; + +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.List; + +public class SplitUtils { + + public static List splitStringByLength(String str, int length) { + List result = new ArrayList<>(); + // 处理空字符串或负数长度的情况 + if (StringUtils.isBlank(str) || length <= 0) { + result.add(str); + } else { + // 按照指定长度拆分字符串 + for (int i = 0; i < str.length(); i += length) { + // 计算结束索引,避免越界 + int endIndex = Math.min(i + length, str.length()); + result.add(str.substring(i, endIndex)); + } + } + return result; + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/util/UUIDUtils.java b/src/main/java/com/cbsd/universaltestsoftware_client/util/UUIDUtils.java new file mode 100644 index 0000000..49ceb33 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/util/UUIDUtils.java @@ -0,0 +1,21 @@ +package com.cbsd.universaltestsoftware_client.util; + +import java.util.UUID; + +/** + *

+ * 字符串工具类 + *

+ * + * @author chenlin + * @date 2019-10-29 + */ +public class UUIDUtils { + /** + * 获取uuid字符串 + * @return + */ + public static String getUUID() { + return UUID.randomUUID().toString().replaceAll("-", "").toLowerCase(); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/util/XmlToObjectConverter.java b/src/main/java/com/cbsd/universaltestsoftware_client/util/XmlToObjectConverter.java new file mode 100644 index 0000000..922dbb7 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/util/XmlToObjectConverter.java @@ -0,0 +1,40 @@ +package com.cbsd.universaltestsoftware_client.util; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +import java.io.StringReader; +import java.io.StringWriter; + +public class XmlToObjectConverter { + public static T convertXmlToObject(String xmlString, Class clazz) throws JAXBException { + + // 创建JAXB上下文 + JAXBContext jaxbContext = JAXBContext.newInstance(clazz); + + // 创建Unmarshaller + Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); + + // 使用StringReader读取XML字符串并转换为对象 + try (StringReader reader = new StringReader(xmlString)) { + return clazz.cast(unmarshaller.unmarshal(reader)); + } + } + + /** + * 实体类转XML字符串 + */ + public static String convertObjectToXml(T entity) throws JAXBException { + JAXBContext jaxbContext = JAXBContext.newInstance(entity.getClass()); + Marshaller marshaller = jaxbContext.createMarshaller(); + + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); + marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8"); + + StringWriter writer = new StringWriter(); + marshaller.marshal(entity, writer); + + return writer.toString().replace("", ""); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/util/YamlUtils.java b/src/main/java/com/cbsd/universaltestsoftware_client/util/YamlUtils.java new file mode 100644 index 0000000..2cadaee --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/util/YamlUtils.java @@ -0,0 +1,86 @@ +package com.cbsd.universaltestsoftware_client.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.dataformat.javaprop.JavaPropsMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; + +import java.io.IOException; +import java.util.Map; +import java.util.Properties; + +/** + * + * yaml转换工具 + * + * @author 00fly + * @version [版本号, 2023年4月25日] + * @see [相关类/方法] + * @since [产品/模块版本] + */ +public final class YamlUtils +{ + private static YAMLMapper yamlMapper = new YAMLMapper(); + + private static JavaPropsMapper javaPropsMapper = new JavaPropsMapper(); + + private YamlUtils() + { + super(); + } + + /** + * yaml转Json字符串 + * + * @param yamlContent + * @return + * @throws IOException + */ + public static String yamlToJson(String yamlContent) + throws IOException + { + JsonNode jsonNode = yamlMapper.readTree(yamlContent); + return jsonNode.toPrettyString(); + } + + /** + * yaml转Map + * + * @param yamlContent + * @return + * @throws IOException + */ + public static Map yamlToMap(String yamlContent) + throws IOException + { + JsonNode jsonNode = yamlMapper.readTree(yamlContent); + return javaPropsMapper.writeValueAsMap(jsonNode); + } + + /** + * yaml转properties + * + * @param yamlContent + * @return + * @throws IOException + */ + public static Properties yamlToProperties(String yamlContent) + throws IOException + { + JsonNode jsonNode = yamlMapper.readTree(yamlContent); + return javaPropsMapper.writeValueAsProperties(jsonNode); + } + + /** + * yaml转properties字符串 + * + * @param yamlContent + * @return + * @throws IOException + */ + public static String yamlToPropText(String yamlContent) + throws IOException + { + JsonNode jsonNode = yamlMapper.readTree(yamlContent); + return javaPropsMapper.writeValueAsString(jsonNode); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/vo/BoardListVo.java b/src/main/java/com/cbsd/universaltestsoftware_client/vo/BoardListVo.java new file mode 100644 index 0000000..cb8180e --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/vo/BoardListVo.java @@ -0,0 +1,34 @@ +package com.cbsd.universaltestsoftware_client.vo; + +import com.baomidou.mybatisplus.annotation.TableField; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import java.util.List; + +@Data +public class BoardListVo { + @ApiModelProperty("板卡id") + private String boardId; + + @ApiModelProperty("板卡类型Id") + private String boardTypeId; + + @ApiModelProperty("板卡类型名称 用户看") + @TableField(exist = false) + private String boardTypeName; + + @ApiModelProperty("板卡名称") + private String boardName; + + @ApiModelProperty("板卡序号") + private String boardIndex; + + + @ApiModelProperty("所有的通道") + private List boardListVos; + + + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/vo/ChannelDataHistoryVo.java b/src/main/java/com/cbsd/universaltestsoftware_client/vo/ChannelDataHistoryVo.java new file mode 100644 index 0000000..8df86a6 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/vo/ChannelDataHistoryVo.java @@ -0,0 +1,45 @@ +package com.cbsd.universaltestsoftware_client.vo; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +public class ChannelDataHistoryVo { + + @ApiModelProperty("通道历史数据id") + @TableId(value = "channel_data_history_id", type = IdType.ASSIGN_UUID) + private String channelDataHistoryId; + + @ApiModelProperty("通道id") + private String channelId; + + @ApiModelProperty("创建时间") + private String createTime; + + + @ApiModelProperty("是否删除0否1是") + private String isDelete; + + @ApiModelProperty("数据信息") + private byte[] content; + + @ApiModelProperty("数据字符串") + @TableField(exist = false) + private String contentString; + + @ApiModelProperty("时间戳") + private Long timestamp; + + /** + * 额外数据 + */ + @ApiModelProperty("额外数据") + private String extraData; + + @ApiModelProperty("通道名字") + @TableField(exist = false) + private String channelName; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/vo/ChannelDataStatisticsVo.java b/src/main/java/com/cbsd/universaltestsoftware_client/vo/ChannelDataStatisticsVo.java new file mode 100644 index 0000000..481f10f --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/vo/ChannelDataStatisticsVo.java @@ -0,0 +1,21 @@ +package com.cbsd.universaltestsoftware_client.vo; + +import lombok.Data; + +import java.util.List; + +@Data +public class ChannelDataStatisticsVo { + + private String channelId; + + private Integer dataByteCount; + + private List protocolStatisticsList; + + @Data + public static class ProtocolStatisticsVo { + private String protocolId; + private Integer parseCount; + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/vo/ChannelDataVo.java b/src/main/java/com/cbsd/universaltestsoftware_client/vo/ChannelDataVo.java new file mode 100644 index 0000000..0c092a3 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/vo/ChannelDataVo.java @@ -0,0 +1,21 @@ +package com.cbsd.universaltestsoftware_client.vo; + +import com.cbsd.universaltestsoftware_client.parser.model.FiledParseData; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.List; + +@Data +public class ChannelDataVo { + + private String channelId; + + private List filedParseDataList; + + private String protocolContent; + + private long timestamp; + + private String dataBoardId; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/vo/ChannelListVo.java b/src/main/java/com/cbsd/universaltestsoftware_client/vo/ChannelListVo.java new file mode 100644 index 0000000..396666d --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/vo/ChannelListVo.java @@ -0,0 +1,13 @@ +package com.cbsd.universaltestsoftware_client.vo; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +public class ChannelListVo { + @ApiModelProperty("通道id") + private String channelId; + + @ApiModelProperty("通道名称") + private String channelName; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/vo/ExportDataBoardVo.java b/src/main/java/com/cbsd/universaltestsoftware_client/vo/ExportDataBoardVo.java new file mode 100644 index 0000000..ecc9b94 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/vo/ExportDataBoardVo.java @@ -0,0 +1,31 @@ +package com.cbsd.universaltestsoftware_client.vo; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +public class ExportDataBoardVo { + + + @ApiModelProperty("通道名字") + private String channelName; + + @ApiModelProperty("协议名字") + private String protocolName; + + @ApiModelProperty("数据字段") + private String dataField; + + @ApiModelProperty("源数据") + private String sourceData; + + @ApiModelProperty("物理解析值") + private String thingValue; + + @ApiModelProperty("物理单位 ") + private String physicalUnit; + + @ApiModelProperty("时间") + private String time; + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/vo/InstructScriptVo.java b/src/main/java/com/cbsd/universaltestsoftware_client/vo/InstructScriptVo.java new file mode 100644 index 0000000..26ad9be --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/vo/InstructScriptVo.java @@ -0,0 +1,16 @@ +package com.cbsd.universaltestsoftware_client.vo; + +import lombok.Data; + +@Data +public class InstructScriptVo { + + private String instructId; + + private String scriptType; + + private String scriptFile; + + private String scriptName; + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/vo/InvocationInstructVo.java b/src/main/java/com/cbsd/universaltestsoftware_client/vo/InvocationInstructVo.java new file mode 100644 index 0000000..f1bc844 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/vo/InvocationInstructVo.java @@ -0,0 +1,17 @@ +package com.cbsd.universaltestsoftware_client.vo; + +import com.cbsd.universaltestsoftware_client.entity.Instruct; +import lombok.Data; + +import java.util.List; + +@Data +public class InvocationInstructVo { + + private String invocationId; + + private String invocationName; + + private List instructList; + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/vo/ProtocolSaveVo.java b/src/main/java/com/cbsd/universaltestsoftware_client/vo/ProtocolSaveVo.java new file mode 100644 index 0000000..d69b9cb --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/vo/ProtocolSaveVo.java @@ -0,0 +1,11 @@ +package com.cbsd.universaltestsoftware_client.vo; + +import com.cbsd.universaltestsoftware_client.entity.Protocol; +import com.cbsd.universaltestsoftware_client.parser.model.ProtocolXml; +import lombok.Data; + +@Data +public class ProtocolSaveVo { + private Protocol protocol; + private ProtocolXml parse; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/vo/ScriptTypeVo.java b/src/main/java/com/cbsd/universaltestsoftware_client/vo/ScriptTypeVo.java new file mode 100644 index 0000000..85857a1 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/vo/ScriptTypeVo.java @@ -0,0 +1,15 @@ +package com.cbsd.universaltestsoftware_client.vo; + +import lombok.Data; + +@Data +public class ScriptTypeVo { + + private String scriptType; + + private String scriptTypeName; + + private String scriptDirectoryPath; + + private String scriptTemplateContent; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/vo/ScriptVo.java b/src/main/java/com/cbsd/universaltestsoftware_client/vo/ScriptVo.java new file mode 100644 index 0000000..ed1f5b6 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/vo/ScriptVo.java @@ -0,0 +1,21 @@ +package com.cbsd.universaltestsoftware_client.vo; + +import lombok.Data; + +@Data +public class ScriptVo { + + private String fileName; + + private String filePath; + + private String scriptName; + + private String scriptType; + + private String scriptComment; + + private int compileStatus; + + private String lastModifiedTime; +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/vo/ServerListVo.java b/src/main/java/com/cbsd/universaltestsoftware_client/vo/ServerListVo.java new file mode 100644 index 0000000..8dcccb6 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/vo/ServerListVo.java @@ -0,0 +1,36 @@ +package com.cbsd.universaltestsoftware_client.vo; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import java.util.List; + +@Data +public class ServerListVo { + + @ApiModelProperty("服务id") + private String serverId; + + @ApiModelProperty("服务端名字") + private String serverName; + + @ApiModelProperty("ip地址") + private String ipAddress; + + @ApiModelProperty("服务端tcp端口") + private Integer tcpPort; + + @ApiModelProperty("服务端http端口") + private Integer httpPort; + + @ApiModelProperty("所有板卡") + private List boardListVos; + + + + + + + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/websocket/DataPlayBackWebSocketConfig.java b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/DataPlayBackWebSocketConfig.java new file mode 100644 index 0000000..f964bb0 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/DataPlayBackWebSocketConfig.java @@ -0,0 +1,24 @@ +package com.cbsd.universaltestsoftware_client.websocket; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; +@Configuration +@EnableWebSocket +public class DataPlayBackWebSocketConfig implements WebSocketConfigurer { + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + // 注册WebSocket处理器,映射路径为/ws/chat + registry.addHandler(dataPlayBackWebSocketHandler(), "/ws/playback") + .setAllowedOrigins("*"); // 允许所有来源,生产环境应指定具体域名 + } + + @Bean + public DataPlayBackWebSocketHandler dataPlayBackWebSocketHandler() { + return new DataPlayBackWebSocketHandler(); + } + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/websocket/DataPlayBackWebSocketHandler.java b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/DataPlayBackWebSocketHandler.java new file mode 100644 index 0000000..ffae606 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/DataPlayBackWebSocketHandler.java @@ -0,0 +1,74 @@ +package com.cbsd.universaltestsoftware_client.websocket; + +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class DataPlayBackWebSocketHandler extends TextWebSocketHandler { + + // 存储所有连接的会话,key为"IP:端口",value为WebSocketSession + public static final Map sessions = new ConcurrentHashMap<>(); + + @Override + public void afterConnectionEstablished(WebSocketSession session) throws Exception { + // 获取客户端IP和端口 + String clientId = getClientId(session); + + // 存储会话 + sessions.put(clientId, session); + + } + + @Override + protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { + // 获取发送者标识 + String payload = message.getPayload(); + System.out.println("收到客户端消息: " + payload); + String senderId = getClientId(session); + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { + // 获取客户端标识 + String clientId = getClientId(session); + + // 移除会话 + sessions.remove(clientId); + } + + /** + * 获取客户端唯一标识(IP:端口) + */ + private String getClientId(WebSocketSession session) { + InetSocketAddress remoteAddress = session.getRemoteAddress(); + if (remoteAddress != null) { + return remoteAddress.getAddress().getHostAddress() + ":" + remoteAddress.getPort(); + } + return "unknown:" + session.getId(); + } + + /** + * 广播消息给所有连接的客户端 + */ + public void broadcast(String message) { + Set clientIds = sessions.keySet(); + + for (String clientId : clientIds) { + WebSocketSession session = sessions.get(clientId); + if (session != null && session.isOpen()) { + try { + session.sendMessage(new TextMessage(message)); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/websocket/DataWebNettyWebSocket.java b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/DataWebNettyWebSocket.java new file mode 100644 index 0000000..7b0f7e9 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/DataWebNettyWebSocket.java @@ -0,0 +1,54 @@ +package com.cbsd.universaltestsoftware_client.websocket; + +import io.netty.handler.codec.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.util.MultiValueMap; +import org.yeauty.annotation.*; +import org.yeauty.pojo.Session; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +//@Component +//@EnableWebSocket +//@ServerEndpoint(port = "8083",path = "/universaltestsoftware_client_api/nettyWs/data") +public class DataWebNettyWebSocket { + + private static final Map sessionMap = new ConcurrentHashMap<>(); + + @BeforeHandshake + public void handshake(Session session, HttpHeaders headers, @RequestParam String req, @RequestParam MultiValueMap reqMap, @PathVariable String arg, @PathVariable Map pathMap){ + + } + + @OnOpen + public void onOpen(Session session, HttpHeaders headers, @RequestParam String req, @RequestParam MultiValueMap reqMap, @PathVariable String arg, @PathVariable Map pathMap){ + String clientId = session.remoteAddress().toString(); + sessionMap.put(clientId, session); + } + + @OnClose + public void onClose(Session session) throws IOException { + String clientId = session.remoteAddress().toString(); + sessionMap.remove(clientId); + } + + @OnError + public void onError(Session session, Throwable throwable) { + throwable.printStackTrace(); + } + + @OnMessage + public void onMessage(Session session, String message) { + + } + + public void sendMessage(String message) { + for (Session session : sessionMap.values()) { + if (session != null && session.isOpen()){ + session.sendText(message); + } + } + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/websocket/DataWebSocketConfig.java b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/DataWebSocketConfig.java new file mode 100644 index 0000000..7466dc0 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/DataWebSocketConfig.java @@ -0,0 +1,24 @@ +package com.cbsd.universaltestsoftware_client.websocket; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; + +@Configuration +@EnableWebSocket +public class DataWebSocketConfig implements WebSocketConfigurer { + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + // 注册WebSocket处理器,映射路径为/ws/chat + registry.addHandler(dataWebSocketHandler(), "/ws/data") + .setAllowedOrigins("*"); // 允许所有来源,生产环境应指定具体域名 + } + + @Bean + public DataWebSocketHandler dataWebSocketHandler() { + return new DataWebSocketHandler(); + } + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/websocket/DataWebSocketHandler.java b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/DataWebSocketHandler.java new file mode 100644 index 0000000..3eefbbc --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/DataWebSocketHandler.java @@ -0,0 +1,71 @@ +package com.cbsd.universaltestsoftware_client.websocket; + +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class DataWebSocketHandler extends TextWebSocketHandler { + + // 存储所有连接的会话,key为"IP:端口",value为WebSocketSession + private static final Map sessions = new ConcurrentHashMap<>(); + + @Override + public void afterConnectionEstablished(WebSocketSession session) throws Exception { + // 获取客户端IP和端口 + String clientId = getClientId(session); + + // 存储会话 + sessions.put(clientId, session); + } + + @Override + protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { + // 获取发送者标识 + String payload = message.getPayload(); + String senderId = getClientId(session); + + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { + // 获取客户端标识 + String clientId = getClientId(session); + + // 移除会话 + sessions.remove(clientId); + } + + /** + * 获取客户端唯一标识(IP:端口) + */ + private String getClientId(WebSocketSession session) { + InetSocketAddress remoteAddress = session.getRemoteAddress(); + if (remoteAddress != null) { + return remoteAddress.getAddress().getHostAddress() + ":" + remoteAddress.getPort(); + } + return "unknown:" + session.getId(); + } + + /** + * 广播消息给所有连接的客户端 + */ + public void broadcast(String message) { + synchronized (sessions) { + for (WebSocketSession session : sessions.values()) { + if (session != null && session.isOpen()) { + try { + session.sendMessage(new TextMessage(message)); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/websocket/NoticeWebSocketConfig.java b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/NoticeWebSocketConfig.java new file mode 100644 index 0000000..8abd2fb --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/NoticeWebSocketConfig.java @@ -0,0 +1,23 @@ +package com.cbsd.universaltestsoftware_client.websocket; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; +@Configuration +@EnableWebSocket +public class NoticeWebSocketConfig implements WebSocketConfigurer { + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + // 注册WebSocket处理器,映射路径为/ws/chat + registry.addHandler(noticeWebSocketHandler(), "/ws/notice") + .setAllowedOrigins("*"); // 允许所有来源,生产环境应指定具体域名 + } + + @Bean + public NoticeWebSocketHandler noticeWebSocketHandler() { + return new NoticeWebSocketHandler(); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/websocket/NoticeWebSocketHandler.java b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/NoticeWebSocketHandler.java new file mode 100644 index 0000000..92cef0d --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/NoticeWebSocketHandler.java @@ -0,0 +1,74 @@ +package com.cbsd.universaltestsoftware_client.websocket; + +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class NoticeWebSocketHandler extends TextWebSocketHandler { + + // 存储所有连接的会话,key为"IP:端口",value为WebSocketSession + private static final Map sessions = new ConcurrentHashMap<>(); + + @Override + public void afterConnectionEstablished(WebSocketSession session) throws Exception { + // 获取客户端IP和端口 + String clientId = getClientId(session); + + // 存储会话 + sessions.put(clientId, session); + + } + + @Override + protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { + // 获取发送者标识 + String senderId = getClientId(session); + + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { + // 获取客户端标识 + String clientId = getClientId(session); + + // 移除会话 + sessions.remove(clientId); + + } + + /** + * 获取客户端唯一标识(IP:端口) + */ + private String getClientId(WebSocketSession session) { + InetSocketAddress remoteAddress = session.getRemoteAddress(); + if (remoteAddress != null) { + return remoteAddress.getAddress().getHostAddress() + ":" + remoteAddress.getPort(); + } + return "unknown:" + session.getId(); + } + + /** + * 广播消息给所有连接的客户端 + */ + public void broadcast(String message) { + Set clientIds = sessions.keySet(); + + for (String clientId : clientIds) { + WebSocketSession session = sessions.get(clientId); + if (session != null && session.isOpen()) { + try { + session.sendMessage(new TextMessage(message)); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/websocket/WebSocketController.java b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/WebSocketController.java new file mode 100644 index 0000000..3de4c55 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/WebSocketController.java @@ -0,0 +1,21 @@ +package com.cbsd.universaltestsoftware_client.websocket; + + + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/websocket") +public class WebSocketController { + + @Autowired + private DataWebSocketHandler dataWebSocketHandler; + + // 广播消息给所有用户 + @GetMapping("/broadcast") + public String broadcast(@RequestParam String message) { + dataWebSocketHandler.broadcast(message); + return "已向所有用户广播消息"; + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/CommandClientMessage.java b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/CommandClientMessage.java new file mode 100644 index 0000000..e4906e7 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/CommandClientMessage.java @@ -0,0 +1,126 @@ +package com.cbsd.universaltestsoftware_client.websocket.command; + +import lombok.Data; +import lombok.Getter; + +import java.util.List; + +@Data +public class CommandClientMessage { + + /** + * 动作 + */ + public enum Operation { + /** + * 状态 + */ + STATE("state"), + /** + * 开始 + */ + START("start"), + /** + * 暂停 + */ + PAUSE("pause"), + /** + * 继续 + */ + RESUME("resume"), + /** + * 停止 + */ + STOP("stop"), + /** + * 重新开始 + */ + RESTART("restart"), + /** + * 单步 + */ + SINGLE_STEP("singleStep"), + /** + * 断点情况下单步 + */ + BREAK_POINT_SINGLE_STEP("breakPointSingleStep"), + /** + * 单次 + */ + ONCE("once"); + + @Getter + private final String value; + + Operation(String value) { + this.value = value; + } + + } + + /** + * 运行模式 + */ + public enum RunMode { + /** + * 顺序执行 + */ + SEQUENTIAL(1), + /** + * 并行执行 + */ + PARALLEL(2), + /** + * 单步执行 + */ + SINGLE(3); + + @Getter + private final int value; + + RunMode(int value) { + this.value = value; + } + + } + + /** + * 方案id + */ + private String schemeId; + /** + * 用例id + */ + private String invocationId; + + /** + * 操作 + */ + private String operation; + + /** + * 测试用例id集合 + */ + private List invocationIdList; + + /** + * 指令id列表 + */ + private List instructIdList; + + /** + * 运行模式 + */ + private int runMode; + + public String getRunModelLabel() { + if (runMode == RunMode.SEQUENTIAL.value) { + return "顺序执行"; + } else if (runMode == RunMode.PARALLEL.value) { + return "并行执行"; + } else if (runMode == RunMode.SINGLE.value) { + return "单步执行"; + } + return ""; + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/CommandRunMessage.java b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/CommandRunMessage.java new file mode 100644 index 0000000..14dfbb8 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/CommandRunMessage.java @@ -0,0 +1,64 @@ +package com.cbsd.universaltestsoftware_client.websocket.command; + +import com.cbsd.universaltestsoftware_client.entity.Instruct; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CommandRunMessage { + + private String schemeId; + + private String invocationId; + + private Instruct instruct; + + private int runMode; + /** + * 状态 1.开始运行 2.运行中 3.运行结束 4.运行失败 + */ + private int commandStatus; + + /** + * 运行状态 0.未运行 1.运行中 2.暂停 3.debug 4.运行结束 + */ + private int runStatus; + + /** + * 运行模式 + */ + public enum RunStatus { + /** + * 未运行 + */ + NOT_RUN(0), + /** + * 运行中 + */ + RUNNING(1), + /** + * 暂停 + */ + PAUSE(2), + /** + * debug + */ + DEBUG(3), + /** + * 方案运行结束 + */ + COMPLETE(4); + + @Getter + private final int value; + + RunStatus(int value) { + this.value = value; + } + + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/CommandWebSocketConfig.java b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/CommandWebSocketConfig.java new file mode 100644 index 0000000..32ae6f5 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/CommandWebSocketConfig.java @@ -0,0 +1,42 @@ +package com.cbsd.universaltestsoftware_client.websocket.command; + +import com.cbsd.universaltestsoftware_client.service.impl.ChannelDataServiceImpl; +import com.cbsd.universaltestsoftware_client.service.impl.InstructServiceImpl; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; + +import javax.annotation.Resource; + + +@Configuration +@EnableWebSocket +public class CommandWebSocketConfig implements WebSocketConfigurer { + + @Resource + private InstructServiceImpl instructService; + + @Resource + private ChannelDataServiceImpl channelDataService; + + /** + * 指令文件路径 + */ + @Value("${cbsd.commandRootPath}") + private String commandRootPath; + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + // 注册WebSocket处理器,映射路径为/ws/chat + registry.addHandler(commandWebSocketHandler(), "/ws/command") + .setAllowedOrigins("*"); // 允许所有来源,生产环境应指定具体域名 + } + + @Bean + public CommandWebSocketHandler commandWebSocketHandler() { + return new CommandWebSocketHandler(instructService, channelDataService, commandRootPath); + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/CommandWebSocketHandler.java b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/CommandWebSocketHandler.java new file mode 100644 index 0000000..f9dde9d --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/CommandWebSocketHandler.java @@ -0,0 +1,160 @@ +package com.cbsd.universaltestsoftware_client.websocket.command; + +import com.alibaba.fastjson.JSON; +import com.cbsd.universaltestsoftware_client.service.impl.ChannelDataServiceImpl; +import com.cbsd.universaltestsoftware_client.service.impl.InstructServiceImpl; +import com.cbsd.universaltestsoftware_client.util.DateUtils; +import lombok.extern.log4j.Log4j2; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +@Log4j2 +public class CommandWebSocketHandler extends TextWebSocketHandler implements SchemeRunThread.OnSchemeRunListener{ + + private final InstructServiceImpl instructService; + + private final ChannelDataServiceImpl channelDataService; + + /** + * 指令文件路径 + */ + private final String commandRootPath; + + /** + * 方案运行县城map,key方案id,value为运行线程 + */ + private final Map schemeThreadMap = new ConcurrentHashMap<>(); + + + public CommandWebSocketHandler(InstructServiceImpl instructService, ChannelDataServiceImpl channelDataService, String commandRootPath) { + this.instructService = instructService; + this.channelDataService = channelDataService; + this.commandRootPath = commandRootPath; + } + + // 存储所有连接的会话,key为"IP:端口",value为WebSocketSession + private static final Map sessions = new ConcurrentHashMap<>(); + + @Override + public void afterConnectionEstablished(WebSocketSession session) { + // 获取客户端IP和端口 + String clientId = getClientId(session); + // 存储会话 + sessions.put(clientId, session); + } + + @Override + public void onComplete(String schemeId) { + if (schemeThreadMap.get(schemeId) != null){ + schemeThreadMap.get(schemeId).stopRun(); + } + schemeThreadMap.remove(schemeId); + } + + @Override + protected void handleTextMessage(WebSocketSession session, TextMessage message) { + // 获取发送者标识 + String senderId = getClientId(session); + String payload = message.getPayload(); + try { + CommandClientMessage clientMessage = JSON.parseObject(payload, CommandClientMessage.class); + String schemeId = clientMessage.getSchemeId(); + if (Objects.equals(clientMessage.getOperation(), CommandClientMessage.Operation.STATE.getValue())) { + SchemeRunThread schemeRunThread = schemeThreadMap.get(schemeId); + if (schemeRunThread != null) { + broadcast(JSON.toJSONString(schemeRunThread.getRunState())); + } + } else if (Objects.equals(clientMessage.getOperation(), CommandClientMessage.Operation.START.getValue())) { + if (schemeThreadMap.get(schemeId) != null) { + return; + } + SchemeRunThread schemeRunThread = new SchemeRunThread(clientMessage, instructService, channelDataService, commandRootPath); + schemeRunThread.setOnSchemeRunListener(this); + schemeThreadMap.put(schemeId, schemeRunThread); + schemeRunThread.start(); + } else if (Objects.equals(clientMessage.getOperation(), CommandClientMessage.Operation.PAUSE.getValue())) { + SchemeRunThread schemeRunThread = schemeThreadMap.get(schemeId); + if (schemeRunThread != null) { + schemeRunThread.pauseRun(); + } + } else if (Objects.equals(clientMessage.getOperation(), CommandClientMessage.Operation.RESUME.getValue())) { + SchemeRunThread schemeRunThread = schemeThreadMap.get(schemeId); + if (schemeRunThread != null) { + schemeRunThread.resumeRun(clientMessage.getInvocationId()); + } + } else if (Objects.equals(clientMessage.getOperation(), CommandClientMessage.Operation.STOP.getValue())) { + SchemeRunThread schemeRunThread = schemeThreadMap.get(schemeId); + if (schemeRunThread != null) { + schemeRunThread.stopRun(); + } + schemeThreadMap.remove(schemeId); + } else if (Objects.equals(clientMessage.getOperation(), CommandClientMessage.Operation.RESTART.getValue())) { + SchemeRunThread schemeRunThread = schemeThreadMap.get(schemeId); + if (schemeRunThread != null) { + schemeRunThread.restartRun(); + } + } else if (Objects.equals(clientMessage.getOperation(), CommandClientMessage.Operation.SINGLE_STEP.getValue())) { + SchemeRunThread schemeRunThread = schemeThreadMap.get(schemeId); + if (schemeRunThread != null) { + schemeRunThread.singleStepRun(); + } + } else if (Objects.equals(clientMessage.getOperation(), CommandClientMessage.Operation.BREAK_POINT_SINGLE_STEP.getValue())) { + SchemeRunThread schemeRunThread = schemeThreadMap.get(schemeId); + if (schemeRunThread != null) { + schemeRunThread.breakPointSingleStepRun(clientMessage.getInvocationId()); + } + } else if (Objects.equals(clientMessage.getOperation(), CommandClientMessage.Operation.ONCE.getValue())) { + + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { + // 获取客户端标识 + String clientId = getClientId(session); + // 移除会话 + sessions.remove(clientId); + } + + /** + * 获取客户端唯一标识(IP:端口) + */ + private String getClientId(WebSocketSession session) { + InetSocketAddress remoteAddress = session.getRemoteAddress(); + if (remoteAddress != null) { + return remoteAddress.getAddress().getHostAddress() + ":" + remoteAddress.getPort(); + } + return "unknown:" + session.getId(); + } + + /** + * 广播消息给所有连接的客户端 + */ + public void broadcast(String message) { + Set clientIds = sessions.keySet(); + for (String clientId : clientIds) { + WebSocketSession session = sessions.get(clientId); + if (session != null && session.isOpen()) { + try { + session.sendMessage(new TextMessage(message)); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/InvocationRunThread.java b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/InvocationRunThread.java new file mode 100644 index 0000000..b2604f9 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/InvocationRunThread.java @@ -0,0 +1,633 @@ +package com.cbsd.universaltestsoftware_client.websocket.command; + +import com.cbsd.client.branch.IBranchConditionInterface; +import com.cbsd.client.pretreatment.CalculateUtils; +import com.cbsd.client.pretreatment.IPreTreatmentInterface; +import com.cbsd.universaltestsoftware_client.entity.Instruct; +import com.cbsd.universaltestsoftware_client.script.ScriptManager; +import com.cbsd.universaltestsoftware_client.service.impl.ChannelDataServiceImpl; +import com.cbsd.universaltestsoftware_client.service.impl.InstructServiceImpl; +import com.cbsd.universaltestsoftware_client.util.DataUtils; +import com.cbsd.universaltestsoftware_client.util.Result; +import com.cbsd.universaltestsoftware_client.util.ResultCode; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.CompositeDisposable; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.io.File; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class InvocationRunThread extends Thread { + + public interface OnInvocationRunListener { + void onInvocationComplete(String invocationId); + + void onInvocationRunning(CommandRunMessage runMessage); + + void onInvocationDebug(String invocationId); + } + + @Setter + private OnInvocationRunListener onInvocationRunListener; + + /** + * 用例id + */ + private final String invocationId; + /** + * 指令运行参数 + */ + private final CommandClientMessage clientMessage; + + private final InstructServiceImpl instructService; + + private final ChannelDataServiceImpl channelDataService; + /** + * 指令文件路径 + */ + private final String commandRootPath; + /** + * 正在运行的用例指令列表 + */ + private List runningInstructList; + /** + * 正在运行的指令索引 + */ + private int runningInstructIndex = 0; + /** + * 正在运行的指令运行次数 + */ + private int runningInstructRunCount = 1; + /** + * 正在运行的指令组list + */ + private final Map runningInstructGroupMap = new LinkedHashMap<>(); + /** + * 处理延时任务 + */ + private final CompositeDisposable disposable = new CompositeDisposable(); + /** + * 是否跳过debug 1.是 0.否 + */ + private int isSkipDebug; + /** + * 是否是单步调试 + */ + private boolean isDebugSingleStep = false; + /** + * 是否是第一次进入断点 + */ + private boolean isFirstEnterBreakPoint = false; + /** + * 运行状态 0.未运行 1.运行中 2.暂停 3.debug 4.运行结束 + */ + private int runStatus = CommandRunMessage.RunStatus.NOT_RUN.getValue(); + + public InvocationRunThread(String invocationId, CommandClientMessage clientMessage, InstructServiceImpl instructService, ChannelDataServiceImpl channelDataService, String commandRootPath) { + this.invocationId = invocationId; + this.clientMessage = clientMessage; + this.instructService = instructService; + this.channelDataService = channelDataService; + this.commandRootPath = commandRootPath; + + initInstructList(); + } + + private void initInstructList() { + //查询该用例的所有指令 + runningInstructList = instructService.getInstructListByInvocationId(invocationId); + if (runningInstructList != null) { + if (clientMessage.getInstructIdList() != null && !clientMessage.getInstructIdList().isEmpty()) { + //将选择的指令的命中次数置为0 + instructService.resetHitCountByInstructIdList(clientMessage.getInstructIdList()); + if (runningInstructList != null) { + runningInstructList.forEach(instruct -> { + instruct.setNeedRun(clientMessage.getInstructIdList().contains(instruct.getInstructId())); + }); + } + } else { + //将该用例的命中次数置为0 + instructService.resetHitCountByInvocation(invocationId); + if (runningInstructList != null) { + runningInstructList.forEach(instruct -> { + instruct.setNeedRun(true); + }); + } + } + + } + } + + private int times = 0; + + @Override + public void run() { + startRun(); + } + + /** + * 开始运行 + */ + public void startRun() { + runStatus = CommandRunMessage.RunStatus.RUNNING.getValue(); + runningInstructIndex = 0; + runningInstructRunCount = 1; + isFirstEnterBreakPoint = false; + runInstruct(runningInstructList.get(runningInstructIndex)); + } + + /** + * 暂停运行 + */ + public void pauseRun() { + if (disposable.size() > 0 && !disposable.isDisposed()) { + disposable.clear(); + } + runStatus = CommandRunMessage.RunStatus.PAUSE.getValue(); + } + + /** + * 继续运行 + */ + public void resumeRun() { + runStatus = CommandRunMessage.RunStatus.RUNNING.getValue(); + this.isSkipDebug = 1; + this.isDebugSingleStep = false; + + if (isFirstEnterBreakPoint) { + runningInstructIndex--; + isFirstEnterBreakPoint = false; + } + Instruct runInstruct = runningInstructList.get(runningInstructIndex); + if (runningInstructRunCount < runInstruct.getRepeatCount()) { + runningInstructRunCount++; + runInstruct(runInstruct); + } else { + runNextInstructIter(); + } + } + + /** + * 停止运行 + */ + public void stopRun() { + if (disposable.size() > 0 && !disposable.isDisposed()) { + disposable.clear(); + } + this.isDebugSingleStep = false; + runStatus = CommandRunMessage.RunStatus.NOT_RUN.getValue(); + } + + /** + * 重新运行 + */ + public void restartRun() { + stopRun(); + startRun(); + } + + /** + * 单步运行 + */ + public void singleStepRun() { + Instruct runInstruct = runningInstructList.get(runningInstructIndex); + if (runningInstructRunCount < runInstruct.getRepeatCount()) { + runningInstructRunCount++; + runInstruct(runInstruct); + } else { + runNextInstructIter(); + } + } + + /** + * 断点单步运行 + */ + public void breakPointSingleStepRun() { + this.isSkipDebug = 1; + this.isDebugSingleStep = true; + this.runStatus = CommandRunMessage.RunStatus.DEBUG.getValue(); + if (isFirstEnterBreakPoint) { + runningInstructIndex--; + isFirstEnterBreakPoint = false; + } + Instruct runInstruct = runningInstructList.get(runningInstructIndex); + if (runningInstructRunCount < runInstruct.getRepeatCount()) { + runningInstructRunCount++; + runInstruct(runInstruct); + } else { + runNextInstructIter(); + } + } + + /** + * 获取当前运行状态 + */ + public CommandRunMessage getRunState() { + Instruct runningInstruct = null; + if (runningInstructList.size() > runningInstructIndex) { + runningInstruct = runningInstructList.get(runningInstructIndex); + } + CommandRunMessage stateMsg = new CommandRunMessage(); + stateMsg.setRunStatus(runStatus); + stateMsg.setInvocationId(invocationId); + stateMsg.setInstruct(runningInstruct); + stateMsg.setSchemeId(clientMessage.getSchemeId()); + stateMsg.setRunMode(clientMessage.getRunMode()); + return stateMsg; + } + + /** + * 运行指令 + * + * @param runInstruct 运行的指令 + */ + private void runInstruct(Instruct runInstruct) { + if (runStatus == CommandRunMessage.RunStatus.PAUSE.getValue()) { + return; + } + if (!runInstruct.isNeedRun()) { + runNextInstructIter(); // 改为迭代版的下一条指令执行 + return; + } + //获取该条指令最新数据 + Instruct instruct = instructService.getById(runInstruct.getInstructId()); + if (instruct.getStatus() == 0) { + //禁用 + runNextInstructIter(); + return; + } + if (instruct.getInstructType() == 2) { + //指令组 + if (!runningInstructGroupMap.containsKey(instruct.getInstructId())) { + runningInstructGroupMap.put(instruct.getInstructId(), 0); + } + runNextInstructIter(); + return; + } + instruct.setNeedRun(true); + if (onInvocationRunListener != null) { + CommandRunMessage stateMsg = getRunState(); + stateMsg.setInstruct(instruct); + stateMsg.setCommandStatus(1); + onInvocationRunListener.onInvocationRunning(stateMsg); + } + if (clientMessage.getRunMode() != CommandClientMessage.RunMode.SINGLE.getValue() + && instruct.getBreakPoint() != null && instruct.getBreakPoint() == 1) { + if (this.isSkipDebug == 0) { + if (onInvocationRunListener != null) { + onInvocationRunListener.onInvocationDebug(invocationId); + } + isFirstEnterBreakPoint = true; + this.runStatus = CommandRunMessage.RunStatus.DEBUG.getValue(); + return; + } + } + this.isSkipDebug = 0; + // 执行指令发送逻辑(核心执行逻辑) + executeInstructAction(instruct); + } + + // 2. 新增:指令执行的核心动作(原 runInstruct 中的发送+延时逻辑) + private void executeInstructAction(Instruct instruct) { + if (disposable.size() > 0 && !disposable.isDisposed()) { + disposable.clear(); + } + // 延时/无延时执行指令(保留原有 RxJava 逻辑,但回调改为迭代) + if (instruct.getLeadTime() != null && instruct.getLeadTime() > 0) { + disposable.add(Observable.timer(instruct.getLeadTime(), TimeUnit.MILLISECONDS) + .subscribe(lead -> { + sendInstruct(instruct); + handleTailTimeAndRepeat(instruct); // 处理尾延时和重复逻辑 + }, e -> log.error("Lead time error", e))); + } else { + sendInstruct(instruct); + handleTailTimeAndRepeat(instruct); // 处理尾延时和重复逻辑 + } + } + + // 3. 新增:处理尾延时和重复次数判断(迭代模式) + private void handleTailTimeAndRepeat(Instruct instruct) { + if (instruct.getTailTime() == null || instruct.getTailTime() < 10) { + instruct.setTailTime(10); + } + disposable.add(Observable.timer(instruct.getTailTime(), TimeUnit.MILLISECONDS) + .subscribe(tail -> { + if (checkBranchCondition(instruct)) { + judgeRepeatCountIter(instruct); // 迭代版重复次数判断 + } + }, e -> log.error("Tail time error", e))); + } + + // 4. 迭代版重复次数判断(核心修改:不再递归调用 runInstruct,而是更新状态后重新执行循环) + private void judgeRepeatCountIter(Instruct instruct) { + if (clientMessage.getRunMode() == CommandClientMessage.RunMode.SINGLE.getValue() || isDebugSingleStep) { + return; // 单步调试逻辑保留 + } + if (instruct.getRepeatCount() != null && instruct.getRepeatCount() > 0) { + if (runningInstructRunCount < instruct.getRepeatCount()) { + runningInstructRunCount++; + runInstruct(instruct); // 此处看似递归,但实际是单次调用,栈会被释放(因为 RxJava 回调是异步,栈已清空) + } else { + runNextInstructIter(); // 执行下一条指令 + } + } else { + runNextInstructIter(); // 无重复次数,执行下一条 + } + } + + // 5. 迭代版执行下一条指令(统一循环入口) + private void runNextInstructIter() { + if (!runningInstructGroupMap.isEmpty()) { + runInstructGroupJudgeRepeatCount(); // 指令组逻辑保留,需确保内部无递归 + } else { + if (runningInstructIndex < runningInstructList.size() - 1) { + runningInstructRunCount = 1; + runningInstructIndex++; + Instruct nextInstruct = runningInstructList.get(runningInstructIndex); + runInstruct(nextInstruct); // 单次调用,无递归栈累积 + } else { + if (onInvocationRunListener != null) { + onInvocationRunListener.onInvocationComplete(invocationId); + } + } + } + } + + /** + * 发送指令 + * + * @param instruct 指令 + */ + private void sendInstruct(Instruct instruct) { + if (runStatus == CommandRunMessage.RunStatus.NOT_RUN.getValue() + || runStatus == CommandRunMessage.RunStatus.PAUSE.getValue()) { + return; + } + Result result = instructService.sendInstruct(getRunState()); + if (result.getCode() != ResultCode.OK) { + //指令由于服务端未连接等原因发送失败,向前端推送失败消息 + if (onInvocationRunListener != null) { + CommandRunMessage stateMsg = getRunState(); + stateMsg.setInstruct(instruct); + stateMsg.setCommandStatus(4); + onInvocationRunListener.onInvocationRunning(stateMsg); + } + } + } + + /** + * 判断分支条件 + * + * @param instruct 指令 + */ + private boolean checkBranchCondition(Instruct instruct) { + if (runStatus == CommandRunMessage.RunStatus.NOT_RUN.getValue() + || runStatus == CommandRunMessage.RunStatus.PAUSE.getValue()) { + return false; + } + if (StringUtils.isNotBlank(instruct.getBranchConditionScriptName())) { + try { + String originalCommand = instruct.getInstructText(); + if (StringUtils.isBlank(originalCommand)) { + //判断指令文件是否为空 + if (StringUtils.isNotBlank(instruct.getInstructDataFile())) { + File commandFile = new File(commandRootPath, instruct.getInstructDataFile()); + if (commandFile.exists()) { + try { + originalCommand = String.join("", Files.readAllLines(commandFile.toPath())); + } catch (Exception e) { + //按照二进制读取 + byte[] bytes = Files.readAllBytes(commandFile.toPath()); + originalCommand = DataUtils.byteToHex(bytes); + } + } + } + } + + String preTreatmentCommand = originalCommand; + Map latestDataMap = channelDataService.getChannelLatestData(instruct.getChannelId()); + //获取预处理脚本 + if (StringUtils.isNotBlank(instruct.getPreProcessScriptName())) { + IPreTreatmentInterface preTreatmentInterface = (IPreTreatmentInterface) ScriptManager.getScriptClass(ScriptManager.ScriptType.IPreTreatmentInterface.getValue(), instruct.getPreProcessScriptName()).newInstance(); + //查询通道数据 + String pretreatmentResult = preTreatmentInterface.pretreatment(originalCommand, instruct.getRepeatCount(), instruct.getHitCount(), instruct.getLastHitTime(), latestDataMap, new CalculateUtils()); + if (StringUtils.isNotBlank(pretreatmentResult)) { + preTreatmentCommand = pretreatmentResult; + } + } + + IBranchConditionInterface branchConditionInterface = (IBranchConditionInterface) ScriptManager.getScriptClass(ScriptManager.ScriptType.IBranchConditionInterface.getValue(), instruct.getBranchConditionScriptName()).newInstance(); + //查询通道数据 + String jumpInstructName = branchConditionInterface.branchCondition(originalCommand, preTreatmentCommand, instruct.getRepeatCount(), instruct.getHitCount(), instruct.getLastHitTime(), latestDataMap); + if (StringUtils.isNotBlank(jumpInstructName)) { + for (int i = 0; i < runningInstructList.size(); i++) { + Instruct jumpInstruct = runningInstructList.get(i); + if (jumpInstruct.getInstructName().equals(jumpInstructName)) { + //需判断跳转的指令是否是指令组下的指令, parentEncoding 不为0且能找到指令组 + Instruct instructGroup = queryInstructGroupByEncoding(jumpInstruct.getParentEncoding()); + if (instructGroup != null) { + //移除上一个组 + Map.Entry lastEntry = getRunningInstructGroupMapLast(); + runningInstructGroupMap.remove(lastEntry.getKey()); + //添加新的组 + if (!runningInstructGroupMap.containsKey(instructGroup.getInstructId())) { + runningInstructGroupMap.put(instructGroup.getInstructId(), 1); + } + } + runningInstructIndex = i; + runningInstructRunCount = 1; + jumpInstruct.setNeedRun(true); + runInstruct(jumpInstruct); + return false; + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + return true; + } + + /** + * 根据父级编码查询父级指令组 + * + * @param parentEncoding 父级编码 + */ + private Instruct queryInstructGroupByEncoding(String parentEncoding) { + if (StringUtils.isNotBlank(parentEncoding) && !"0".equals(parentEncoding)) { + for (Instruct instruct : runningInstructList) { + if (instruct.getEncoding().equals(parentEncoding)) { + return instruct; + } + } + } + return null; + } + + /** + * 判断指令运行次数 + * + * @param instruct 指令 + */ + private void runInstructJudgeRepeatCount(Instruct instruct) { + if (instruct.getRepeatCount() != null && instruct.getRepeatCount() > 0) { + if (runningInstructRunCount < instruct.getRepeatCount()) { + if (clientMessage.getRunMode() != CommandClientMessage.RunMode.SINGLE.getValue()) { + if (!isDebugSingleStep) { + runningInstructRunCount++; + runInstruct(instruct); + } + } + } else { + if (clientMessage.getRunMode() != CommandClientMessage.RunMode.SINGLE.getValue()) { + if (!isDebugSingleStep) { + runNextInstructIter(); + } + } + } + } else { + if (clientMessage.getRunMode() != CommandClientMessage.RunMode.SINGLE.getValue()) { + if (!isDebugSingleStep) { + runNextInstructIter(); + } + } + } + } + + /** + * 判断指令组重复次数 + */ + private void runInstructGroupJudgeRepeatCount() { + //当前指令 + Instruct currentInstruct = runningInstructList.get(runningInstructIndex); + if (currentInstruct.getInstructType() == 2) { + //是指令组 + //判断当前组执行次数 + if (checkGroupRunCount()) { + //需要继续执行, 即执行组第一条指令 + Map.Entry lastEntry = getRunningInstructGroupMapLast(); + Instruct lastRunInstructGroup = instructService.getById(lastEntry.getKey()); + List groupInstructList = getGroupInstructList(lastRunInstructGroup); + if (groupInstructList.isEmpty()) { + //组下面没有指令可执行,正常执行下一条指令 + runNextInstructIter(); + } else { + Instruct groupFirstInstruct = groupInstructList.get(0); + for (int i = 0; i < runningInstructList.size(); i++) { + if (runningInstructList.get(i).getInstructId().equals(groupFirstInstruct.getInstructId())) { + runningInstructIndex = i; + runningInstructRunCount = 1; + runInstruct(runningInstructList.get(runningInstructIndex)); + return; + } + } + } + } else { + //执行完成, 正常执行下一条指令 + runNextInstructIter(); + } + } else { + //是指令 + //判断当前指令是否是当前组的最后一条 + Map.Entry lastEntry = getRunningInstructGroupMapLast(); + Instruct lastRunInstructGroup = instructService.getById(lastEntry.getKey()); + List groupInstructList = getGroupInstructList(lastRunInstructGroup); + if (currentInstruct.getInstructId().equals(groupInstructList.get(groupInstructList.size() - 1).getInstructId())) { + //是当前组最后一条指令 + //判断当前组执行次数 + if (lastEntry.getValue() < lastRunInstructGroup.getRepeatCount()) { + updateGroupRunCount(lastEntry.getKey(), lastEntry.getValue() + 1); + Instruct groupFirstInstruct = groupInstructList.get(0); + for (int i = 0; i < runningInstructList.size(); i++) { + if (runningInstructList.get(i).getInstructId().equals(groupFirstInstruct.getInstructId())) { + runningInstructIndex = i; + runningInstructRunCount = 1; + runInstruct(runningInstructList.get(runningInstructIndex)); + return; + } + } + } else { + runningInstructGroupMap.remove(lastEntry.getKey()); + runNextInstructIter(); + } + } else { + if (runningInstructIndex < runningInstructList.size() - 1) { + runningInstructRunCount = 1; + runningInstructIndex++; + Instruct nextInstruct = runningInstructList.get(runningInstructIndex); + runInstruct(nextInstruct); // 单次调用,无递归栈累积 + } else { + if (onInvocationRunListener != null) { + onInvocationRunListener.onInvocationComplete(invocationId); + } + } + } + } + } + + /** + * 检查指令组执行次数 + * + * @return 是否继续执行 + */ + private boolean checkGroupRunCount() { + Map.Entry lastEntry = getRunningInstructGroupMapLast(); + Instruct lastGroup = instructService.getById(lastEntry.getKey()); + if (lastEntry.getValue() < lastGroup.getRepeatCount()) { + //还要执行, 将运行次数加1 + updateGroupRunCount(lastEntry.getKey(), lastEntry.getValue() + 1); + return true; + } else { + //执行完成 + runningInstructGroupMap.remove(lastEntry.getKey()); + if (!runningInstructGroupMap.isEmpty()) { + return checkGroupRunCount(); + } else { + //整个组执行完了 + return false; + } + } + } + + /** + * 更新指令组运行次数 + * + * @param instructId 指令id + * @param runCount 运行次数 + */ + private void updateGroupRunCount(String instructId, int runCount) { + runningInstructGroupMap.replace(instructId, runCount); + } + + /** + * 获取运行中map的最后一个组 + */ + private Map.Entry getRunningInstructGroupMapLast() { + List> entries = new ArrayList<>(runningInstructGroupMap.entrySet()); + return entries.get(entries.size() - 1); + } + + /** + * 获取指令组内指令 + * + * @param instructGroup 指令组 + */ + private List getGroupInstructList(Instruct instructGroup) { + List instructList = new ArrayList<>(); + runningInstructList.forEach(instruct -> { + if (instruct.getParentEncoding().startsWith(instructGroup.getEncoding())) { + instructList.add(instruct); + } + }); + return instructList; + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/InvocationRunThreadBack.java b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/InvocationRunThreadBack.java new file mode 100644 index 0000000..69e6e29 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/InvocationRunThreadBack.java @@ -0,0 +1,631 @@ +package com.cbsd.universaltestsoftware_client.websocket.command; + +import com.cbsd.client.branch.IBranchConditionInterface; +import com.cbsd.client.pretreatment.CalculateUtils; +import com.cbsd.client.pretreatment.IPreTreatmentInterface; +import com.cbsd.universaltestsoftware_client.entity.Instruct; +import com.cbsd.universaltestsoftware_client.script.ScriptManager; +import com.cbsd.universaltestsoftware_client.service.impl.ChannelDataServiceImpl; +import com.cbsd.universaltestsoftware_client.service.impl.InstructServiceImpl; +import com.cbsd.universaltestsoftware_client.util.DataUtils; +import com.cbsd.universaltestsoftware_client.util.Result; +import com.cbsd.universaltestsoftware_client.util.ResultCode; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.CompositeDisposable; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.io.File; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class InvocationRunThreadBack extends Thread { + + public interface OnInvocationRunListener { + void onInvocationComplete(String invocationId); + + void onInvocationRunning(CommandRunMessage runMessage); + + void onInvocationDebug(String invocationId); + } + + @Setter + private OnInvocationRunListener onInvocationRunListener; + + /** + * 用例id + */ + private final String invocationId; + /** + * 指令运行参数 + */ + private final CommandClientMessage clientMessage; + + private final InstructServiceImpl instructService; + + private final ChannelDataServiceImpl channelDataService; + /** + * 指令文件路径 + */ + private final String commandRootPath; + /** + * 正在运行的用例指令列表 + */ + private List runningInstructList; + /** + * 正在运行的指令索引 + */ + private int runningInstructIndex = 0; + /** + * 正在运行的指令运行次数 + */ + private int runningInstructRunCount = 1; + /** + * 正在运行的指令组list + */ + private final Map runningInstructGroupMap = new LinkedHashMap<>(); + /** + * 处理延时任务 + */ + private final CompositeDisposable disposable = new CompositeDisposable(); + /** + * 是否跳过debug 1.是 0.否 + */ + private int isSkipDebug; + /** + * 是否是单步调试 + */ + private boolean isDebugSingleStep = false; + /** + * 是否是第一次进入断点 + */ + private boolean isFirstEnterBreakPoint = false; + /** + * 运行状态 0.未运行 1.运行中 2.暂停 3.debug 4.运行结束 + */ + private int runStatus = CommandRunMessage.RunStatus.NOT_RUN.getValue(); + + public InvocationRunThreadBack(String invocationId, CommandClientMessage clientMessage, InstructServiceImpl instructService, ChannelDataServiceImpl channelDataService, String commandRootPath) { + this.invocationId = invocationId; + this.clientMessage = clientMessage; + this.instructService = instructService; + this.channelDataService = channelDataService; + this.commandRootPath = commandRootPath; + + initInstructList(); + } + + private void initInstructList() { + //查询该用例的所有指令 + runningInstructList = instructService.getInstructListByInvocationId(invocationId); + if (runningInstructList != null) { + if (clientMessage.getInstructIdList() != null && !clientMessage.getInstructIdList().isEmpty()) { + //将选择的指令的命中次数置为0 + instructService.resetHitCountByInstructIdList(clientMessage.getInstructIdList()); + if (runningInstructList != null) { + runningInstructList.forEach(instruct -> { + instruct.setNeedRun(clientMessage.getInstructIdList().contains(instruct.getInstructId())); + }); + } + } else { + //将该用例的命中次数置为0 + instructService.resetHitCountByInvocation(invocationId); + if (runningInstructList != null) { + runningInstructList.forEach(instruct -> { + instruct.setNeedRun(true); + }); + } + } + + } + } + + private int times = 0; + + @Override + public void run() { + startRun(); +// while (times < 100){ +// times ++; +// System.out.println("========= times:" + times); +// } + } + + /** + * 开始运行 + */ + public void startRun() { + runStatus = CommandRunMessage.RunStatus.RUNNING.getValue(); + runningInstructIndex = 0; + runningInstructRunCount = 1; + isFirstEnterBreakPoint = false; + runInstruct(runningInstructList.get(runningInstructIndex)); + } + + /** + * 暂停运行 + */ + public void pauseRun() { + if (disposable.size() > 0 && !disposable.isDisposed()) { + disposable.clear(); + } + runStatus = CommandRunMessage.RunStatus.PAUSE.getValue(); + } + + /** + * 继续运行 + */ + public void resumeRun() { + runStatus = CommandRunMessage.RunStatus.RUNNING.getValue(); + this.isSkipDebug = 1; + this.isDebugSingleStep = false; + + if (isFirstEnterBreakPoint) { + runningInstructIndex--; + isFirstEnterBreakPoint = false; + } + Instruct runInstruct = runningInstructList.get(runningInstructIndex); + if (runningInstructRunCount < runInstruct.getRepeatCount()) { + runningInstructRunCount++; + runInstruct(runInstruct); + } else { + runNextInstruct(); + } + } + + /** + * 停止运行 + */ + public void stopRun() { + if (disposable.size() > 0 && !disposable.isDisposed()) { + disposable.clear(); + } + this.isDebugSingleStep = false; + runStatus = CommandRunMessage.RunStatus.NOT_RUN.getValue(); + } + + /** + * 重新运行 + */ + public void restartRun() { + stopRun(); + startRun(); + } + + /** + * 单步运行 + */ + public void singleStepRun() { + Instruct runInstruct = runningInstructList.get(runningInstructIndex); + if (runningInstructRunCount < runInstruct.getRepeatCount()) { + runningInstructRunCount++; + runInstruct(runInstruct); + } else { + runNextInstruct(); + } + } + + /** + * 断点单步运行 + */ + public void breakPointSingleStepRun() { + this.isSkipDebug = 1; + this.isDebugSingleStep = true; + this.runStatus = CommandRunMessage.RunStatus.DEBUG.getValue(); + if (isFirstEnterBreakPoint) { + runningInstructIndex--; + isFirstEnterBreakPoint = false; + } + Instruct runInstruct = runningInstructList.get(runningInstructIndex); + if (runningInstructRunCount < runInstruct.getRepeatCount()) { + runningInstructRunCount++; + runInstruct(runInstruct); + } else { + runNextInstruct(); + } + } + + /** + * 获取当前运行状态 + */ + public CommandRunMessage getRunState() { + Instruct runningInstruct = null; + if (runningInstructList.size() > runningInstructIndex) { + runningInstruct = runningInstructList.get(runningInstructIndex); + } + CommandRunMessage stateMsg = new CommandRunMessage(); + stateMsg.setRunStatus(runStatus); + stateMsg.setInvocationId(invocationId); + stateMsg.setInstruct(runningInstruct); + stateMsg.setRunMode(clientMessage.getRunMode()); + return stateMsg; + } + + /** + * 运行指令 + * + * @param runInstruct 运行的指令 + */ + private void runInstruct(Instruct runInstruct) { +// if (isPause) { +// return; +// } + if (runStatus == CommandRunMessage.RunStatus.PAUSE.getValue()) { + return; + } + if (!runInstruct.isNeedRun()) { + //不需要运行,运行下一个 + runNextInstruct(); + return; + } + //获取该条指令最新数据 + Instruct instruct = instructService.getById(runInstruct.getInstructId()); + if (instruct.getStatus() == 0) { + //禁用 + runNextInstruct(); + return; + } + if (instruct.getInstructType() == 2) { + //指令组 + if (!runningInstructGroupMap.containsKey(instruct.getInstructId())) { + runningInstructGroupMap.put(instruct.getInstructId(), 0); + } + runNextInstruct(); + return; + } + instruct.setNeedRun(true); + if (onInvocationRunListener != null) { + CommandRunMessage stateMsg = getRunState(); + stateMsg.setInstruct(instruct); + stateMsg.setCommandStatus(1); + onInvocationRunListener.onInvocationRunning(stateMsg); + } + if (clientMessage.getRunMode() != CommandClientMessage.RunMode.SINGLE.getValue() + && instruct.getBreakPoint() != null && instruct.getBreakPoint() == 1) { + if (this.isSkipDebug == 0) { + if (onInvocationRunListener != null) { + onInvocationRunListener.onInvocationDebug(invocationId); + } + isFirstEnterBreakPoint = true; + this.runStatus = CommandRunMessage.RunStatus.DEBUG.getValue(); + return; + } + } + this.isSkipDebug = 0; + if (disposable.size() > 0 && !disposable.isDisposed()) { + disposable.clear(); + } + if (instruct.getLeadTime() != null && instruct.getLeadTime() > 0) { + disposable.add(Observable.timer(instruct.getLeadTime(), TimeUnit.MILLISECONDS) + .subscribe(lead -> { + sendInstruct(instruct); + if (instruct.getTailTime() != null && instruct.getTailTime() > 0) { + disposable.add(Observable.timer(instruct.getTailTime(), TimeUnit.MILLISECONDS) + .subscribe(tail -> { + if (checkBranchCondition(instruct)) { + runInstructJudgeRepeatCount(instruct); + } + }, e -> { + + })); + } else { + if (checkBranchCondition(instruct)) { + runInstructJudgeRepeatCount(instruct); + } + } + }, e -> { + + })); + } else { + sendInstruct(instruct); + if (instruct.getTailTime() != null && instruct.getTailTime() > 0) { + disposable.add(Observable.timer(instruct.getTailTime(), TimeUnit.MILLISECONDS) + .subscribe(tail -> { + if (runStatus == CommandRunMessage.RunStatus.NOT_RUN.getValue() + || runStatus == CommandRunMessage.RunStatus.PAUSE.getValue()) { + return; + } + if (checkBranchCondition(instruct)) { + runInstructJudgeRepeatCount(instruct); + } + }, e -> { + + })); + } else { + if (runStatus == CommandRunMessage.RunStatus.NOT_RUN.getValue() + || runStatus == CommandRunMessage.RunStatus.PAUSE.getValue()) { + return; + } + if (checkBranchCondition(instruct)) { + runInstructJudgeRepeatCount(instruct); + } + } + } + +// if (instruct.getLeadTime() != null && instruct.getLeadTime() > 0) { +// CountDownLatch leadLatch = new CountDownLatch(1); +// disposable.add(Observable.timer(instruct.getLeadTime(), TimeUnit.MILLISECONDS) +// .doOnNext(lead -> leadLatch.countDown()) +// .doOnError(e -> leadLatch.countDown()).subscribe()); +// try { +// leadLatch.await(); // 等待直到计时器完成或超时(如果有的话) +// } catch (InterruptedException e) { +// e.printStackTrace(); +// } +// } +// sendInstruct(instruct); +// if (instruct.getTailTime() != null && instruct.getTailTime() > 0) { +// CountDownLatch tailLatch = new CountDownLatch(1); +// disposable.add(Observable.timer(instruct.getTailTime(), TimeUnit.MILLISECONDS) +// .doOnNext(tail -> tailLatch.countDown()) +// .doOnError(e -> tailLatch.countDown()).subscribe()); +// try { +// tailLatch.await(); // 等待直到计时器完成或超时(如果有的话) +// } catch (InterruptedException e) { +// e.printStackTrace(); +// } +// } +// if (runStatus == CommandRunMessage.RunStatus.NOT_RUN.getValue() +// || runStatus == CommandRunMessage.RunStatus.PAUSE.getValue()) { +// return; +// } +// if (checkBranchCondition(instruct)) { +// runInstructJudgeRepeatCount(instruct); +// } + } + + /** + * 运行下一条指令 + */ + private void runNextInstruct() { + if (!runningInstructGroupMap.isEmpty()) { + runInstructGroupJudgeRepeatCount(); + } else { + if (runningInstructIndex < runningInstructList.size() - 1) { + runningInstructRunCount = 1; + runningInstructIndex++; + Instruct instruct = runningInstructList.get(runningInstructIndex); + runInstruct(instruct); + } else { + if (onInvocationRunListener != null) { + onInvocationRunListener.onInvocationComplete(invocationId); + } + } + } + } + + /** + * 发送指令 + * + * @param instruct 指令 + */ + private void sendInstruct(Instruct instruct) { + Result result = instructService.sendInstruct(getRunState()); + if (result.getCode() != ResultCode.OK) { + //指令由于服务端未连接等原因发送失败,向前端推送失败消息 + if (onInvocationRunListener != null) { + CommandRunMessage stateMsg = getRunState(); + stateMsg.setInstruct(instruct); + stateMsg.setCommandStatus(4); + onInvocationRunListener.onInvocationRunning(stateMsg); + } + } + } + + /** + * 判断分支条件 + * + * @param instruct 指令 + */ + private boolean checkBranchCondition(Instruct instruct) { + if (StringUtils.isNotBlank(instruct.getBranchConditionScriptName())) { + try { + String originalCommand = instruct.getInstructText(); + if (StringUtils.isBlank(originalCommand)) { + //判断指令文件是否为空 + if (StringUtils.isNotBlank(instruct.getInstructDataFile())) { + File commandFile = new File(commandRootPath, instruct.getInstructDataFile()); + if (commandFile.exists()) { + try { + originalCommand = String.join("", Files.readAllLines(commandFile.toPath())); + } catch (Exception e) { + //按照二进制读取 + byte[] bytes = Files.readAllBytes(commandFile.toPath()); + originalCommand = DataUtils.byteToHex(bytes); + } + } + } + } + + String preTreatmentCommand = originalCommand; + Map latestDataMap = channelDataService.getChannelLatestData(instruct.getChannelId()); + //获取预处理脚本 + if (StringUtils.isNotBlank(instruct.getPreProcessScriptName())) { + IPreTreatmentInterface preTreatmentInterface = (IPreTreatmentInterface) ScriptManager.getScriptClass(ScriptManager.ScriptType.IPreTreatmentInterface.getValue(), instruct.getPreProcessScriptName()).newInstance(); + //查询通道数据 + String pretreatmentResult = preTreatmentInterface.pretreatment(originalCommand, instruct.getRepeatCount(), instruct.getHitCount(), instruct.getLastHitTime(), latestDataMap, new CalculateUtils()); + if (StringUtils.isNotBlank(pretreatmentResult)) { + preTreatmentCommand = pretreatmentResult; + } + } + + IBranchConditionInterface branchConditionInterface = (IBranchConditionInterface) ScriptManager.getScriptClass(ScriptManager.ScriptType.IBranchConditionInterface.getValue(), instruct.getBranchConditionScriptName()).newInstance(); + //查询通道数据 + String jumpInstructName = branchConditionInterface.branchCondition(originalCommand, preTreatmentCommand, instruct.getRepeatCount(), instruct.getHitCount(), instruct.getLastHitTime(), latestDataMap); + if (StringUtils.isNotBlank(jumpInstructName)) { + for (int i = 0; i < runningInstructList.size(); i++) { + Instruct jumpInstruct = runningInstructList.get(i); + if (jumpInstruct.getInstructName().equals(jumpInstructName)) { + runningInstructIndex = i; + runningInstructRunCount = 1; + jumpInstruct.setNeedRun(true); + runInstruct(jumpInstruct); + return false; + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + return true; + } + + /** + * 判断指令运行次数 + * + * @param instruct 指令 + */ + private void runInstructJudgeRepeatCount(Instruct instruct) { + if (instruct.getRepeatCount() != null && instruct.getRepeatCount() > 0) { + System.out.println("运行次数:" + runningInstructRunCount + ", 重复次数: " + instruct.getRepeatCount() + ", 运行模式: " + clientMessage.getRunMode() + ", isDebugSingleStep: " + isDebugSingleStep); + if (runningInstructRunCount < instruct.getRepeatCount()) { + if (clientMessage.getRunMode() != CommandClientMessage.RunMode.SINGLE.getValue()) { + if (!isDebugSingleStep) { + runningInstructRunCount++; + runInstruct(instruct); + } + } + } else { + if (clientMessage.getRunMode() != CommandClientMessage.RunMode.SINGLE.getValue()) { + if (!isDebugSingleStep) { + runNextInstruct(); + } + } + } + } else { + if (clientMessage.getRunMode() != CommandClientMessage.RunMode.SINGLE.getValue()) { + if (!isDebugSingleStep) { + runNextInstruct(); + } + } + } + } + + /** + * 判断指令组重复次数 + */ + private void runInstructGroupJudgeRepeatCount() { + //当前指令 + Instruct currentInstruct = runningInstructList.get(runningInstructIndex); + if (currentInstruct.getInstructType() == 2) { + //是指令组 + //判断当前组执行次数 + if (checkGroupRunCount()) { + //需要继续执行, 即执行组第一条指令 + Map.Entry lastEntry = getRunningInstructGroupMapLast(); + Instruct lastRunInstructGroup = instructService.getById(lastEntry.getKey()); + List groupInstructList = getGroupInstructList(lastRunInstructGroup); + if (groupInstructList.isEmpty()) { + //组下面没有指令可执行,正常执行下一条指令 + runNextInstruct(); + } else { + Instruct groupFirstInstruct = groupInstructList.get(0); + for (int i = 0; i < runningInstructList.size(); i++) { + if (runningInstructList.get(i).getInstructId().equals(groupFirstInstruct.getInstructId())) { + runningInstructIndex = i; + runningInstructRunCount = 1; + runInstruct(runningInstructList.get(runningInstructIndex)); + return; + } + } + } + } else { + //执行完成, 正常执行下一条指令 + runNextInstruct(); + } + } else { + //是指令 + //判断当前指令是否是当前组的最后一条 + Map.Entry lastEntry = getRunningInstructGroupMapLast(); + Instruct lastRunInstructGroup = instructService.getById(lastEntry.getKey()); + List groupInstructList = getGroupInstructList(lastRunInstructGroup); + if (currentInstruct.getInstructId().equals(groupInstructList.get(groupInstructList.size() - 1).getInstructId())) { + //是当前组最后一条指令 + //判断当前组执行次数 + if (lastEntry.getValue() < lastRunInstructGroup.getRepeatCount()) { + updateGroupRunCount(lastEntry.getKey(), lastEntry.getValue() + 1); + Instruct groupFirstInstruct = groupInstructList.get(0); + for (int i = 0; i < runningInstructList.size(); i++) { + if (runningInstructList.get(i).getInstructId().equals(groupFirstInstruct.getInstructId())) { + runningInstructIndex = i; + runningInstructRunCount = 1; + runInstruct(runningInstructList.get(runningInstructIndex)); + return; + } + } + } else { + runningInstructGroupMap.remove(lastEntry.getKey()); + runNextInstruct(); + } + } else { + runningInstructIndex++; + runningInstructRunCount = 1; + runInstruct(runningInstructList.get(runningInstructIndex)); + } + } + } + + /** + * 检查指令组执行次数 + * + * @return 是否继续执行 + */ + private boolean checkGroupRunCount() { + Map.Entry lastEntry = getRunningInstructGroupMapLast(); + Instruct lastGroup = instructService.getById(lastEntry.getKey()); + if (lastEntry.getValue() < lastGroup.getRepeatCount()) { + //还要执行, 将运行次数加1 + updateGroupRunCount(lastEntry.getKey(), lastEntry.getValue() + 1); + return true; + } else { + //执行完成 + runningInstructGroupMap.remove(lastEntry.getKey()); + if (!runningInstructGroupMap.isEmpty()) { + return checkGroupRunCount(); + } else { + //整个组执行完了 + return false; + } + } + } + + /** + * 更新指令组运行次数 + * + * @param instructId 指令id + * @param runCount 运行次数 + */ + private void updateGroupRunCount(String instructId, int runCount) { + runningInstructGroupMap.replace(instructId, runCount); + } + + /** + * 获取运行中map的最后一个组 + */ + private Map.Entry getRunningInstructGroupMapLast() { + List> entries = new ArrayList<>(runningInstructGroupMap.entrySet()); + return entries.get(entries.size() - 1); + } + + /** + * 获取指令组内指令 + * + * @param instructGroup 指令组 + */ + private List getGroupInstructList(Instruct instructGroup) { + List instructList = new ArrayList<>(); + runningInstructList.forEach(instruct -> { + if (instruct.getParentEncoding().startsWith(instructGroup.getEncoding())) { + instructList.add(instruct); + } + }); + return instructList; + } +} diff --git a/src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/SchemeRunThread.java b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/SchemeRunThread.java new file mode 100644 index 0000000..f6d0134 --- /dev/null +++ b/src/main/java/com/cbsd/universaltestsoftware_client/websocket/command/SchemeRunThread.java @@ -0,0 +1,218 @@ +package com.cbsd.universaltestsoftware_client.websocket.command; + +import com.alibaba.fastjson.JSON; +import com.cbsd.universaltestsoftware_client.entity.Instruct; +import com.cbsd.universaltestsoftware_client.entity.Invocation; +import com.cbsd.universaltestsoftware_client.service.impl.ChannelDataServiceImpl; +import com.cbsd.universaltestsoftware_client.service.impl.InstructServiceImpl; +import com.cbsd.universaltestsoftware_client.util.DateUtils; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +public class SchemeRunThread extends Thread implements InvocationRunThread.OnInvocationRunListener { + + public interface OnSchemeRunListener { + void onComplete(String schemeId); + } + + @Setter + private OnSchemeRunListener onSchemeRunListener; + + private final CommandClientMessage clientMessage; + + private final InstructServiceImpl instructService; + + private final ChannelDataServiceImpl channelDataService; + + /** + * 指令文件路径 + */ + private final String commandRootPath; + + private List invocationList; + + /** + * 用例执行线程map, key用例id, value用例执行线程 + */ + private final Map invocationRunThreadMap = new HashMap<>(); + + private int invocationRunIndex = 0; + + private int invocationCompleteCount = 0; + + public SchemeRunThread(CommandClientMessage clientMessage, InstructServiceImpl instructService, ChannelDataServiceImpl channelDataService, String commandRootPath) { + this.clientMessage = clientMessage; + this.instructService = instructService; + this.channelDataService = channelDataService; + this.commandRootPath = commandRootPath; + } + + @Override + public void run() { + log.info(" 遥控方案开始运行, 运行模式:{}, 时间: {}", clientMessage.getRunModelLabel(), DateUtils.dateToString()); + List invocationIdList = clientMessage.getInvocationIdList(); + //根据选择用例id列表获取用例列表 + invocationList = instructService.getInvocationListByIds(invocationIdList); + if (invocationList == null || invocationList.isEmpty()) { + schemeRunCompleted(); + return; + } + + if (clientMessage.getRunMode() == CommandClientMessage.RunMode.SEQUENTIAL.getValue() || clientMessage.getRunMode() == CommandClientMessage.RunMode.SINGLE.getValue()) { + //顺序运行 或 单步运行 + createAndStartInvocationRunThread(invocationList.get(invocationRunIndex)); + } else if (clientMessage.getRunMode() == CommandClientMessage.RunMode.PARALLEL.getValue()) { + //并行运行 + for (Invocation invocation : invocationList) { + createAndStartInvocationRunThread(invocation); + } + } + } + + private void createAndStartInvocationRunThread(Invocation invocation) { + InvocationRunThread invocationRunThread = new InvocationRunThread(invocation.getInvocationId(), clientMessage, instructService, channelDataService, commandRootPath); + invocationRunThread.setOnInvocationRunListener(this); + invocationRunThreadMap.put(invocation.getInvocationId(), invocationRunThread); + invocationRunThread.start(); + log.info(" 用例 {} 开始运行, 时间: {}", invocation.getInvocationName(), DateUtils.dateToString()); + } + + @Override + public void onInvocationComplete(String invocationId) { + Invocation currentRunningInvocation = invocationList.get(invocationRunIndex); + log.info(" 用例 {} 运行完成, 时间: {}", currentRunningInvocation.getInvocationName(), DateUtils.dateToString()); + invocationRunThreadMap.remove(invocationId); + if (clientMessage.getRunMode() == CommandClientMessage.RunMode.SEQUENTIAL.getValue() || clientMessage.getRunMode() == CommandClientMessage.RunMode.SINGLE.getValue()) { + //顺序运行 或 单步运行 + if (invocationRunIndex < invocationList.size() - 1) { + invocationRunIndex++; + createAndStartInvocationRunThread(invocationList.get(invocationRunIndex)); + } else { + //运行完成 + schemeRunCompleted(); + } + } else if (clientMessage.getRunMode() == CommandClientMessage.RunMode.PARALLEL.getValue()) { + //并行运行 + invocationCompleteCount++; + if (invocationList != null && invocationCompleteCount == invocationList.size()) { + schemeRunCompleted(); + } + } + } + + @Override + public void onInvocationRunning(CommandRunMessage runMessage) { + instructService.broadcast(JSON.toJSONString(runMessage)); + } + + @Override + public void onInvocationDebug(String invocationId) { + CommandRunMessage runMessage = new CommandRunMessage(); + runMessage.setRunStatus(CommandRunMessage.RunStatus.DEBUG.getValue()); + runMessage.setRunMode(clientMessage.getRunMode()); + runMessage.setSchemeId(clientMessage.getSchemeId()); + runMessage.setInvocationId(invocationId); + runMessage.setCommandStatus(2); + instructService.broadcast(JSON.toJSONString(runMessage)); + } + + /** + * 方案运行完成 + */ + private void schemeRunCompleted() { + log.info(" 遥控方案运行完成, 时间: {}", DateUtils.dateToString()); + CommandRunMessage runMessage = new CommandRunMessage(); + runMessage.setRunStatus(CommandRunMessage.RunStatus.COMPLETE.getValue()); + runMessage.setSchemeId(clientMessage.getSchemeId()); + instructService.broadcast(JSON.toJSONString(runMessage)); + if (onSchemeRunListener != null) { + onSchemeRunListener.onComplete(clientMessage.getSchemeId()); + } + stopRun(); + } + + /** + * 暂停运行 + */ + public void pauseRun() { + for (InvocationRunThread invocationRunThread : invocationRunThreadMap.values()) { + invocationRunThread.pauseRun(); + } + } + + /** + * 继续运行 + */ + public void resumeRun(String invocationId) { + if (clientMessage.getRunMode() == CommandClientMessage.RunMode.PARALLEL.getValue()){ + InvocationRunThread invocationRunThread = invocationRunThreadMap.get(invocationId); + if (invocationRunThread != null){ + invocationRunThread.resumeRun(); + } + } else { + for (InvocationRunThread invocationRunThread : invocationRunThreadMap.values()) { + invocationRunThread.resumeRun(); + } + } + } + + /** + * 停止运行 + */ + public void stopRun() { + for (InvocationRunThread invocationRunThread : invocationRunThreadMap.values()) { + invocationRunThread.stopRun(); + invocationRunThread.interrupt(); + } + invocationRunThreadMap.clear(); + interrupt(); + } + + /** + * 重新运行 + */ + public void restartRun() { + for (InvocationRunThread invocationRunThread : invocationRunThreadMap.values()) { + invocationRunThread.restartRun(); + } + } + + /** + * 单步运行 + */ + public void singleStepRun() { + for (InvocationRunThread invocationRunThread : invocationRunThreadMap.values()) { + invocationRunThread.singleStepRun(); + } + } + + /** + * 断点单步运行 + */ + public void breakPointSingleStepRun(String invocationId) { + InvocationRunThread invocationRunThread = invocationRunThreadMap.get(invocationId); + if (invocationRunThread != null){ + invocationRunThread.breakPointSingleStepRun(); + } + } + + + /** + * 获取当前方案运行状态 + */ + public List getRunState() { + List runStateList = new ArrayList<>(); + for (InvocationRunThread invocationRunThread : invocationRunThreadMap.values()) { + CommandRunMessage stateMsg = invocationRunThread.getRunState(); + stateMsg.setSchemeId(clientMessage.getSchemeId()); + runStateList.add(stateMsg); + } + return runStateList; + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..b1f4fe8 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,75 @@ +server: + port: 8081 + servlet: + context-path: /universaltestsoftware_client_api + encoding: + force: true + charset: UTF-8 + enabled: true + tomcat: + uri-encoding: UTF-8 +logging: + level: + web: trace + org.springframework.web.socket: WARN +#数据源连接信息 +spring: + servlet: + multipart: + enabled: true + max-file-size: 300MB + max-request-size: 2048MB + datasource: + url: jdbc:mysql://192.168.0.201:3306/universaltestsoftware_client_api?useUnicode=true&characterEncoding=utf8&autoReconnect=true&serverTimezone=Asia/Shanghai&useSSL=false&connectTimeout=30000&socketTimeout=30000 + username: root + password: Fushoukeji123 + driver-class-name: com.mysql.jdbc.Driver + druid: + keep-alive: true # 开启连接保活(核心) + initial-size: 5 # 初始化连接数 + min-idle: 5 # 最小空闲连接数 + max-active: 2000 # 最大活跃连接数 + max-wait: 30000 # 获取连接最大等待时间(30秒) + # 调整空闲连接检测频率:60秒检测一次(降低性能开销) + time-between-eviction-runs-millis: 60000 + # 连接最小生存时间:30分钟(远大于MySQL默认超时,避免提前回收) + min-evictable-idle-time-millis: 1800000 + validation-query: SELECT 1 # 测试连接有效性的SQL + test-while-idle: true # 空闲时检测连接(必须开启) + test-on-borrow: true # 获取连接时检测(解决失效连接问题,核心修改) + test-on-return: false # 归还时不检测(性能优化) + pool-prepared-statements: false # MySQL建议关闭PSCache + max-pool-prepared-statement-per-connection-size: 20 + filters: stat,wall # 监控+SQL防火墙 + connection-properties: 'druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000' + # Wall过滤器配置 + filter: + wall: + enabled: true + config: + noneBaseStatementAllow: true + commentAllow: true + mysql: + allowMultiQueries: true + flyway: + #是否启用 + enabled: true + # 可以支持多个location, 用','隔开 + locations: classpath:db/migration + #是否创建元数据表 + validate-on-migrate: true + # flyway 的 clean 命令会删除指定 schema 下的所有 table, 生产务必禁掉。这个默认值是 + clean-disabled: false + # 如果数据库不是空表,需要设置成 true,否则启动报错 + baseline-on-migrate: true + # 版本控制日志表,默认flyway_schema_history,不同系统建议修改数据 + # table: flyway_schema_history + +cbsd: + uploadfilesrootpath: "C:/Java" + scriptBasePackageName: "com.cbsd.client" + scriptRootPath: C:\script + commandRootPath: C:\command + downloadUrl : C:\download + sampling-interval-ms: 1000 # 字段解析采样间隔(毫秒),默认1000即每秒最多解析一帧字段 + diff --git a/src/main/resources/application-pro.yml b/src/main/resources/application-pro.yml new file mode 100644 index 0000000..01622bc --- /dev/null +++ b/src/main/resources/application-pro.yml @@ -0,0 +1,74 @@ +server: + port: 8081 + servlet: + context-path: /universaltestsoftware_client_api + encoding: + force: true + charset: UTF-8 + enabled: true + tomcat: + uri-encoding: UTF-8 +logging: + level: + web: trace + org.springframework.web.socket: WARN +#数据源连接信息 +spring: + servlet: + multipart: + enabled: true + max-file-size: 300MB + max-request-size: 2048MB + datasource: + url: jdbc:mysql://127.0.0.1:3306/universaltestsoftware_client_api?useUnicode=true&characterEncoding=utf8&autoReconnect=true&serverTimezone=Asia/Shanghai&useSSL=false&connectTimeout=30000&socketTimeout=30000 + username: root + password: + driver-class-name: com.mysql.jdbc.Driver + druid: + keep-alive: true # 开启连接保活(核心) + initial-size: 5 # 初始化连接数 + min-idle: 5 # 最小空闲连接数 + max-active: 2000 # 最大活跃连接数 + max-wait: 30000 # 获取连接最大等待时间(30秒) + # 调整空闲连接检测频率:60秒检测一次(降低性能开销) + time-between-eviction-runs-millis: 60000 + # 连接最小生存时间:30分钟(远大于MySQL默认超时,避免提前回收) + min-evictable-idle-time-millis: 1800000 + validation-query: SELECT 1 # 测试连接有效性的SQL + test-while-idle: true # 空闲时检测连接(必须开启) + test-on-borrow: true # 获取连接时检测(解决失效连接问题,核心修改) + test-on-return: false # 归还时不检测(性能优化) + pool-prepared-statements: false # MySQL建议关闭PSCache + max-pool-prepared-statement-per-connection-size: 20 + filters: stat,wall # 监控+SQL防火墙 + connection-properties: 'druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000' + # Wall过滤器配置 + filter: + wall: + enabled: true + config: + noneBaseStatementAllow: true + commentAllow: true + mysql: + allowMultiQueries: true + flyway: + #是否启用 + enabled: true + # 可以支持多个location, 用','隔开 + locations: classpath:db/migration + #是否创建元数据表 + validate-on-migrate: true + # flyway 的 clean 命令会删除指定 schema 下的所有 table, 生产务必禁掉。这个默认值是 + clean-disabled: false + # 如果数据库不是空表,需要设置成 true,否则启动报错 + baseline-on-migrate: true + # 版本控制日志表,默认flyway_schema_history,不同系统建议修改数据 + # table: flyway_schema_history + +cbsd: + uploadfilesrootpath: "C:/Java" + scriptBasePackageName: "com.cbsd.client" + scriptRootPath: C:\script + commandRootPath: C:\command + downloadUrl : C:\download + sampling-interval-ms: 1000 # 字段解析采样间隔(毫秒),默认1000即每秒最多解析一帧字段 \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..92a118b --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,23 @@ +spring: + #放开springmvc对静态资源的拦截 + mvc: + static-path-pattern: /static/** + profiles: + active: pro + +mybatis-plus: + # 包扫描路径(当前项目的实体类所在位置。别名包扫描路径,通过该属性可以给包中的类注册别名,多个路径用逗号分割) + type-aliases-package: com.cbsd.managerUser.* + # xml扫描,多个目录用逗号或者分号分隔(告诉 Mapper 所对应的 XML 文件位置) + mapper-locations: classpath*:mapper/**/*.xml + configuration: + # 是否开启自动驼峰命名规则映射:从数据库列名到Java属性驼峰命名的类似映射 + map-underscore-to-camel-case: true + # 如果查询结果中包含空值的列,则 MyBatis 在映射的时候,不会映射这个字段 + # 允许在resultType="map"时映射null值 + call-setters-on-nulls: true + # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用(包括执行结果) +# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl +springfox: + documentation: + auto-startup: true #false关闭swagger文档 true打开swagger diff --git a/src/main/resources/db/migration/V1_0__init_client.sql b/src/main/resources/db/migration/V1_0__init_client.sql new file mode 100644 index 0000000..6537788 --- /dev/null +++ b/src/main/resources/db/migration/V1_0__init_client.sql @@ -0,0 +1,236 @@ +/* + Navicat Premium Dump SQL + + Source Server : cbsd-dev + Source Server Type : MySQL + Source Server Version : 50638 (5.6.38) + Source Host : cloud.cbsd-pro.com:3306 + Source Schema : universaltestsoftware_client_api + + Target Server Type : MySQL + Target Server Version : 50638 (5.6.38) + File Encoding : 65001 + + Date: 14/10/2025 14:07:01 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for boot_log +-- ---------------------------- +DROP TABLE IF EXISTS `boot_log`; +CREATE TABLE `boot_log` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID,自增长', + `event_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '事件ID,用于唯一标识一个日志事件', + `event_date` datetime NULL DEFAULT NULL COMMENT '事件发生时间', + `thread` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '产生日志的线程名称', + `log_class` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '产生日志的类名', + `message` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '日志消息内容', + `exception` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '异常堆栈信息', + `level` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '日志级别(如:DEBUG, INFO, WARN, ERROR)', + `time` datetime NULL DEFAULT NULL COMMENT '日志记录时间', + `method` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '产生日志的方法/函数名', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 642869 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '系统启动日志表,记录应用程序运行过程中的日志信息' ROW_FORMAT = Compact; + +-- ---------------------------- +-- Table structure for channel_data +-- ---------------------------- +DROP TABLE IF EXISTS `channel_data`; +CREATE TABLE `channel_data` ( + `data_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '解析数据id', + `channel_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '通道id', + `data` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '数据内容', + `protocol_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '协议id', + `protocol_content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '协议内容', + `time` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '数据时间', + PRIMARY KEY (`data_id`) USING BTREE, + INDEX `INDEX_CHANNEL`(`channel_id`, `protocol_id`) USING BTREE COMMENT '通道id,协议id' +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '解析的数据' ROW_FORMAT = Compact; + +-- ---------------------------- +-- Table structure for data_board +-- ---------------------------- +DROP TABLE IF EXISTS `data_board`; +CREATE TABLE `data_board` ( + `data_board_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '数据看板id', + `data_board_type` int(1) NULL DEFAULT NULL COMMENT '看板类型,0:表格,1:曲线', + `data_board_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '看板名字', + `data_type` int(1) NULL DEFAULT NULL COMMENT '0:遥测,1:数据回放', + `create_time` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`data_board_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '数据看板' ROW_FORMAT = Compact; + +-- ---------------------------- +-- Table structure for data_board_channel +-- ---------------------------- +DROP TABLE IF EXISTS `data_board_channel`; +CREATE TABLE `data_board_channel` ( + `data_board_channel_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '数据看板通道id', + `data_board_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '数据看板id', + `channel_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '通道Id', + `channel_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '通道名字', + `parameter` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '参数名字', + `create_time` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建时间', + `sn` int(11) NULL DEFAULT NULL COMMENT '索引', + `is_left_yaxis` int(1) NULL DEFAULT 1 COMMENT '是否是左边的y轴 0否 1是 默认1', + `legend_color` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '图例颜色', + PRIMARY KEY (`data_board_channel_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '数据看板通道' ROW_FORMAT = Compact; + +-- ---------------------------- +-- Table structure for instruct +-- ---------------------------- +DROP TABLE IF EXISTS `instruct`; +CREATE TABLE `instruct` ( + `instruct_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '指令id', + `instruct_type` int(11) NOT NULL DEFAULT 1 COMMENT '指令类型 1.常规指令 2.指令组', + `encoding` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '编码', + `parent_encoding` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '父级编码', + `parent_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '父级名字', + `instruct_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '指令名字', + `server_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '服务id', + `ip_address` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'ip地址', + `tcp_port` int(12) NULL DEFAULT NULL COMMENT '服务端tcp端口', + `instruct_text` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '指令数据', + `channel_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '通道id', + `channel_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '通道名字', + `invocation_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '调用用列id', + `operation_type` int(11) NOT NULL DEFAULT 0 COMMENT '指令类型 0.数据 1.寄存器', + `extra_data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '额外数据', + `board_type_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '板卡类型名称', + `board_index` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '板卡编号', + `protocol_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '协议id', + `protocol_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '协议名字', + `protocol_content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '协议内容', + `sort` int(12) NULL DEFAULT NULL COMMENT '排序', + `status` int(1) NULL DEFAULT 1 COMMENT '状态0:禁用,1:启用', + `lead_time` int(32) NOT NULL DEFAULT 0 COMMENT '前置等待时间ms', + `tail_time` int(32) NOT NULL DEFAULT 0 COMMENT '后置等待时间ms', + `hit_count` int(32) NULL DEFAULT 0 COMMENT '命中次数', + `last_hit_time` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '最后命中时间', + `instruct_tag` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '指令标识', + `pre_process_script` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '预处理脚本', + `pre_process_script_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '预处理脚本文件名称', + `pre_process_script_param` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '预处理脚本参数', + `pre_process_script_file` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '预处理脚本文件', + `pre_process_script_comment` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '预处理脚本注释', + `repeat_count` int(32) NULL DEFAULT 1 COMMENT '重复次数', + `branch_condition_script` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '分支条件脚本', + `branch_condition_script_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分支条件脚本名称', + `branch_condition_script_file` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分支条件脚本文件', + `branch_condition_script_param` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '分支条件脚本参数', + `branch_condition_script_comment` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '分支条件脚本注释', + `check_script` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '检查脚本', + `check_script_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '检查脚本名称', + `check_script_file` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '检查脚本文件', + `check_script_comment` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '检查脚本注释', + `instruct_data_file` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '指令数据文件', + `jump_target` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '跳转目标', + `break_point` int(11) NOT NULL DEFAULT 0 COMMENT '断点 1.是 0.否', + PRIMARY KEY (`instruct_id`) USING BTREE, + UNIQUE INDEX `UNIQUE_NAME`(`invocation_id`, `instruct_name`) USING BTREE COMMENT '同一用例指令名称不能重复' +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '指令' ROW_FORMAT = Compact; + +-- ---------------------------- +-- Table structure for invocation +-- ---------------------------- +DROP TABLE IF EXISTS `invocation`; +CREATE TABLE `invocation` ( + `invocation_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用例id', + `invocation_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用例名字', + `sort` int(11) NOT NULL DEFAULT 1 COMMENT '用例序号', + `scheme_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '方案id', + `create_time` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建时间', + `update_time` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '修改时间', + PRIMARY KEY (`invocation_id`) USING BTREE, + UNIQUE INDEX `UNIQUEEN_NAME`(`scheme_id`, `invocation_name`) USING BTREE COMMENT '同一方案用例名称唯一' +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用列' ROW_FORMAT = Compact; + +-- ---------------------------- +-- Table structure for parser +-- ---------------------------- +DROP TABLE IF EXISTS `parser`; +CREATE TABLE `parser` ( + `parser_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '解析器id', + `parser_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '解析器名称', + `parser_sort` int(11) NULL DEFAULT NULL COMMENT '解析器顺序', + `channel_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '通道id', + `is_delete` int(11) NOT NULL DEFAULT 0 COMMENT '是否删除 1.是 0.否', + `create_time` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`parser_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '解析器' ROW_FORMAT = Compact; + +-- ---------------------------- +-- Table structure for parser_protocol +-- ---------------------------- +DROP TABLE IF EXISTS `parser_protocol`; +CREATE TABLE `parser_protocol` ( + `parser_protocol_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '解析器协议id', + `parser_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '解析器id', + `protocol_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '协议id', + `create_time` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`parser_protocol_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '解析器协议' ROW_FORMAT = Compact; + +-- ---------------------------- +-- Table structure for protocol +-- ---------------------------- +DROP TABLE IF EXISTS `protocol`; +CREATE TABLE `protocol` ( + `protocol_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '协议id', + `protocol_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '协议名字', + `protocol_content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '协议内容', + `protocol_type` int(11) NULL DEFAULT NULL COMMENT '协议类型 1.解析 2.指令', + `protocol_tag` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '协议标签', + `create_time` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建时间', + `update_time` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '修改时间', + PRIMARY KEY (`protocol_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '协议' ROW_FORMAT = Compact; + +-- ---------------------------- +-- Table structure for scheme +-- ---------------------------- +DROP TABLE IF EXISTS `scheme`; +CREATE TABLE `scheme` ( + `scheme_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '方案id', + `scheme_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '方案名字', + `create_time` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建时间', + `update_time` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '修改时间', + PRIMARY KEY (`scheme_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '方案' ROW_FORMAT = Compact; + +-- ---------------------------- +-- Table structure for server +-- ---------------------------- +DROP TABLE IF EXISTS `server`; +CREATE TABLE `server` ( + `server_id` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '服务端id', + `server_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '服务端名字', + `ip_address` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'ip地址', + `tcp_port` int(12) NULL DEFAULT NULL COMMENT '服务端tcp端口', + `http_port` int(12) NULL DEFAULT NULL COMMENT '服务端http端口', + `reconnect_initial_delay` int(32) NULL DEFAULT 1000 COMMENT '重连初始延迟', + `reconnect_max_delay` int(32) NULL DEFAULT 10000 COMMENT '重连最大延迟', + `reconnect_multiplier` double(12, 2) NULL DEFAULT 1.50 COMMENT '重连延迟乘数', + `create_time` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建时间', + `update_time` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '修改时间', + `is_delete` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '是否删除0否1是', + `connect_status` int(1) NULL DEFAULT 0 COMMENT '连接状态0:不连接,1要连接', + PRIMARY KEY (`server_id`) USING BTREE, + UNIQUE INDEX `name_index`(`server_name`, `is_delete`) USING BTREE COMMENT '服务端名字不能重复' +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '服务端' ROW_FORMAT = Compact; + +-- ---------------------------- +-- Table structure for system_config +-- ---------------------------- +DROP TABLE IF EXISTS `system_config`; +CREATE TABLE `system_config` ( + `type` int(11) NOT NULL COMMENT '类型: 1.指令显示列', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '内容', + PRIMARY KEY (`type`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '系统配置(仅类型和内容)' ROW_FORMAT = Compact; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/src/main/resources/db/migration/V1_10__add_channel_data_raw.sql b/src/main/resources/db/migration/V1_10__add_channel_data_raw.sql new file mode 100644 index 0000000..d82fd8e --- /dev/null +++ b/src/main/resources/db/migration/V1_10__add_channel_data_raw.sql @@ -0,0 +1 @@ +ALTER TABLE `channel_data` ADD COLUMN `raw_data` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '原始帧数据(十六进制)' AFTER `data`; diff --git a/src/main/resources/db/migration/V1_1__sensor_equipment.sql b/src/main/resources/db/migration/V1_1__sensor_equipment.sql new file mode 100644 index 0000000..6d2b0fc --- /dev/null +++ b/src/main/resources/db/migration/V1_1__sensor_equipment.sql @@ -0,0 +1,11 @@ +DROP TABLE IF EXISTS `sensor_equipment`; +CREATE TABLE `sensor_equipment` ( + `equipment_id` varchar(32) NOT NULL COMMENT '设备id', + `equipment_name` varchar(255) DEFAULT NULL COMMENT '设备名称', + `equipment_desc` text COMMENT '设备描述', + `channel_id` varchar(32) DEFAULT NULL COMMENT '通道id', + `channel_address` varchar(255) DEFAULT NULL COMMENT '通道地址', + `equipment_param` varchar(255) DEFAULT NULL COMMENT '设备参数', + `create_time` varchar(20) DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`equipment_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='传感设备'; \ No newline at end of file diff --git a/src/main/resources/db/migration/V1_2__instruct_change.sql b/src/main/resources/db/migration/V1_2__instruct_change.sql new file mode 100644 index 0000000..e400862 --- /dev/null +++ b/src/main/resources/db/migration/V1_2__instruct_change.sql @@ -0,0 +1,4 @@ +ALTER TABLE instruct ADD COLUMN instruct_function_name varchar(255) DEFAULT NULL COMMENT '指令寄存器方法名称'; + +ALTER TABLE instruct DROP COLUMN protocol_content; + diff --git a/src/main/resources/db/migration/V1_3__instruct_data_split.sql b/src/main/resources/db/migration/V1_3__instruct_data_split.sql new file mode 100644 index 0000000..16af3fb --- /dev/null +++ b/src/main/resources/db/migration/V1_3__instruct_data_split.sql @@ -0,0 +1,2 @@ +ALTER TABLE instruct ADD COLUMN data_splitting_length int(11) NOT NULL DEFAULT '0' COMMENT '数据拆分长度'; + diff --git a/src/main/resources/db/migration/V1_4__instruct_tail_time.sql b/src/main/resources/db/migration/V1_4__instruct_tail_time.sql new file mode 100644 index 0000000..fa293d2 --- /dev/null +++ b/src/main/resources/db/migration/V1_4__instruct_tail_time.sql @@ -0,0 +1 @@ +ALTER TABLE instruct ALTER COLUMN tail_time SET DEFAULT '1000'; \ No newline at end of file diff --git a/src/main/resources/db/migration/V1_5__data_board_channel.sql b/src/main/resources/db/migration/V1_5__data_board_channel.sql new file mode 100644 index 0000000..5f612b6 --- /dev/null +++ b/src/main/resources/db/migration/V1_5__data_board_channel.sql @@ -0,0 +1,3 @@ +ALTER TABLE `data_board_channel` +ADD COLUMN `protocol_id` varchar(64) DEFAULT NULL COMMENT '协议id' AFTER `legend_color`, +ADD COLUMN `protocol_name` varchar(255) DEFAULT NULL COMMENT '协议名字' AFTER `protocol_id`; \ No newline at end of file diff --git a/src/main/resources/db/migration/V1_6__channel_data_statistics.sql b/src/main/resources/db/migration/V1_6__channel_data_statistics.sql new file mode 100644 index 0000000..7c9554d --- /dev/null +++ b/src/main/resources/db/migration/V1_6__channel_data_statistics.sql @@ -0,0 +1,20 @@ +DROP TABLE IF EXISTS `channel_data_traffic`; +CREATE TABLE `channel_data_traffic` ( + `data_traffic_id` varchar(32) NOT NULL COMMENT '数据流量id', + `channel_id` varchar(50) DEFAULT NULL COMMENT '通道id', + `byte_count` int(11) NOT NULL DEFAULT '0' COMMENT '字节数', + `update_time` varchar(50) DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`data_traffic_id`), + UNIQUE KEY `INDEX_CHANNEL_ID` (`channel_id`) USING BTREE COMMENT '通道唯一' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通道流量统计'; + +DROP TABLE IF EXISTS `channel_data_hit`; +CREATE TABLE `channel_data_hit` ( + `hit_id` varchar(32) NOT NULL COMMENT '命中id', + `channel_id` varchar(50) DEFAULT NULL COMMENT '通道id', + `protocol_id` varchar(50) DEFAULT NULL COMMENT '协议id', + `hit_count` int(11) NOT NULL DEFAULT '0' COMMENT '命中次数', + `update_time` varchar(50) DEFAULT NULL COMMENT '命中更新时间', + PRIMARY KEY (`hit_id`), + UNIQUE KEY `INDEX_1` (`channel_id`,`protocol_id`) USING BTREE COMMENT '通道+协议唯一' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通道协议命中次数'; \ No newline at end of file diff --git a/src/main/resources/db/migration/V1_7__1_27.sql b/src/main/resources/db/migration/V1_7__1_27.sql new file mode 100644 index 0000000..76f84f6 --- /dev/null +++ b/src/main/resources/db/migration/V1_7__1_27.sql @@ -0,0 +1,3 @@ +ALTER TABLE instruct ADD COLUMN instruct_text_data longtext DEFAULT NULL COMMENT '指令数据列表'; + + diff --git a/src/main/resources/db/migration/V1_8__download.sql b/src/main/resources/db/migration/V1_8__download.sql new file mode 100644 index 0000000..a510806 --- /dev/null +++ b/src/main/resources/db/migration/V1_8__download.sql @@ -0,0 +1,13 @@ +DROP TABLE IF EXISTS `download`; +CREATE TABLE `download` ( + `download_id` varchar(64) NOT NULL COMMENT '下载id', + `start_time` varchar(64) DEFAULT NULL COMMENT '开始时间', + `end_time` varchar(64) DEFAULT NULL COMMENT '结束时间', + `create_time` varchar(64) DEFAULT NULL COMMENT '创建时间', + `channel_name` varchar(255) DEFAULT NULL COMMENT '通道名字', + `protocol_name` varchar(255) DEFAULT NULL COMMENT '协议名字', + `state` int(1) DEFAULT '1' COMMENT '1:生成中,2:已完成', + `download_url` varchar(255) DEFAULT NULL COMMENT '下载地址', + `type` int(1) DEFAULT NULL COMMENT '类型 1:解析数据 ,2:原数据', + PRIMARY KEY (`download_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/src/main/resources/db/migration/V1_9__2_10.sql b/src/main/resources/db/migration/V1_9__2_10.sql new file mode 100644 index 0000000..a18184e --- /dev/null +++ b/src/main/resources/db/migration/V1_9__2_10.sql @@ -0,0 +1,3 @@ +CREATE INDEX idx_channel_protocol_time ON channel_data (channel_id, protocol_id, time DESC); + + diff --git a/src/main/resources/mapper/BootLogMapper.xml b/src/main/resources/mapper/BootLogMapper.xml new file mode 100644 index 0000000..c023821 --- /dev/null +++ b/src/main/resources/mapper/BootLogMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/mapper/ChannelDataHitMapper.xml b/src/main/resources/mapper/ChannelDataHitMapper.xml new file mode 100644 index 0000000..25525b4 --- /dev/null +++ b/src/main/resources/mapper/ChannelDataHitMapper.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + hit_id,channel_id,protocol_id,hit_count,update_time + + diff --git a/src/main/resources/mapper/ChannelDataMapper.xml b/src/main/resources/mapper/ChannelDataMapper.xml new file mode 100644 index 0000000..b772ed2 --- /dev/null +++ b/src/main/resources/mapper/ChannelDataMapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + data_id,channel_id,data,time + + diff --git a/src/main/resources/mapper/ChannelDataTrafficMapper.xml b/src/main/resources/mapper/ChannelDataTrafficMapper.xml new file mode 100644 index 0000000..8aef8f8 --- /dev/null +++ b/src/main/resources/mapper/ChannelDataTrafficMapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + data_traffic_id,channel_id,byte_count,update_time + + diff --git a/src/main/resources/mapper/DataBoardChannelMapper.xml b/src/main/resources/mapper/DataBoardChannelMapper.xml new file mode 100644 index 0000000..33b190c --- /dev/null +++ b/src/main/resources/mapper/DataBoardChannelMapper.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/src/main/resources/mapper/DataBoardMapper.xml b/src/main/resources/mapper/DataBoardMapper.xml new file mode 100644 index 0000000..04b6296 --- /dev/null +++ b/src/main/resources/mapper/DataBoardMapper.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + data_board_id, data_board_type, data_board_name, create_time + + + diff --git a/src/main/resources/mapper/DownloadMapper.xml b/src/main/resources/mapper/DownloadMapper.xml new file mode 100644 index 0000000..1ad5874 --- /dev/null +++ b/src/main/resources/mapper/DownloadMapper.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + download_id, start_time, end_time, create_time, channel_name, protocol_name, state, download_url + + + diff --git a/src/main/resources/mapper/InstructMapper.xml b/src/main/resources/mapper/InstructMapper.xml new file mode 100644 index 0000000..215ad4f --- /dev/null +++ b/src/main/resources/mapper/InstructMapper.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + UPDATE instruct + SET sort = #{sort} + WHERE instruct_id = #{instructId} + + + + + + UPDATE instruct t + JOIN ( + SELECT instruct_id, + (@rn := @rn + 1) AS rn + FROM (SELECT * FROM instruct WHERE invocation_id = #{invocationId} ORDER BY sort) tmp, + (SELECT @rn := 0) init + ) r ON t.instruct_id = r.instruct_id + SET t.sort = r.rn + WHERE t.invocation_id = #{invocationId} + + + + + + + + + UPDATE instruct + SET sort = 0 + WHERE instruct_id IN + + #{id} + + + + + + + + + + UPDATE instruct + SET sort = sort + 1 + WHERE invocation_id = #{invocationId} + AND sort >= #{sort} and instruct_id !=#{instructId} + + + UPDATE instruct + SET sort = sort - 1 + WHERE invocation_id = #{invocationId} + AND sort >= #{oldSort} + AND sort <= #{newSort} + AND instruct_id != #{instructId} + + + UPDATE instruct + SET sort = sort + 1 + WHERE invocation_id = #{invocationId} + AND sort >= #{newSort} + AND sort <= #{oldSort} + AND instruct_id != #{instructId} + + + + diff --git a/src/main/resources/mapper/InvocationMapper.xml b/src/main/resources/mapper/InvocationMapper.xml new file mode 100644 index 0000000..529965d --- /dev/null +++ b/src/main/resources/mapper/InvocationMapper.xml @@ -0,0 +1,17 @@ + + + + + + update invocation set sort = sort - 1 where sort > #{oldSort} and sort <= #{newSort} and scheme_id = #{schemeId} + + + + update invocation set sort = sort + 1 where sort >= #{newSort} and sort < #{oldSort} and scheme_id = #{schemeId} + + + + update invocation set sort = sort - 1 where sort >= #{startSort} and scheme_id = #{schemeId} + + + diff --git a/src/main/resources/mapper/ParserMapper.xml b/src/main/resources/mapper/ParserMapper.xml new file mode 100644 index 0000000..78dc214 --- /dev/null +++ b/src/main/resources/mapper/ParserMapper.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + parser_id,parser_name,parser_sort,channel_id,is_delete,create_time + + diff --git a/src/main/resources/mapper/ParserProtocolMapper.xml b/src/main/resources/mapper/ParserProtocolMapper.xml new file mode 100644 index 0000000..eaeeda4 --- /dev/null +++ b/src/main/resources/mapper/ParserProtocolMapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + parser_protocol_id,parser_id,protocol_id,create_time + + diff --git a/src/main/resources/mapper/ProtocolMapper.xml b/src/main/resources/mapper/ProtocolMapper.xml new file mode 100644 index 0000000..071a5a0 --- /dev/null +++ b/src/main/resources/mapper/ProtocolMapper.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + protocol_id, protocol_name, protocol_content, protocol_tag, create_time, update_time + + + diff --git a/src/main/resources/mapper/SchemeMapper.xml b/src/main/resources/mapper/SchemeMapper.xml new file mode 100644 index 0000000..548cafc --- /dev/null +++ b/src/main/resources/mapper/SchemeMapper.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + scheme_id, scheme_name, create_time, update_time + + + diff --git a/src/main/resources/mapper/SensorEquipmentMapper.xml b/src/main/resources/mapper/SensorEquipmentMapper.xml new file mode 100644 index 0000000..2fdbfd6 --- /dev/null +++ b/src/main/resources/mapper/SensorEquipmentMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/src/main/resources/mapper/ServerMapper.xml b/src/main/resources/mapper/ServerMapper.xml new file mode 100644 index 0000000..e43d5be --- /dev/null +++ b/src/main/resources/mapper/ServerMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/mapper/SystemConfigMapper.xml b/src/main/resources/mapper/SystemConfigMapper.xml new file mode 100644 index 0000000..fbb72f6 --- /dev/null +++ b/src/main/resources/mapper/SystemConfigMapper.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + type,content + + diff --git a/src/main/xml/protocol.xml b/src/main/xml/protocol.xml new file mode 100644 index 0000000..1e22ad7 --- /dev/null +++ b/src/main/xml/protocol.xml @@ -0,0 +1,163 @@ + + 温湿度协议 + + 0x01 + + 0 + 4 + CrcCheck + + + + + 0 + + 帧头 + hex + + 8 + 0 + + + + BIG + HEAD + + + + + + + + + + + + + + 1 + + 功能码 + hex + + 8 + 0 + + + + BIG + FIX + + + + + + + + + + + + + + 2 + + 数据长度 + int + + 8 + 0 + + + + BIG + LENGTH + + + + + + + + + + + + + + 3 + + 温度 + double + + 16 + 0 + + + TemperatureParser + BIG + VAR + + + + + + + + + + + + + + 4 + + 湿度 + double + + 16 + 0 + + + TemperatureParser + BIG + VAR + + + + + % + + + + + + + + + 5 + + 校验位 + hex + + 16 + 0 + + + + BIG + CALC + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/xml/protocolForInstruct.xml b/src/main/xml/protocolForInstruct.xml new file mode 100644 index 0000000..a6a5d02 --- /dev/null +++ b/src/main/xml/protocolForInstruct.xml @@ -0,0 +1,137 @@ + + 指令协议 + + + + + + + + + + 0 + + 地址 + hex + + 8 + 0 + + + + BIG + VAR + + + + + + + + + + + + + + 1 + + 功能码 + hex + + 8 + 0 + + + + BIG + VAR + + + + + + + + + + + + + + 2 + + 日期 + hex + + 8 + 0 + + + + BIG + DATE + + + + + + + + + + + + + + 2 + + 时间 + hex + + 8 + 0 + + + + BIG + TIME + + + + + + + + + + + + + + 3 + + 次数 + int + + 8 + 0 + + + + BIG + VAR + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/xml/protocolForVoltage.xml b/src/main/xml/protocolForVoltage.xml new file mode 100644 index 0000000..14ba483 --- /dev/null +++ b/src/main/xml/protocolForVoltage.xml @@ -0,0 +1,812 @@ + + 电压协议 + + + + + + + + + + 0 + + 电压-0 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 1 + + 电压-1 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 2 + + 电压-2 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 3 + + 电压-3 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 4 + + 电压-4 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 5 + + 电压-5 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 6 + + 电压-6 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 7 + + 电压-7 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 8 + + 电压-8 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 9 + + 电压-9 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 10 + + 电压-10 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 11 + + 电压-11 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 12 + + 电压-12 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 13 + + 电压-13 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 14 + + 电压-14 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 15 + + 电压-15 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 16 + + 电压-16 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 17 + + 电压-17 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 18 + + 电压-18 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 19 + + 电压-19 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 20 + + 电压-20 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 21 + + 电压-21 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 22 + + 电压-22 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 23 + + 电压-23 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 24 + + 电压-24 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 25 + + 电压-25 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 26 + + 电压-26 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 27 + + 电压-27 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 28 + + 电压-28 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 29 + + 电压-29 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 30 + + 电压-30 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + 31 + + 电压-31 + double + + 64 + 0 + + + VoltageParser + + VAR + + + + + V + + + + + + + + + \ No newline at end of file diff --git a/src/main/xml/protocolWithNoHeader.xml b/src/main/xml/protocolWithNoHeader.xml new file mode 100644 index 0000000..a1b3b4d --- /dev/null +++ b/src/main/xml/protocolWithNoHeader.xml @@ -0,0 +1,62 @@ + + 温湿度无帧头协议 + + + + + + + + + + 0 + + 温度 + double + + 16 + 0 + + + TemperatureParser + BIG + VAR + + + + + + + + + + + + + + 1 + + 湿度 + double + + 16 + 0 + + + TemperatureParser + BIG + VAR + + + + + % + + + + + + + + + \ No newline at end of file diff --git a/src/main/xml/protocol_digital.xml b/src/main/xml/protocol_digital.xml new file mode 100644 index 0000000..c5201de --- /dev/null +++ b/src/main/xml/protocol_digital.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/xml/protocol_variable_length.xml b/src/main/xml/protocol_variable_length.xml new file mode 100644 index 0000000..231fcfc --- /dev/null +++ b/src/main/xml/protocol_variable_length.xml @@ -0,0 +1,147 @@ + + 温湿度协议 + + 0x01 + + 0 + 7 + CrcCheck + + + 0x01 + 10 + + + 0x02 + 20 + + + + + + 0 + + 帧头 + hex + + 8 + 0 + + + + BIG + HEAD + + + + + + + + + + + + + + 1 + + 功能码 + hex + + 8 + 0 + + + + BIG + FIX + + + + + + + + + + + + + + 2 + + 数据长度 + int + + 8 + 0 + + + + BIG + VARIABLE_LENGTH + + + + + + + + + + + + + + 3 + + 温度 + double + + 0 + 0 + + + TemperatureParser + BIG + VAR + + + + + + + + + + + + + + 4 + + 校验位 + hex + + 16 + 0 + + + + BIG + CALC + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/com/cbsd/universaltestsoftware_client/UniversaltestsoftwareClientApiApplicationTests.java b/src/test/java/com/cbsd/universaltestsoftware_client/UniversaltestsoftwareClientApiApplicationTests.java new file mode 100644 index 0000000..f942a36 --- /dev/null +++ b/src/test/java/com/cbsd/universaltestsoftware_client/UniversaltestsoftwareClientApiApplicationTests.java @@ -0,0 +1,13 @@ +package com.cbsd.universaltestsoftware_client; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class UniversaltestsoftwareClientApiApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/src/test/java/com/cbsd/universaltestsoftware_client/controller/BoardControllerTest.java b/src/test/java/com/cbsd/universaltestsoftware_client/controller/BoardControllerTest.java new file mode 100644 index 0000000..4931612 --- /dev/null +++ b/src/test/java/com/cbsd/universaltestsoftware_client/controller/BoardControllerTest.java @@ -0,0 +1,119 @@ +package com.cbsd.universaltestsoftware_client.controller; + +import com.cbsd.universaltestsoftware_client.dto.BoardTemperatureDto; +import com.cbsd.universaltestsoftware_client.dto.ChannelPageDto; +import com.cbsd.universaltestsoftware_client.dto.PageBoard; +import com.cbsd.universaltestsoftware_client.entity.Board; +import com.cbsd.universaltestsoftware_client.entity.Channel; +import com.cbsd.universaltestsoftware_client.service.BoardService; +import com.cbsd.universaltestsoftware_client.util.Result; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.mockito.Mockito.*; + +class BoardControllerTest { + @Mock + BoardService boardService; + @InjectMocks + BoardController boardController; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void testPageList() { + when(boardService.pageList(any(PageBoard.class))).thenReturn("pageListResponse"); + + String result = boardController.pageList(new PageBoard("1960241264519610369")); + Assertions.assertEquals("pageListResponse", result); + } + + @Test + void testSaveOrUpdateBoard() { + when(boardService.saveOrUpdateBoard(any(Board.class))).thenReturn("saveOrUpdateBoardResponse"); + + String result = boardController.saveOrUpdateBoard(new Board()); + Assertions.assertEquals("saveOrUpdateBoardResponse", result); + } + + @Test + void testDeleteById() { + when(boardService.deleteById(anyString(), anyString())).thenReturn("deleteByIdResponse"); + + String result = boardController.deleteById("serverId", "boardId"); + Assertions.assertEquals("deleteByIdResponse", result); + } + + @Test + void testGetBoardType() { + when(boardService.getBoardType(anyString())).thenReturn("getBoardTypeResponse"); + + String result = boardController.getBoardType("serverId"); + Assertions.assertEquals("getBoardTypeResponse", result); + } + + @Test + void testSaveOrUpdateChannel() { + when(boardService.saveOrUpdateChannel(any(Channel.class))).thenReturn("saveOrUpdateChannelResponse"); + + String result = boardController.saveOrUpdateChannel(new Channel()); + Assertions.assertEquals("saveOrUpdateChannelResponse", result); + } + + @Test + void testPageChannelList() { + when(boardService.pageChannelList(any(ChannelPageDto.class))).thenReturn(new Result()); + + Result result = boardController.pageChannelList(new ChannelPageDto("", "")); + Assertions.assertEquals(new Result(), result); + } + + @Test + void testGetChannelTypeAll() { + when(boardService.getChannelTypeAll(anyString(), anyString())).thenReturn("getChannelTypeAllResponse"); + + String result = boardController.getChannelTypeAll("serverId", "boardTypeId"); + Assertions.assertEquals("getChannelTypeAllResponse", result); + } + + @Test + void testDeleteChannelById() { + when(boardService.deleteChannelById(anyString(), anyString())).thenReturn("deleteChannelByIdResponse"); + + String result = boardController.deleteChannelById("serverId", "channelId"); + Assertions.assertEquals("deleteChannelByIdResponse", result); + } + + @Test + void testGetChannelPath() { + when(boardService.getChannelPath(anyString(), anyString())).thenReturn("getChannelPathResponse"); + + String result = boardController.getChannelPath("serverId", "boardId"); + Assertions.assertEquals("getChannelPathResponse", result); + } + + @Test + void testRefressBoardInformation() { + when(boardService.refressBoardInformation(anyString())).thenReturn("refressBoardInformationResponse"); + + String result = boardController.refressBoardInformation("serverId"); + Assertions.assertEquals("refressBoardInformationResponse", result); + } + + @Test + void testGetBoardTemperature() { + when(boardService.getBoardTemperature(any(BoardTemperatureDto.class))).thenReturn("getBoardTemperatureResponse"); + + String result = boardController.getBoardTemperature(new BoardTemperatureDto()); + Assertions.assertEquals("getBoardTemperatureResponse", result); + } +} + +//Generated with love by TestMe :) Please raise issues & feature requests at: https://weirddev.com/forum#!/testme \ No newline at end of file diff --git a/src/test/java/com/cbsd/universaltestsoftware_client/service/impl/BoardServiceImplTest.java b/src/test/java/com/cbsd/universaltestsoftware_client/service/impl/BoardServiceImplTest.java new file mode 100644 index 0000000..27be5ca --- /dev/null +++ b/src/test/java/com/cbsd/universaltestsoftware_client/service/impl/BoardServiceImplTest.java @@ -0,0 +1,48 @@ +package com.cbsd.universaltestsoftware_client.service.impl; + +import com.cbsd.universaltestsoftware_client.dto.PageBoard; +import com.cbsd.universaltestsoftware_client.entity.ServerInfo; +import com.cbsd.universaltestsoftware_client.mapper.ChannelDataTrafficMapper; +import com.cbsd.universaltestsoftware_client.mapper.ParserMapper; +import com.cbsd.universaltestsoftware_client.mapper.ServerMapper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.Serializable; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.when; + +class BoardServiceImplTest { + @Mock + ServerMapper serverMapper; + @Mock + ParserMapper parserMapper; + @Mock + ChannelDataTrafficMapper channelDataTrafficMapper; + @InjectMocks + BoardServiceImpl boardServiceImpl; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void testPageList() { + ServerInfo serverInfo = new ServerInfo(); + serverInfo.setConnectStatus(1); + serverInfo.setIpAddress("192.168.100.4"); + serverInfo.setHttpPort(8082); + when(serverMapper.selectById(any(Serializable.class))).thenReturn(serverInfo); + + String result = boardServiceImpl.pageList(new PageBoard("1960241264519610369")); + Assertions.assertTrue(result.contains("\"code\":1")); + } +} + +//Generated with love by TestMe :) Please raise issues & feature requests at: https://weirddev.com/forum#!/testme \ No newline at end of file diff --git a/update_channel_data_structure.sql b/update_channel_data_structure.sql new file mode 100644 index 0000000..8a6f282 --- /dev/null +++ b/update_channel_data_structure.sql @@ -0,0 +1,18 @@ +# 第一步 新增new_id字段 +ALTER TABLE channel_data ADD COLUMN new_id INT NOT NULL FIRST; + +# 第二步 给new_id赋值 +SET @row_num = 0; +UPDATE channel_data SET new_id = (@row_num := @row_num + 1); + +# 第三步 删除原主键,将new_id设为主键 +ALTER TABLE channel_data DROP PRIMARY KEY, ADD PRIMARY KEY (new_id); + +# 第四步 将new_id设置为自增 +ALTER TABLE channel_data MODIFY COLUMN new_id INT NOT NULL AUTO_INCREMENT; + +# 第五步 删除原data_id字段 +ALTER TABLE channel_data DROP COLUMN data_id; + +# 第六步 重命名new_id字段为data_id +ALTER TABLE channel_data CHANGE COLUMN new_id data_id INT NOT NULL AUTO_INCREMENT;