From 84b3f54afabe021ecec408e1c3b688dd6be2aeec Mon Sep 17 00:00:00 2001 From: Shu Guang <61069967+shuguangnet@users.noreply.github.com> Date: Thu, 15 May 2025 23:48:47 +0800 Subject: [PATCH] feat: update api of openai --- .gitignore | 35 + .mvn/wrapper/maven-wrapper.jar | Bin 0 -> 58727 bytes .mvn/wrapper/maven-wrapper.properties | 2 + LICENSE | 201 ++++ README.md | 842 +++++++++++++++++ README_en.md | 881 ++++++++++++++++++ doc/swagger.png | Bin 0 -> 263468 bytes mvnw | 316 +++++++ mvnw.cmd | 188 ++++ pom.xml | 197 ++++ sql/bms_boot_structure.sql | 247 +++++ .../backend/VueBookBackendApplication.java | 23 + .../com/book/backend/common/BaseContext.java | 25 + .../com/book/backend/common/BasePage.java | 30 + .../book/backend/common/CustomException.java | 11 + .../book/backend/common/JwtProperties.java | 46 + .../backend/common/MyMetaObjectHandler.java | 46 + src/main/java/com/book/backend/common/R.java | 72 ++ .../common/exception/BusinessException.java | 34 + .../backend/common/exception/CommonError.java | 19 + .../backend/common/exception/ErrorCode.java | 42 + .../exception/GlobalExceptionHandler.java | 26 + .../backend/common/exception/ThrowUtils.java | 43 + .../common/exception/VueBookException.java | 59 ++ .../config/HttpSessionConfigurator.java | 39 + .../com/book/backend/config/JsonConfig.java | 31 + .../book/backend/config/MyBatisConfig.java | 27 + .../com/book/backend/config/RedisConfig.java | 31 + .../book/backend/config/RedissonConfig.java | 37 + .../config/ThreadPoolExecutorConfig.java | 34 + .../com/book/backend/config/WebMvcConfig.java | 30 + .../book/backend/config/WebSocketConfig.java | 20 + .../book/backend/constant/ChatConstant.java | 16 + .../com/book/backend/constant/Constant.java | 58 ++ .../admin/AdminFunctionController.java | 414 ++++++++ .../admin/AdminLoginController.java | 44 + .../BookAdminFunctionController.java | 165 ++++ .../bookadmin/BookAdminLoginController.java | 48 + .../user/UserFunctionController.java | 165 ++++ .../controller/user/UserLoginController.java | 46 + .../interceptor/AuthInterceptorHandler.java | 89 ++ .../job/cycle/ConvertCommentListToRedis.java | 59 ++ .../job/cycle/IncSyncDeleteAIMessage.java | 83 ++ .../once/EasyExcelBatchImportBookList.java | 67 ++ .../backend/job/once/FetchInitBookList.java | 81 ++ .../com/book/backend/manager/AiManager.java | 39 + .../java/com/book/backend/manager/AiPost.java | 79 ++ .../book/backend/manager/AlibabaAIModel.java | 84 ++ .../manager/GuavaRateLimiterManager.java | 27 + .../book/backend/manager/SparkAIManager.java | 307 ++++++ .../com/book/backend/manager/SparkClient.java | 109 +++ .../manager/constant/SparkApiVersion.java | 50 + .../manager/constant/SparkErrorCode.java | 43 + .../manager/constant/SparkMessageRole.java | 25 + .../manager/exception/SparkException.java | 52 ++ .../manager/listener/SparkBaseListener.java | 146 +++ .../listener/SparkConsoleListener.java | 98 ++ .../listener/SparkSyncChatListener.java | 65 ++ .../manager/model/SparkChatParameter.java | 94 ++ .../backend/manager/model/SparkMessage.java | 112 +++ .../manager/model/SparkRequestBuilder.java | 130 +++ .../manager/model/SparkSyncChatResponse.java | 76 ++ .../manager/model/request/SparkRequest.java | 63 ++ .../model/request/SparkRequestHeader.java | 43 + .../model/request/SparkRequestMessage.java | 33 + .../model/request/SparkRequestParameter.java | 32 + .../model/request/SparkRequestPayload.java | 47 + .../function/SparkFunctionBuilder.java | 81 ++ .../function/SparkRequestFunctionMessage.java | 61 ++ .../SparkRequestFunctionParameters.java | 65 ++ .../SparkRequestFunctionProperty.java | 47 + .../function/SparkRequestFunctions.java | 31 + .../manager/model/response/SparkResponse.java | 32 + .../model/response/SparkResponseChoices.java | 54 ++ .../response/SparkResponseFunctionCall.java | 53 ++ .../model/response/SparkResponseHeader.java | 64 ++ .../model/response/SparkResponsePayload.java | 32 + .../model/response/SparkResponseUsage.java | 22 + .../model/response/SparkTextUsage.java | 71 ++ .../com/book/backend/mapper/AdminsMapper.java | 18 + .../backend/mapper/AiIntelligentMapper.java | 18 + .../book/backend/mapper/BookAdminsMapper.java | 18 + .../book/backend/mapper/BookRuleMapper.java | 18 + .../book/backend/mapper/BookTypeMapper.java | 18 + .../backend/mapper/BooksBorrowMapper.java | 18 + .../com/book/backend/mapper/BooksMapper.java | 18 + .../com/book/backend/mapper/ChartMapper.java | 18 + .../com/book/backend/mapper/ChatMapper.java | 18 + .../book/backend/mapper/CommentMapper.java | 18 + .../com/book/backend/mapper/NoticeMapper.java | 18 + .../mapper/UserInterfaceInfoMapper.java | 18 + .../com/book/backend/mapper/UsersMapper.java | 20 + .../book/backend/mapper/ViolationMapper.java | 18 + .../java/com/book/backend/pojo/Admins.java | 56 ++ .../com/book/backend/pojo/AiIntelligent.java | 50 + .../com/book/backend/pojo/BookAdmins.java | 61 ++ .../java/com/book/backend/pojo/BookRule.java | 61 ++ .../java/com/book/backend/pojo/BookType.java | 46 + .../java/com/book/backend/pojo/Books.java | 76 ++ .../com/book/backend/pojo/BooksBorrow.java | 85 ++ .../java/com/book/backend/pojo/Chart.java | 87 ++ src/main/java/com/book/backend/pojo/Chat.java | 58 ++ .../java/com/book/backend/pojo/Comment.java | 56 ++ .../java/com/book/backend/pojo/Notice.java | 51 + .../book/backend/pojo/UserInterfaceInfo.java | 57 ++ .../java/com/book/backend/pojo/Users.java | 67 ++ .../java/com/book/backend/pojo/Violation.java | 81 ++ .../com/book/backend/pojo/dto/BookDTO.java | 16 + .../com/book/backend/pojo/dto/BookData.java | 63 ++ .../book/backend/pojo/dto/BookRuleDTO.java | 14 + .../book/backend/pojo/dto/BooksBorrowDTO.java | 18 + .../com/book/backend/pojo/dto/BorrowData.java | 26 + .../book/backend/pojo/dto/BorrowTypeDTO.java | 19 + .../com/book/backend/pojo/dto/CommentDTO.java | 17 + .../com/book/backend/pojo/dto/UsersDTO.java | 14 + .../book/backend/pojo/dto/ViolationDTO.java | 26 + .../pojo/dto/chart/ChartAddRequest.java | 37 + .../pojo/dto/chart/ChartEditRequest.java | 42 + .../pojo/dto/chart/ChartUpdateRequest.java | 73 ++ .../pojo/dto/chart/GenChartByAiRequest.java | 35 + .../backend/pojo/dto/chat/ChatRequest.java | 27 + .../backend/pojo/dto/chat/MessageRequest.java | 21 + .../com/book/backend/pojo/vo/BiResponse.java | 16 + .../java/com/book/backend/pojo/vo/ChatVo.java | 38 + .../book/backend/service/AdminsService.java | 44 + .../backend/service/AiIntelligentService.java | 28 + .../backend/service/BookAdminsService.java | 74 ++ .../book/backend/service/BookRuleService.java | 61 ++ .../book/backend/service/BookTypeService.java | 61 ++ .../backend/service/BooksBorrowService.java | 47 + .../book/backend/service/BooksService.java | 91 ++ .../book/backend/service/ChartService.java | 20 + .../com/book/backend/service/ChatService.java | 13 + .../book/backend/service/CommentService.java | 30 + .../book/backend/service/NoticeService.java | 65 ++ .../service/UserInterfaceInfoService.java | 13 + .../book/backend/service/UsersService.java | 75 ++ .../backend/service/ViolationService.java | 31 + .../service/impl/AdminsServiceImpl.java | 196 ++++ .../impl/AiIntelligentServiceImpl.java | 304 ++++++ .../service/impl/BookAdminsServiceImpl.java | 277 ++++++ .../service/impl/BookRuleServiceImpl.java | 152 +++ .../service/impl/BookTypeServiceImpl.java | 130 +++ .../service/impl/BooksBorrowServiceImpl.java | 231 +++++ .../service/impl/BooksServiceImpl.java | 390 ++++++++ .../service/impl/ChartServiceImpl.java | 172 ++++ .../backend/service/impl/ChatServiceImpl.java | 22 + .../service/impl/CommentServiceImpl.java | 136 +++ .../service/impl/NoticeServiceImpl.java | 141 +++ .../impl/UserInterfaceInfoServiceImpl.java | 22 + .../service/impl/UsersServiceImpl.java | 296 ++++++ .../service/impl/ViolationServiceImpl.java | 175 ++++ .../book/backend/utils/BorrowDateUtil.java | 28 + .../com/book/backend/utils/ExcelUtils.java | 68 ++ .../java/com/book/backend/utils/JwtKit.java | 71 ++ .../java/com/book/backend/utils/NetUtils.java | 53 ++ .../com/book/backend/utils/NumberUtil.java | 77 ++ .../book/backend/utils/RandomNameUtils.java | 92 ++ .../com/book/backend/utils/RedisUtil.java | 524 +++++++++++ .../book/backend/utils/SpringBootUtil.java | 63 ++ .../java/com/book/backend/utils/SqlUtils.java | 23 + .../java/com/book/backend/utils/Utility.java | 191 ++++ .../java/com/book/backend/ws/WebSocket.java | 388 ++++++++ src/main/resources/application-dev.yml | 35 + src/main/resources/application.yml | 43 + src/main/resources/banner.txt | 5 + src/main/resources/mapper/AdminsMapper.xml | 22 + .../resources/mapper/AiIntelligentMapper.xml | 20 + .../resources/mapper/BookAdminsMapper.xml | 23 + src/main/resources/mapper/BookRuleMapper.xml | 23 + src/main/resources/mapper/BookTypeMapper.xml | 19 + .../resources/mapper/BooksBorrowMapper.xml | 23 + src/main/resources/mapper/BooksMapper.xml | 27 + src/main/resources/mapper/ChartMapper.xml | 30 + src/main/resources/mapper/ChatMapper.xml | 22 + src/main/resources/mapper/CommentMapper.xml | 22 + src/main/resources/mapper/NoticeMapper.xml | 20 + .../mapper/UserInterfaceInfoMapper.xml | 22 + src/main/resources/mapper/UsersMapper.xml | 24 + src/main/resources/mapper/ViolationMapper.xml | 26 + src/main/resources/test_excel.xlsx | Bin 0 -> 9467 bytes src/test/java/com/book/backend/AITest.java | 360 +++++++ src/test/java/com/book/backend/AliAITest.java | 90 ++ .../java/com/book/backend/BenchmarkTest.java | 46 + .../java/com/book/backend/BigModelNew.java | 298 ++++++ .../java/com/book/backend/CrawlerTest.java | 179 ++++ .../java/com/book/backend/OpenAPITest.java | 78 ++ src/test/java/com/book/backend/RedisTest.java | 47 + .../java/com/book/backend/SparkAITest.java | 66 ++ .../com/book/backend/SparkClientTest.java | 165 ++++ .../VueBookBackendApplicationTests.java | 82 ++ .../book/backend/VueBookBackendUserTest.java | 145 +++ .../com/book/backend/domain/BookData.java | 63 ++ .../com/book/backend/domain/DemoData.java | 22 + .../book/backend/testutils/EasyExcelTest.java | 85 ++ 195 files changed, 15991 insertions(+) create mode 100644 .gitignore create mode 100644 .mvn/wrapper/maven-wrapper.jar create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 LICENSE create mode 100644 README.md create mode 100644 README_en.md create mode 100644 doc/swagger.png create mode 100644 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml create mode 100644 sql/bms_boot_structure.sql create mode 100644 src/main/java/com/book/backend/VueBookBackendApplication.java create mode 100644 src/main/java/com/book/backend/common/BaseContext.java create mode 100644 src/main/java/com/book/backend/common/BasePage.java create mode 100644 src/main/java/com/book/backend/common/CustomException.java create mode 100644 src/main/java/com/book/backend/common/JwtProperties.java create mode 100644 src/main/java/com/book/backend/common/MyMetaObjectHandler.java create mode 100644 src/main/java/com/book/backend/common/R.java create mode 100644 src/main/java/com/book/backend/common/exception/BusinessException.java create mode 100644 src/main/java/com/book/backend/common/exception/CommonError.java create mode 100644 src/main/java/com/book/backend/common/exception/ErrorCode.java create mode 100644 src/main/java/com/book/backend/common/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/book/backend/common/exception/ThrowUtils.java create mode 100644 src/main/java/com/book/backend/common/exception/VueBookException.java create mode 100644 src/main/java/com/book/backend/config/HttpSessionConfigurator.java create mode 100644 src/main/java/com/book/backend/config/JsonConfig.java create mode 100644 src/main/java/com/book/backend/config/MyBatisConfig.java create mode 100644 src/main/java/com/book/backend/config/RedisConfig.java create mode 100644 src/main/java/com/book/backend/config/RedissonConfig.java create mode 100644 src/main/java/com/book/backend/config/ThreadPoolExecutorConfig.java create mode 100644 src/main/java/com/book/backend/config/WebMvcConfig.java create mode 100644 src/main/java/com/book/backend/config/WebSocketConfig.java create mode 100644 src/main/java/com/book/backend/constant/ChatConstant.java create mode 100644 src/main/java/com/book/backend/constant/Constant.java create mode 100644 src/main/java/com/book/backend/controller/admin/AdminFunctionController.java create mode 100644 src/main/java/com/book/backend/controller/admin/AdminLoginController.java create mode 100644 src/main/java/com/book/backend/controller/bookadmin/BookAdminFunctionController.java create mode 100644 src/main/java/com/book/backend/controller/bookadmin/BookAdminLoginController.java create mode 100644 src/main/java/com/book/backend/controller/user/UserFunctionController.java create mode 100644 src/main/java/com/book/backend/controller/user/UserLoginController.java create mode 100644 src/main/java/com/book/backend/interceptor/AuthInterceptorHandler.java create mode 100644 src/main/java/com/book/backend/job/cycle/ConvertCommentListToRedis.java create mode 100644 src/main/java/com/book/backend/job/cycle/IncSyncDeleteAIMessage.java create mode 100644 src/main/java/com/book/backend/job/once/EasyExcelBatchImportBookList.java create mode 100644 src/main/java/com/book/backend/job/once/FetchInitBookList.java create mode 100644 src/main/java/com/book/backend/manager/AiManager.java create mode 100644 src/main/java/com/book/backend/manager/AiPost.java create mode 100644 src/main/java/com/book/backend/manager/AlibabaAIModel.java create mode 100644 src/main/java/com/book/backend/manager/GuavaRateLimiterManager.java create mode 100644 src/main/java/com/book/backend/manager/SparkAIManager.java create mode 100644 src/main/java/com/book/backend/manager/SparkClient.java create mode 100644 src/main/java/com/book/backend/manager/constant/SparkApiVersion.java create mode 100644 src/main/java/com/book/backend/manager/constant/SparkErrorCode.java create mode 100644 src/main/java/com/book/backend/manager/constant/SparkMessageRole.java create mode 100644 src/main/java/com/book/backend/manager/exception/SparkException.java create mode 100644 src/main/java/com/book/backend/manager/listener/SparkBaseListener.java create mode 100644 src/main/java/com/book/backend/manager/listener/SparkConsoleListener.java create mode 100644 src/main/java/com/book/backend/manager/listener/SparkSyncChatListener.java create mode 100644 src/main/java/com/book/backend/manager/model/SparkChatParameter.java create mode 100644 src/main/java/com/book/backend/manager/model/SparkMessage.java create mode 100644 src/main/java/com/book/backend/manager/model/SparkRequestBuilder.java create mode 100644 src/main/java/com/book/backend/manager/model/SparkSyncChatResponse.java create mode 100644 src/main/java/com/book/backend/manager/model/request/SparkRequest.java create mode 100644 src/main/java/com/book/backend/manager/model/request/SparkRequestHeader.java create mode 100644 src/main/java/com/book/backend/manager/model/request/SparkRequestMessage.java create mode 100644 src/main/java/com/book/backend/manager/model/request/SparkRequestParameter.java create mode 100644 src/main/java/com/book/backend/manager/model/request/SparkRequestPayload.java create mode 100644 src/main/java/com/book/backend/manager/model/request/function/SparkFunctionBuilder.java create mode 100644 src/main/java/com/book/backend/manager/model/request/function/SparkRequestFunctionMessage.java create mode 100644 src/main/java/com/book/backend/manager/model/request/function/SparkRequestFunctionParameters.java create mode 100644 src/main/java/com/book/backend/manager/model/request/function/SparkRequestFunctionProperty.java create mode 100644 src/main/java/com/book/backend/manager/model/request/function/SparkRequestFunctions.java create mode 100644 src/main/java/com/book/backend/manager/model/response/SparkResponse.java create mode 100644 src/main/java/com/book/backend/manager/model/response/SparkResponseChoices.java create mode 100644 src/main/java/com/book/backend/manager/model/response/SparkResponseFunctionCall.java create mode 100644 src/main/java/com/book/backend/manager/model/response/SparkResponseHeader.java create mode 100644 src/main/java/com/book/backend/manager/model/response/SparkResponsePayload.java create mode 100644 src/main/java/com/book/backend/manager/model/response/SparkResponseUsage.java create mode 100644 src/main/java/com/book/backend/manager/model/response/SparkTextUsage.java create mode 100644 src/main/java/com/book/backend/mapper/AdminsMapper.java create mode 100644 src/main/java/com/book/backend/mapper/AiIntelligentMapper.java create mode 100644 src/main/java/com/book/backend/mapper/BookAdminsMapper.java create mode 100644 src/main/java/com/book/backend/mapper/BookRuleMapper.java create mode 100644 src/main/java/com/book/backend/mapper/BookTypeMapper.java create mode 100644 src/main/java/com/book/backend/mapper/BooksBorrowMapper.java create mode 100644 src/main/java/com/book/backend/mapper/BooksMapper.java create mode 100644 src/main/java/com/book/backend/mapper/ChartMapper.java create mode 100644 src/main/java/com/book/backend/mapper/ChatMapper.java create mode 100644 src/main/java/com/book/backend/mapper/CommentMapper.java create mode 100644 src/main/java/com/book/backend/mapper/NoticeMapper.java create mode 100644 src/main/java/com/book/backend/mapper/UserInterfaceInfoMapper.java create mode 100644 src/main/java/com/book/backend/mapper/UsersMapper.java create mode 100644 src/main/java/com/book/backend/mapper/ViolationMapper.java create mode 100644 src/main/java/com/book/backend/pojo/Admins.java create mode 100644 src/main/java/com/book/backend/pojo/AiIntelligent.java create mode 100644 src/main/java/com/book/backend/pojo/BookAdmins.java create mode 100644 src/main/java/com/book/backend/pojo/BookRule.java create mode 100644 src/main/java/com/book/backend/pojo/BookType.java create mode 100644 src/main/java/com/book/backend/pojo/Books.java create mode 100644 src/main/java/com/book/backend/pojo/BooksBorrow.java create mode 100644 src/main/java/com/book/backend/pojo/Chart.java create mode 100644 src/main/java/com/book/backend/pojo/Chat.java create mode 100644 src/main/java/com/book/backend/pojo/Comment.java create mode 100644 src/main/java/com/book/backend/pojo/Notice.java create mode 100644 src/main/java/com/book/backend/pojo/UserInterfaceInfo.java create mode 100644 src/main/java/com/book/backend/pojo/Users.java create mode 100644 src/main/java/com/book/backend/pojo/Violation.java create mode 100644 src/main/java/com/book/backend/pojo/dto/BookDTO.java create mode 100644 src/main/java/com/book/backend/pojo/dto/BookData.java create mode 100644 src/main/java/com/book/backend/pojo/dto/BookRuleDTO.java create mode 100644 src/main/java/com/book/backend/pojo/dto/BooksBorrowDTO.java create mode 100644 src/main/java/com/book/backend/pojo/dto/BorrowData.java create mode 100644 src/main/java/com/book/backend/pojo/dto/BorrowTypeDTO.java create mode 100644 src/main/java/com/book/backend/pojo/dto/CommentDTO.java create mode 100644 src/main/java/com/book/backend/pojo/dto/UsersDTO.java create mode 100644 src/main/java/com/book/backend/pojo/dto/ViolationDTO.java create mode 100644 src/main/java/com/book/backend/pojo/dto/chart/ChartAddRequest.java create mode 100644 src/main/java/com/book/backend/pojo/dto/chart/ChartEditRequest.java create mode 100644 src/main/java/com/book/backend/pojo/dto/chart/ChartUpdateRequest.java create mode 100644 src/main/java/com/book/backend/pojo/dto/chart/GenChartByAiRequest.java create mode 100644 src/main/java/com/book/backend/pojo/dto/chat/ChatRequest.java create mode 100644 src/main/java/com/book/backend/pojo/dto/chat/MessageRequest.java create mode 100644 src/main/java/com/book/backend/pojo/vo/BiResponse.java create mode 100644 src/main/java/com/book/backend/pojo/vo/ChatVo.java create mode 100644 src/main/java/com/book/backend/service/AdminsService.java create mode 100644 src/main/java/com/book/backend/service/AiIntelligentService.java create mode 100644 src/main/java/com/book/backend/service/BookAdminsService.java create mode 100644 src/main/java/com/book/backend/service/BookRuleService.java create mode 100644 src/main/java/com/book/backend/service/BookTypeService.java create mode 100644 src/main/java/com/book/backend/service/BooksBorrowService.java create mode 100644 src/main/java/com/book/backend/service/BooksService.java create mode 100644 src/main/java/com/book/backend/service/ChartService.java create mode 100644 src/main/java/com/book/backend/service/ChatService.java create mode 100644 src/main/java/com/book/backend/service/CommentService.java create mode 100644 src/main/java/com/book/backend/service/NoticeService.java create mode 100644 src/main/java/com/book/backend/service/UserInterfaceInfoService.java create mode 100644 src/main/java/com/book/backend/service/UsersService.java create mode 100644 src/main/java/com/book/backend/service/ViolationService.java create mode 100644 src/main/java/com/book/backend/service/impl/AdminsServiceImpl.java create mode 100644 src/main/java/com/book/backend/service/impl/AiIntelligentServiceImpl.java create mode 100644 src/main/java/com/book/backend/service/impl/BookAdminsServiceImpl.java create mode 100644 src/main/java/com/book/backend/service/impl/BookRuleServiceImpl.java create mode 100644 src/main/java/com/book/backend/service/impl/BookTypeServiceImpl.java create mode 100644 src/main/java/com/book/backend/service/impl/BooksBorrowServiceImpl.java create mode 100644 src/main/java/com/book/backend/service/impl/BooksServiceImpl.java create mode 100644 src/main/java/com/book/backend/service/impl/ChartServiceImpl.java create mode 100644 src/main/java/com/book/backend/service/impl/ChatServiceImpl.java create mode 100644 src/main/java/com/book/backend/service/impl/CommentServiceImpl.java create mode 100644 src/main/java/com/book/backend/service/impl/NoticeServiceImpl.java create mode 100644 src/main/java/com/book/backend/service/impl/UserInterfaceInfoServiceImpl.java create mode 100644 src/main/java/com/book/backend/service/impl/UsersServiceImpl.java create mode 100644 src/main/java/com/book/backend/service/impl/ViolationServiceImpl.java create mode 100644 src/main/java/com/book/backend/utils/BorrowDateUtil.java create mode 100644 src/main/java/com/book/backend/utils/ExcelUtils.java create mode 100644 src/main/java/com/book/backend/utils/JwtKit.java create mode 100644 src/main/java/com/book/backend/utils/NetUtils.java create mode 100644 src/main/java/com/book/backend/utils/NumberUtil.java create mode 100644 src/main/java/com/book/backend/utils/RandomNameUtils.java create mode 100644 src/main/java/com/book/backend/utils/RedisUtil.java create mode 100644 src/main/java/com/book/backend/utils/SpringBootUtil.java create mode 100644 src/main/java/com/book/backend/utils/SqlUtils.java create mode 100644 src/main/java/com/book/backend/utils/Utility.java create mode 100644 src/main/java/com/book/backend/ws/WebSocket.java create mode 100644 src/main/resources/application-dev.yml create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/banner.txt create mode 100644 src/main/resources/mapper/AdminsMapper.xml create mode 100644 src/main/resources/mapper/AiIntelligentMapper.xml create mode 100644 src/main/resources/mapper/BookAdminsMapper.xml create mode 100644 src/main/resources/mapper/BookRuleMapper.xml create mode 100644 src/main/resources/mapper/BookTypeMapper.xml create mode 100644 src/main/resources/mapper/BooksBorrowMapper.xml create mode 100644 src/main/resources/mapper/BooksMapper.xml create mode 100644 src/main/resources/mapper/ChartMapper.xml create mode 100644 src/main/resources/mapper/ChatMapper.xml create mode 100644 src/main/resources/mapper/CommentMapper.xml create mode 100644 src/main/resources/mapper/NoticeMapper.xml create mode 100644 src/main/resources/mapper/UserInterfaceInfoMapper.xml create mode 100644 src/main/resources/mapper/UsersMapper.xml create mode 100644 src/main/resources/mapper/ViolationMapper.xml create mode 100644 src/main/resources/test_excel.xlsx create mode 100644 src/test/java/com/book/backend/AITest.java create mode 100644 src/test/java/com/book/backend/AliAITest.java create mode 100644 src/test/java/com/book/backend/BenchmarkTest.java create mode 100644 src/test/java/com/book/backend/BigModelNew.java create mode 100644 src/test/java/com/book/backend/CrawlerTest.java create mode 100644 src/test/java/com/book/backend/OpenAPITest.java create mode 100644 src/test/java/com/book/backend/RedisTest.java create mode 100644 src/test/java/com/book/backend/SparkAITest.java create mode 100644 src/test/java/com/book/backend/SparkClientTest.java create mode 100644 src/test/java/com/book/backend/VueBookBackendApplicationTests.java create mode 100644 src/test/java/com/book/backend/VueBookBackendUserTest.java create mode 100644 src/test/java/com/book/backend/domain/BookData.java create mode 100644 src/test/java/com/book/backend/domain/DemoData.java create mode 100644 src/test/java/com/book/backend/testutils/EasyExcelTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2afe33a --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +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/ +src/main/resources/application-prod.yml +/jmh-result.json diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..c1dd12f17644411d6e840bd5a10c6ecda0175f18 GIT binary patch literal 58727 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%0ANsA|A3-jj|r`KAo%oTajX3>^E` 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 YM{}PByk10KD_nuQ4E7Du?}+~TKh4V)`~Uy| literal 0 HcmV?d00001 diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..b74bf7f --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f5ddf7 --- /dev/null +++ b/README.md @@ -0,0 +1,842 @@ +# 智能图书馆开源文档 + +>作者:[程序员小白条](https://luoye6.github.io/) +> +>[Gitee 主页](https://gitee.com/falle22222n-leaves) +> +>[GitHub 主页](https://github.com/luoye6) + +Language:**[English](README_en.md)**| **[中文](README.md).** + +## ☀️新手必读 + ++ 本项目拥有完整的API后台接口文档(文尾)(重点⭐) ++ 未经本人允许擅自将本项目作于商用、竞赛、贩卖源码、毕设擅自改动作者等违法行为,将依法追究法律责任,后果自负,项目已申请软件著作权。 ++ 拥有典型的大学生思维、狂妄、自大的人请不要加我,谢谢合作! ++ 如果项目对您有所帮助,可以Star⭐一下,受到鼓励的我会继续加油。 ++ [项目在线演示地址](https://www.xiaobaitiao.top) ++ [项目前端地址](https://gitee.com/falle22222n-leaves/vue_-book-manage-system) ++ [项目后端地址](https://gitee.com/falle22222n-leaves/vue_-book-manage-system_backend) ++ [项目部署视频](https://www.bilibili.com/video/BV1Zh4y1z7QE/?spm_id_from=333.999.0.0) + +[![star](https://gitee.com/falle22222n-leaves/vue_-book-manage-system/badge/star.svg?theme=dark)](https://gitee.com/falle22222n-leaves/vue_-book-manage-system) [![gitee](https://badgen.net/badge/gitee/falle22222n-leaves/red)](https://gitee.com/falle22222n-leaves) [![github](https://badgen.net/badge/github/github?icon)](https://github.com/luoye6) + +## 黑马程序员推荐作品(doge) +![](https://pic.yupi.icu/5563/202503251541861.png) + +## ☀️个人介绍 + +![](https://pic.yupi.icu/5563/202403021406388.png) + +![](https://pic.yupi.icu/5563/202403021406360.png) + +## 🐼新项目上线(开新坑) +智能 AI 旅游推荐平台上线!欢迎收藏和 Fork,技术栈 Vue3+SpringBoot2+TypeScript,UI 设计更加美观,且可以自定义主题颜色,支持讯飞星火 AI 大模型和可添加协同过滤算法。 ++ [项目在线演示地址](https://www.xiaobaitiao.icu) ++ [项目前端地址](https://gitee.com/falle22222n-leaves/vue3_tourism_frontend) ++ [项目后端地址](https://gitee.com/falle22222n-leaves/vue3_tourism_backend) + +## ☀️项目介绍 + +**AI 智能图书馆**(AI Intelligent Library)是一个利用 AI 模型和数据分析对用户所喜欢的图书进行精准推荐的系统,并且提供了 AIGC 的在线生成借阅量分析的 BI 图表功能,能够起到一个数据分析师的作用。其主要有三大使用者:用户(借阅人)、图书管理员、系统管理员。 + +> Ps:如果你想要简易和新颖,那么这个项目将会是不错的选择~ + +![](https://pic.yupi.icu/5563/202403041924533.png) + +![](https://pic.yupi.icu/5563/202403041924237.png) + +## ☀️功能和特性 + +### 用户功能 + +1)图书查询功能:分页构造器缓解数据过大压力,后端可设置请求数防止爬虫请求数过大,服务器负载过大。模糊查询进行字段搜索。表格均**可导出 PDF 和 EXCEL**。 + +2)读者规则功能:查询现有的借阅规则,借阅规则包括:借阅编号,可借阅图书数量,可借阅天数,可借阅图书馆,过期扣费/天。 + +3)查看公告: 可以查询图书管理员发布的公告列表,**文字滑动效果**。 + +4)个人信息: 可以查看个人的借阅证编号,借阅证姓名,规则编号,状态,可以修改个人账户的密码。 + +5)借阅信息: 可以查看自身借阅过的图书记录和归还情况。 + +6)违章信息: 可以查询自身归还的图书是否有违章信息。 + +7)读者留言: 实现留言功能并以**弹幕形式**显示。 + +8)**智能推荐**用户输入自己的偏好,AI 根据数据库书籍列表和用户偏好,给用户推荐书籍。 + +### 图书管理员功能 + +1)借阅图书: 图书管理员输入借阅证号(用户)和要借的图书编号和当前的时间,点击借阅。 + +2)归还图书: 输入图书编号查看图书是否逾期,并且可以设置违规信息,然后选择是否归还图书。 + +3)借书报表: 用于查询已经借阅并归还的书籍列表,同样使用分页构造器和模糊查询字段,显示借阅证编号,图书编号,借阅日期,截止日期,归还日期,违章信息,处理人。 + +4)还书报表: 用于查询已经借阅但是还未归还的书籍列表,显示借阅证编号,图书编号,借阅日期,截止日期。 + +5)发布公告: 可以查询当前发布的公告列表,并进行删除,修改,增加功能,分页构造器用于缓解数据量大的情况。 + +### 系统管理员功能 + +1)书籍管理: 可以查询当前的所有图书,显示图书编号,图书昵称,作者,图书馆,分类,位置,状态,描述。可以进行添加,修改,删除图书。利用分页构造器实现批量查询。利用模糊查询实现图书搜索功能。**利用插件实现 PDF 和 EXCEL 导出**。 + +2)书籍类型: 显示查询当前的所有图书类型,可以进行添加,修改,删除图书类型,利用分页构造器实现批量查询,缓解数据压力。 + +3)借阅证管理: 可以查询当前的所有借阅证列表,也就是用户数量,可以进行添加,修改,删除操作。同样实现分页。 + +4)借阅信息查询: 可以查询当前已经完成借阅和归还的记录,显示借阅证号,书籍编号,借阅日期,截止日期,归还日期,违章信息,处理人。分页功能,PDF 和 EXCEL 导出。 + +5)借阅规则管理: 可以查询当前所有的借阅规则,显示限制借阅天数,限制本数,限制图书馆,逾期费用,可以进行添加、删除、修改操作。 + +6)图书管理员管理: 显示当前的图书管理员列表,显示账号,姓名,邮箱,可以进行添加、删除、修改操作。 + +7)系统管理: 可以查询一个月内的借阅量,以一周为时间间隔,计算借阅量,**用 Echarts 实现各种图表的展示**。 + +8)系统分析:可以上传某个时间段的借阅量和日期,并且输入分析目标和想要生成的图表类型,等待一段时间后,**AI 将会给出分析结论和可视化图表**。 + +### 特性(亮点) + +1)本项目采用前后端分离的模式,前端构建页面,后端作数据接口,前端调用后端数据接口得到数据,重新渲染页面。 + +2)前端在 Authorization 字段提供 Token 令牌,API 认证使用 Token 认证,使用 HTTP Status Code 表示状态,数据返回格式使用 JSON。 + +3)后端已开启 CORS 跨域支持,采用权限拦截器进行权限校验,并检查登录情况。 + +4)添加全局异常处理机制,捕获异常,增强系统健壮性。 + +5)前端用 Echarts 可视化库实现了图书借阅的分析图标(折线图、饼图),并通过 Loading 配置提高加载体验。 + +6)留言组件采用弹幕形式,贴合用户的喜好。 + +7)引入 knife4j 依赖,使用 Swagger + Knife4j 自动生成 OpenAPI 规范的接口文档,前端可以在此基础上使用插件自动生成接口请求代码,降低前后端协作成本 + +8)使用 ElementUI 组件库进行前端界面搭建,快速实现页面生成,并实现了前后端统一权限管理,多环境切换等能力。 + +9)基于 MyBatis Plus 框架的 QueryWrapper 实现对 MySQL 数据库的灵活查询,并配合 MyBatisX 插件自动生成后端 CRUD 基础代码,减少重复工作。 + +10)前端路由懒加载、CDN 静态资源缓存优化、图片懒加载效果。 + +## ☀️运行方式 + +### 2 分钟快速上手使用项目 + +1)找到 SpringBoot 启动类,点击运行 + +![](https://pic.yupi.icu/5563/202403041925113.png) + +2)打开 Knife4J 注册用户,或者可以直接找我拿数据库模拟数据(简易)。 + +![](https://pic.yupi.icu/5563/202403041925196.png) + +![](https://pic.yupi.icu/5563/202403041925244.png) + +3)前端输入表单内容后点击登录即可成功,开始愉快使用功能~ + +![](https://pic.yupi.icu/5563/202403041925792.png) + +![](https://pic.yupi.icu/5563/202403041925648.png) + +## ☀️部署方式 + +### 前置条件 + +**前端** + +软件:Vscode 或者 Webstorm(推荐) + +环境:Node 版本 16 或者 18(推荐) **注:千万别选 18 以上的版本!** + +**后端** + +软件:Eclipse 或者 IDEA(推荐) + +环境:MySQL 5.7 或者 8.0(推荐)Redis(可选) + +### 前端部署 + +1)点击克隆/下载项目,会使用 Git 进行版本控制的,推荐 Git Clone,不会的小伙伴可以选择下载一个 Zip 压缩包,然后解压到自己电脑的 D 盘,推荐直接 Star,后续直接向我拿数据库模拟文件和 API 接口文档。 + +![](https://pic.yupi.icu/5563/202403041926975.png) + +2)利用 Vscode 或者 Webstorm 打开前端页面,配置 Configuration。配置 Node 环境和包管理工具即可,我这边选择的包管理工具是 Npm,其他包管理工具如:Yarn、Cnpm、Pnpm 皆可。 **注:注意更改 Npm 的镜像地址为淘宝的新镜像地址,否则会出现 Npm Install 一直卡进度条的情况。** + +3)直接点击 dev 的运行,或者打开控制台,输入 npm run serve 即可成功启动前端项目。 + +```shell +npm config set registry https://registry.npmmirror.com/ +``` + +![](https://pic.yupi.icu/5563/202403041926892.png) + +![](https://pic.yupi.icu/5563/202403041926931.png) + +![](https://pic.yupi.icu/5563/202403041926639.png) + +4)将图片链接进行自定义切换,可以切换为你自己的图床的图片链接,比如七牛云、GitHub 等,也可以寻找在线图片,复制百度文库图片链接(多试几次,有些图片有防盗链)。**更换背景后,可以看到右下角的权限切换小图标。** + +![](https://pic.yupi.icu/5563/202403041926849.png) + +![](https://pic.yupi.icu/5563/202403041926100.png) + + + +### 后端部署 + +1)点击克隆/下载项目,会使用 Git 进行版本控制的,推荐 Git Clone,不会的小伙伴可以选择下载一个 Zip 压缩包,然后解压到自己电脑的 D 盘,推荐直接 Star,后续直接向我拿数据库模拟文件和 API 接口文档。 + +![](https://pic.yupi.icu/5563/202403041926093.png) + +2)领取数据库模拟文件后,利用 Navicat 或者 SQLYog 等软件导入数据库文件,记得先建立一个名为 bms_boot 的数据库,然后右键点击运行 SQL 文件即可,运行成功,无报错后,重新打开数据库,检查是否有数据,如果有数据,表明导入成功。 + +![](https://pic.yupi.icu/5563/202403041926256.png) + +![](https://pic.yupi.icu/5563/202403041926292.png) + +3)用 IDEA 打开后端项目,找到 application-dev.yml 文件,修改其中的 MySQL 配置,保证用户名和密码正确,注:密码不能以数字 0 开头。 + +![](https://pic.yupi.icu/5563/202403041926672.png) + +4)导入 Maven 依赖,注意看自己的 Maven 版本是否正确,建议选择跟我一样的,3.8以上的版本,发现依赖导入很慢,是因为没有配置国内镜像,默认连接的是国外服务器,因此阿里云镜像配置可以看这篇博客。[CSDN Maven 配置教程](https://blog.csdn.net/lianghecai52171314/article/details/102625184?ops_request_misc=&request_id=&biz_id=102&utm_term=Maven) + +![](https://pic.yupi.icu/5563/202403041926747.png) + +5)找到 SpringBoot 启动类,我建议用 Debug 模式启动项目,更好排查错误。 + +![](https://pic.yupi.icu/5563/202403041926037.png) + +6)如果遇到错误,大概率可能是 JDK 版本问题,我项目用的是 JDK 8,建议选择与我相同版本。 + +![](https://pic.yupi.icu/5563/202403041926752.png) + +![](https://pic.yupi.icu/5563/202403041926887.png) + +7)成功启动项目效果展示如下 + +![](https://pic.yupi.icu/5563/202403041926993.png) + +### 前后端联调 + +1)如果需要修改端口和前缀(比如/api),需要同时修改前端和后端。 + +![](https://pic.yupi.icu/5563/202403041926975.png) + +![](https://pic.yupi.icu/5563/202403041926787.png) + +## ☀️技术选型 + +### 前端 + +| **技术** | **作用** | **版本** | +| ---------------------------- | ------------------------------------------------------------ | ---------------------------------------------------- | +| Vue | 提供前端交互 | 2.6.14 | +| Vue-Router | 路由式编程导航 | 3.5.1 | +| Element-UI | 模块组件库,绘制界面 | 2.4.5 | +| Axios | 发送ajax请求给后端请求数据 | 1.2.1 | +| core-js | 兼容性更强,浏览器适配 | 3.8.3 | +| swiper | 轮播图插件(快速实现) | 3.4.2 | +| vue-baberrage | vue弹幕插件(实现留言功能) | 3.2.4 | +| vue-json-excel | 表格导出Excel | 0.3.0 | +| html2canvas+jspdf | 表格导出PDF | 1.4.1 2.5.1 | +| node-polyfill-webpack-plugin | webpack5中移除了nodejs核心模块的polyfill自动引入 | 2.0.1 | +| default-passive-events | **Chrome** 增加了新的事件捕获机制 **Passive Event Listeners**(被动事件侦听器) | 让页面滑动更加流畅,主要用于提升移动端滑动行为的性能 | +| nprogress | 发送请求显示进度条(人机交互友好) | 0.2.0 | +| echarts | 数据转图标的好工具(功能强大) | 5.4.1 | +| less lessloader | 方便样式开发 | 4.1.3 11.1.0 | + +### 后端 + +| **技术及版本** | **作用** | **版本** | +| ------------------------------------ | ------------------------------------------------------------ | --------------------------------- | +| SpringBoot | 应用开发框架 | 2.7.8 | +| JDK | Java 开发包 | 1.8 | +| MySQL | 提供后端数据库 | 8.0.23 | +| MyBatisPlus | 提供连接数据库和快捷的增删改查 | 3.5.1 | +| SpringBoot-Configuration-processor | 配置处理器 定义的类和配置文件绑定一般没有提示,因此可以添加配置处理器,产生相对应的提示. | | +| SpringBoot-Starter-Web | 后端集成Tomcat MVC | 用于和前端连接 | +| SpringBoot-starter-test | Junit4单元测试前端在调用接口前,后端先调用单元测试进行增删改查,注意Junit4和5的问题,注解@RunWith是否添加 | | +| Lombok | 实体类方法的快速生成 简化代码 | | +| mybatis-plus-generator | 代码生成器 | 3.5.1 | +| MyBatisX | MyBatisPlus插件直接生成mapper,实体类,service | | +| jjwt | token工具包 | 0.9.0 | +| fastjson | 阿里巴巴的 JSON 工具类 | 1.2.83 | +| hutool | hutool工具包(简化开发工具类) | [文档](https://hutool.cn/docs/#/) | +| knife4j-openapi2-spring-boot-starter | Knife4j 在线接口文档测试工具 | 4.0.0 | +| gson | 谷歌的 JSON 工具类 | 2.8.5 | +| Java-WebSocket | 讯飞星火 AI 配置 | 1.3.8 | +| okhttp | 讯飞星火 AI 配置 | 4.10.0 | +| okio | 讯飞星火 AI 配置 | 2.10.0 | +| jsoup | 简易爬虫工具 | 1.15.3 | +| guava | 谷歌工具类 | 30.1-jre | +| spring-boot-starter-data-redis | Redis 的 Starter | | +| broadscope-bailian-sdk-java | 阿里云 AI 模型 | 1.1.7 | +| spring-boot-starter-websocket | WebSocket 的 Starter | | + +## ☀️架构 + +![](https://pic.yupi.icu/5563/202403061541028.png) + +## ☀️核心设计 + +### 智能推荐功能 + +1)用户输入自己的图书偏爱信息。 + +2)前端发送 Axios 请求。 + +3)后端先判断文本是否违法(为空或者文本字数过长)。 + +4)查看接口是否存在。 + +5)查看 AI 接口调用次数是否充足。 + +6)GuavaRateLimiter 进行单体限流,判断请求次数是否超出正常业务频次。 + +7)给 AI 模型人工预设,并且查询数据库中的书籍列表进行拼接。 + +8)查询 AI 模型与该用户最近的五条历史记录,用于上下文关联。 + +9)FutureTask 同步调用获取 AI 结果,并设置超时时间(超时抛出异常) + +10)获取 AI 模型推荐信息后进行持久化,并且减少接口调用次数(判断是否成功) + +11)返回处理好的 AI 推荐信息给前端,并设置响应状态码为 200 即可。 + +### 智能分析功能 + +1)用户输入分析目标、图标名称、选择图标类型、上传 Excel 文件,点击提交,发送 Axios 请求至后端。 + +2)校验文件是否为空、名称是否过长、文件大小检验、文件后缀校验 + +3)获取管理员 ID,从接口信息表查询管理员 ID 拥有的接口,接口判空。 + +4)判断 AI 接口调用次数是否足够 + +5)GuavaRateLimiter 进行单体限流,判断请求次数是否超出正常业务频次。 + +6)构造 AI 模型的提示词和角色 + +7)构造用户输入,拼接用户输入信息,并用工具类将 Excel 转为 CSV 字符串数据。 + +8)利用讯飞星火 AI 模型,传入调用者 ID 和输入参数,利用 FutureTask 同步获取,并设置超时时间(超时抛出异常) + +9)对 AI 生成结果进行判断,格式错误就返回前端错误信息,并提示重新调用(后续考虑 RabbitMQ 进行重试和补偿机制) + +10)将 AI 生成结果持久化到数据库,并更新接口调用次数(判断是否成功),动态给前端返回图标和数据结论。 + +## ☀️学完这个项目你能得到什么 + +1)简单地调用 AI 模型(讯飞星火 | 阿里百炼)获取自定义文本内容。 + +2)简单的 JWT 权限校验 ,利用后端拦截器进行登录校验。 + +3)上传 Excel 文件,Excel 文件转换为 CSV 数据,AIGC 在线生成可视化图表。 + +4)Jousp 批量爬取图书列表,可结合 SpringSchedule 定时任务执行。 + +5)简单的增删改查系统,前后端是如何联调协作的。 + +6)前端路由懒加载、CDN 静态资源缓存优化、图片懒加载是如何实现的 + +7)利用 Lodash 进行节流控制,尽量降低无效的恶意刷留言情况。 + +8)利用自定义线程池和 FutureTask 进行超时请求处理。 + +9)利用Google 的 GuavaRateLimiter 进行单体限流控制。 + +10)定时任务结合 Redis 做一个缓存预热,加快查询效率,提高用户体验。 + +## ☀️项目简介 + ++ 主要使用Vue2和SpringBoot2实现 ++ 项目权限控制分别为:用户借阅,图书管理员,系统管理员 ++ 开发工具:IDEA2022.1.3(真不推荐用eclipse开发,IDEA项目可以导出为eclipse项目,二者不影响,但需要自己学教程) ++ [IDEA->Eclipse](https://blog.csdn.net/HD202202/article/details/128076400) ++ [Eclipse->IDEA](https://blog.csdn.net/q20010619/article/details/125096051) + ++ 学校老师硬性要求软件的话,还是按要求来。可以先问一下是否可以选择其他软件开发。 ++ 用户账号密码: 相思断红肠 123456 ++ 图书管理员账号密码: admin 123456 ++ 系统管理员账号密码: root 123456 ++ [前端样式参考](https://gitee.com/mingyuefusu/tushuguanlixitong) 感谢原作者**明月复苏** + ++ 遇到交互功能错误,或者页面无法打开,请用开发者工具F12查看请求和响应状态码情况,当然可能小白不懂,那也没关系,可以加我**QQ:909088445**。白天上课,晚上有空才能回答,感谢体谅!⭐⭐⭐ + +## ☀️项目详细介绍(亮点) + ++ 本项目采用前后端分离的模式,前端构建页面,后端作数据接口,前端调用后端数据接口得到数据,重新渲染页面。 ++ 后端已开启 CORS 跨域支持 ++ API 认证使用 Token 认证 ++ 前端在 Authorization 字段提供 Token 令牌 ++ 使用 HTTP Status Code 表示状态 ++ 数据返回格式使用 JSON ++ 后端采用权限拦截器进行权限校验,并检查登录情况 ++ 添加全局异常处理机制,捕获异常,增强系统健壮性 ++ 前端用 Echarts 可视化库实现了图书借阅的分析图标(折线图、饼图),并通过 Loading 配置提高加载体验。 ++ 留言组件采用弹幕形式,贴合用户的喜好。 ++ 引入 knife4j 依赖,使用 Swagger + Knife4j 自动生成 OpenAPI 规范的接口文档,前端可以在此基础上使用插件自动生成接口请求代码,降低前后端协作成本 ++ 使用 ElementUI 组件库进行前端界面搭建,快速实现页面生成,并实现了前后端统一权限管理,多环境切换等能力。 ++ 基于 MyBatis Plus 框架的 QueryWrapper 实现对 MySQL 数据库的灵活查询,并配合 MyBatisX 插件自动生成后端 CRUD 基础代码,减少重复工作。 ++ 前端路由懒加载、CDN 静态资源缓存优化、图片懒加载效果。 + +### ⭐用户模块功能介绍 + +![](https://pic.yupi.icu/5563/202403021406815.png) + ++ 图书查询功能:分页构造器缓解数据过大压力,后端可设置请求数防止爬虫请求数过大,服务器负载过大。模糊查询进行字段搜索。表格均可导出PDF和EXCEL。 ++ 读者规则功能:查询现有的借阅规则,借阅规则包括:借阅编号,可借阅图书数量,可借阅天数,可借阅图书馆,过期扣费/天。 ++ 查看公告: 可以查询图书管理员发布的公告列表,文字滑动效果。 + ++ 个人信息: 可以查看个人的借阅证编号,借阅证姓名,规则编号,状态,可以修改个人账户的密码。 + ++ 借阅信息: 可以查看自身借阅过的图书记录和归还情况。 ++ 违章信息: 可以查询自身归还的图书是否有违章信息。 ++ 读者留言: 实现留言功能并以弹幕形式显示。 + +### ⭐图书管理员模块功能介绍 + +![](https://pic.yupi.icu/5563/202403021406227.png) + ++ 借阅图书: 图书管理员输入借阅证号(用户)和要借的图书编号和当前的时间,点击借阅。 ++ 归还图书: 输入图书编号查看图书是否逾期,并且可以设置违规信息,然后选择是否归还图书 ++ 借书报表: 用于查询已经借阅并归还的书籍列表,同样使用分页构造器和模糊查询字段,显示借阅证编号,图书编号,借阅日期,截止日期,归还日期,违章信息,处理人。 ++ 还书报表: 用于查询已经借阅但是还未归还的书籍列表,显示借阅证编号,图书编号,借阅日期,截止日期。 + ++ 发布公告: 可以查询当前发布的公告列表,并进行删除,修改,增加功能,分页构造器用于缓解数据量大的情况。 + +### ⭐系统管理员模块功能介绍 + +![](https://pic.yupi.icu/5563/202403021406443.png) + ++ 书籍管理: 可以查询当前的所有图书,显示图书编号,图书昵称,作者,图书馆,分类,位置,状态,描述。可以进行添加,修改,删除图书。利用分页构造器实现批量查询。利用模糊查询实现图书搜索功能。利用插件实现PDF和EXCEL导出。 ++ 书籍类型: 显示查询当前的所有图书类型,可以进行添加,修改,删除图书类型,利用分页构造器实现批量查询,缓解数据压力。 ++ 借阅证管理: 可以查询当前的所有借阅证列表,也就是用户数量,可以进行添加,修改,删除操作。同样实现分页。 ++ 借阅信息查询: 可以查询当前已经完成借阅和归还的记录,显示借阅证号,书籍编号,借阅日期,截止日期,归还日期,违章信息,处理人。分页功能,PDF和EXCEL导出。 ++ 借阅规则管理: 可以查询当前所有的借阅规则,显示限制借阅天数,限制本数,限制图书馆,逾期费用,可以进行添加、删除、修改操作。 ++ 图书管理员管理: 显示当前的图书管理员列表,显示账号,姓名,邮箱,可以进行添加、删除、修改操作。 ++ 系统管理: 可以查询一个月内的借阅量,以一周为时间间隔,计算借阅量,用Echarts实现折线图的展示。 + +## ☀️数据库表设计 + +### t_users表 + +| 列名 | 数据类型以及长度 | 备注 | +| ----------- | ---------------- | ------------------------------------------------- | +| user_id | int(11) | 主键 非空 自增 用户表的唯一标识 | +| username | varchar(32) | 用户名 非空 | +| password | varchar(32) | 密码(MD5加密) 非空 | +| card_name | varchar(10) | 真实姓名 非空 | +| card_number | Bigint(11) | 借阅证编号 固定 11位随机生成 非空(后文都改BigInt) | +| rule_number | int(11) | 规则编号 可以自定义 也就是权限功能 | +| status | int(1) | 1表示可用 0表示禁用 | +| create_time | datetime | 创建时间 Java注解 JsonFormatter | +| update_time | datetime | 更新时间 Java注解 JsonFormatter | + +### t_admins表 + +| 列名 | 数据类型以及长度 | 备注 | +| ----------- | ---------------- | --------------------------------- | +| admin_id | int(11) | 主键 非空 自增 管理员表的唯一标识 | +| username | varchar(32) | 用户名 非空 | +| password | varchar(32) | 密码(MD5加密) 非空 | +| admin_name | varchar(10) | 管理员真实姓名 非空 | +| status | int(1) | 1表示可用 0表示禁用 | +| create_time | datetime | 创建时间 Java注解 JsonFormatter | +| update_time | datetime | 更新时间 Java注解 JsonFormatter | + +### t_book_admins表 + +| 列名 | 数据类型以及长度 | 备注 | +| --------------- | ---------------- | ------------------------------- | +| book_admin_id | int(11) | 主键 非空 自增 管理表的唯一标识 | +| username | varchar(32) | 用户名 非空 | +| password | varchar(32) | 密码(MD5加密)非空 | +| book_admin_name | varchar(10) | 图书管理员真实姓名 非空 | +| status | int(1) | 1表示可用 0表示禁用 | +| email | varchar(255) | 电子邮箱 | +| create_time | datetime | 创建时间 Java注解 JsonFormatter | +| update_time | datetime | 更新时间 Java注解 JsonFormatter | + +### t_books表 + +| 列名 | 数据类型以及长度 | 备注 | +| ---------------- | ---------------- | ------------------------------- | +| book_id | int(11) | 主键 自增 非空 图书表的唯一标识 | +| book_number | int(11) | 图书编号 非空 图书的唯一标识 | +| book_name | varchar(32) | 图书名称 非空 | +| book_author | varchar(32) | 图书作者 非空 | +| book_library | varchar(32) | 图书所在图书馆的名称 非空 | +| book_type | varchar(32) | 图书类别 非空 | +| book_location | varchar(32) | 图书位置 非空 | +| book_status | varchar(32) | 图书状态(未借出/已借出) | +| book_description | varchar(100) | 图书描述 | +| create_time | datetime | 创建时间 Java注解 JsonFormatter | +| update_time | datetime | 更新时间 Java注解 JsonFormatter | + +### t_books_borrow表 + +| 列名 | 数据类型以及长度 | 备注 | +| ----------- | ---------------- | ------------------------------------------------------------ | +| borrow_id | int(11) | 主键 自增 非空 借阅表的唯一标识 | +| card_number | int(11) | 借阅证编号 固定 11位随机生成 非空 用户与图书关联的的唯一标识 | +| book_number | int(11) | 图书编号 非空 图书的唯一标识 | +| borrow_date | datetime | 借阅日期 Java注解 JsonFormatter | +| close_date | datetime | 截止日期 Java注解 JsonFormatter | +| return_date | datetime | 归还日期 Java注解 JsonFormatter | +| create_time | datetime | 创建时间 Java注解 JsonFormatter | +| update_time | datetime | 更新时间 Java注解 JsonFormatter | + +### t_notice表 + +| 列名 | 数据类型以及长度 | 备注 | +| --------------- | ---------------- | ----------------------------------- | +| notice_id | int(11) | 主键 非空 自增 公告表记录的唯一标识 | +| notice_title | varchar(32) | 公告的题目 非空 | +| notice_content | varchar(255) | 公告的内容 非空 | +| notice_admin_id | int(11) | 发布公告的管理员的id | +| create_time | datetime | 创建时间 Java注解 JsonFormatter | +| update_time | datetime | 更新时间 Java注解 JsonFormatter | + +### t_violation表 + +| 列名 | 数据类型以及长度 | 备注 | +| ------------------ | ---------------- | ----------------------------------- | +| violation_id | int(11) | 主键 非空 自增 违章表记录的唯一标识 | +| card_number | int(11) | 借阅证编号 固定 11位随机生成 非空 | +| book_number | int(11) | 图书编号 非空 图书的唯一标识 | +| borrow_date | datetime | 借阅日期 Java注解 JsonFormatter | +| close_date | datetime | 截止日期 Java注解 JsonFormatter | +| return_date | datetime | 归还日期 Java注解 JsonFormatter | +| violation_message | varchar(100) | 违章信息 非空 | +| violation_admin_id | int(11) | 违章信息管理员的id | +| create_time | datetime | 创建时间 Java注解 JsonFormatter | +| update_time | datetime | 更新时间 Java注解 JsonFormatter | + +### t_comment表 + +| 列名 | 数据类型以及长度 | 备注 | +| --------------------- | ---------------- | ----------------------------------- | +| comment_id | int(11) | 主键 非空 自增 留言表记录的唯一标识 | +| comment_avatar | varchar(255) | 留言的头像 | +| comment_barrage_style | varchar(32) | 弹幕的高度 | +| comment_message | varchar(255) | 留言的内容 | +| comment_time | int(11) | 留言的时间(控制速度) | +| create_time | datetime | 创建时间 Java注解 JsonFormatter | +| update_time | datetime | 更新时间 Java注解 JsonFormatter | + +### t_book_rule表 + +| 列名 | 数据类型以及长度 | 备注 | +| ------------------ | ---------------- | ------------------------------------- | +| rule_id | int(11) | 主键 非空 自增 借阅规则记录的唯一标识 | +| book_rule_id | int(11) | 借阅规则编号 非空 | +| book_days | int(11) | 借阅天数 非空 | +| book_limit_number | int(11) | 限制借阅的本数 非空 | +| book_limit_library | varchar(255) | 限制的图书馆 非空 | +| book_overdue_fee | double | 图书借阅逾期后每天费用 非空 | +| create_time | datetime | 创建时间 Java注解 JsonFormatter | +| update_time | datetime | 更新时间 Java注解 JsonFormatter | + +### t_book_type表 + +| 列名 | 数据类型以及长度 | 备注 | +| ------------ | ---------------- | ------------------------------------- | +| type_id | int(11) | 主键 非空 自增 图书类别记录的唯一标识 | +| type_name | varchar(32) | 借阅类别的昵称 非空 | +| type_content | varchar(255) | 借阅类别的描述 非空 | +| create_time | datetime | 创建时间 Java注解 JsonFormatter | +| update_time | datetime | 更新时间 Java注解 JsonFormatter | + +## 🐼功能演示图 + +### 用户模块功能图 + +**首页轮播图演示** + +![](https://pic.yupi.icu/5563/202403021406581.png) + +**图书查询演示** + +![](https://pic.yupi.icu/5563/202403021406053.png) + +**读者规则演示** + +![](https://pic.yupi.icu/5563/202403021406571.png) + +**查看公告演示** + +![](https://pic.yupi.icu/5563/202403021406776.png) + +**个人信息演示** + +![](https://pic.yupi.icu/5563/202403021406779.png) + +**借阅信息演示** + +![](https://pic.yupi.icu/5563/202403021406890.png) + +**违章信息演示** + +![](https://pic.yupi.icu/5563/202403021406091.png) + +**读者留言演示** + +![](https://pic.yupi.icu/5563/202403021406261.png) + +**智能推荐演示** + +![](https://pic.yupi.icu/5563/202403021406234.png) + +### 图书管理员功能图 + +**借阅图书演示** + +![](https://pic.yupi.icu/5563/202403021406213.png) + +**归还图书演示** + +![](https://pic.yupi.icu/5563/202403021406604.png) + +**借书报表演示** + +![](https://pic.yupi.icu/5563/202403021406590.png) + +**还书报表演示** + +![](https://pic.yupi.icu/5563/202403021406562.png) + +**发布公告演示** + +![](https://pic.yupi.icu/5563/202403021406616.png) + +### 系统管理员功能图 + ++ 由于篇幅受限,系统功能展示主要功能。 + +**系统管理演示** + +![](https://pic.yupi.icu/5563/202403021406081.png) + +![](https://pic.yupi.icu/5563/202403021406169.png) + +**智能分析演示** + +![](https://pic.yupi.icu/5563/202403021406245.png) + +## 🐼部署项目 + +![](https://pic.yupi.icu/5563/202403021406282.png) + ++ 可以下载ZIP压缩包或者使用克隆(Git clone) ++ 复制http或者ssh的链接(github建议ssh,gittee都可以) ++ 在D盘新建一个文件夹,点击进入该文件夹,右键Git Bash Here + +![](https://pic.yupi.icu/5563/202403021406715.png) + ++ 还没有下载Git或者不会Git的建议先看基础教程(30分钟左右) + ++ 输入git init 初始化git项目 然后出现一个.git文件夹 ++ 输入git remote add origin xxxxxx(xxx为刚刚复制的http或者ssh链接) + ++ 输入git pull origin master 从远程代码托管仓库拉取代码 ++ 成功拉取项目(前端后端都是如此) ++ 前端项目注意依赖下载使用npm install 或者 yarn install (Vscode或者Webstorm) ++ 后端项目注意maven依赖下载(IDEA(推荐)或者Ecplise) ++ 前端npm 镜像源建议淘宝镜像源,后端maven镜像源推荐阿里云镜像源(非必选,但更换后下载快速) + +## 🐼部署项目问题 + +⭐ + ++ 乱码问题 项目采用的UFT-8 ++ 一般出现乱码就是UTF-8和GBK二者相反 ++ 请百度IDEA乱码和Eclipse乱码问题(描述清楚即可) + +⭐ + ++ 点击交互按钮,没有发生反应。 ++ 很明显,请求失败,浏览器打开开发者工具,Edge浏览器直接ctrl+shift+i,其他浏览器按F12 ++ 查看红色的请求和响应状态码问题 + +⭐ + ++ 先阅读文档再进行问题的查询或者提问 ++ 提问有技巧,模糊的发言,让高级架构师找BUG也无从下手 + +⭐ + ++ **QQ:909088445** ++ 一般晚上在线,建议先自己寻找问题!!! ++ 开源免费, 定制化和调试项目付费。 + +## 🐼需求分析和设计 + +需求分析和设计文档,有(**付费**)需求的可以加 QQ:909088445,适合走毕设和课设的小伙伴,图省事的可以找我。 + +![](https://pic.yupi.icu/5563/202403061545778.png) + +## 🐼项目API接口文档 + ++ 接口文档篇幅过大 ++ 本来想完全采用RESTFUL风格,做到一半忘记了 ++ 看清楚文档的基准地址 ++ 要API后端接口文档详细内容和数据库结构(**由于服务器、域名、 AI 模型的成本问题,现数据库文件已收费**)+内容一起的,将前后端**star**⭐的截图加我QQ:**909088445**发我即可领取~感谢支持 + +#### **数据库领取截图示例(Gitee&GitHub):** + +![](https://pic.yupi.icu/5563/202403021406801.png) + +![](https://pic.yupi.icu/5563/202403021406821.png) + +![](https://pic.yupi.icu/5563/202403092029471.png) + +![](https://pic.yupi.icu/5563/202403092029399.png) + +## 🐼(重点)远程部署和项目讲解服务 + +远程部署服务需自己先下载向日葵远程控制软件,然后加 WX 或者 QQ 即可(**付费服务**),远程部署用于给完全不懂的小白,项目讲解服务用于**课设、实训、毕设语音答辩服务**,想减省时间,提高通过率,直接加我即可,可以**定制背景图片和整体的样式功能**,**降重服务**也可私我!新增定制协同过滤算法和数据可视化功能!有需求的小伙伴,请先编写一份具体的需求文档! + + +## 🐷其他 + ++ 个人博客地址: https://luoye6.github.io/ ++ 个人博客采用Hexo+Github托管 ++ 采用butterfly主题可以实现定制化 ++ 推荐有空闲时间的,可以花1-2天搭建个人博客用于记录笔记。 + +## ☕请我喝咖啡 + +如果本项目对您有所帮助,不妨请作者我喝杯咖啡 :) + +
+ +## **版本迭代** + +### 2023-3-19 + +1.引入knife4j依赖,使用 Swagger + Knife4j 自动生成 OpenAPI 规范的接口文档,前端可以在此基础上使用插件自动生成接口请求代码,降低前后端协作成本。 + +2.引入jsoup依赖可以自定义添加爬虫功能,可以批量添加图书并且是比较真实的数据。 + +3.添加事务管理器,可以进行用@Transactional指定异常类型回滚和事务传播行为。 + +### 2023-4-13 + +1.手动在增加和删除逻辑较为复杂的数据库操作上,添加了@Transactional注解,遇到运行时异常直接回滚数据库,防止借书和还书出现逻辑错误。 + +2.修复11位图书编号无法借书的Bug,其原因是因为11位超出了Integer的2147483647(10位)。解决方法:数据库改为BigInt,Java改为Long。 + +3.**注意**:不要随便删除用户和公告!!!会导致其他人体验的时候出现逻辑错误!!!请明白了项目逻辑再去做删除操作!!!感谢配合!!! + +4.下一期准备优化图表的展示,逾期图书后告警通知之类的功能,感谢大家的支持,我会继续维护和优化功能,有Bug可以加我QQ或者提出issue,勿要恶意利用bug,再次鸣谢。 + +5.劳动节准备录一期部署项目的视频会发到b站,到时候会将部署讲清楚,方便大家课设或者毕设的完成,此项目有数据库表设计、API接口文档、内容功能介绍、亮点介绍,唯一缺少的可能是数据流图、ER图之类的,star的人多了,我会添加上去。 + +### 2023-5-1 + +1.添加“系统管理员”权限的系统管理功能,**添加借书类型分析统计图(饼图)**采用Echarts。 + +2.优化请求在没有收到数据时的显示卡顿的情况,添加“加载中”状态,**使用v-loading**(ElementUI组件库),**优化用户人机交互体验**,在服务器调用接口缓慢的情况下,给予**良好的交互**。 + +3.轮播图优化:**压缩图片体积**,另外使用Swiper的**懒加载**,实现图片加载中状态,然后图片完全加载完成后才显示图片,**优化用户体验过程**。 + +4.后端**新增自定义错误码枚举类**,可以自定义状态码进行返回,保留原有枚举类。 + +5.前端优化部分表格内容展示,当纵向内容过长,**设置了表格最大高度**,超出就会显示滑动窗口。优化表格列宽度,**提高表格美观度**。 + +6.**添加**书籍管理组件的**批量删除图书**功能,优化管理员体验,不用单个删除图书,**提高效率**。 + +7.Jmeter进行压力测试,服务器接口在**100个用户并发**发送请求的情况下,**QPS达到50**以上。 + +### 2023-5-20 + +**后端更新情况** + +1.~~防止前端抓包被获取明文密码,前端输入密码,进行md5加密(混合盐值,防止碰撞),后端直接与数据库加密后的密码比较,相等代表登录成功。提高系统**安全性**!~~。 + +2.整改Controller层,**将业务代码全部放入Service层**,由Controller调用Service服务,并修改了@Transactional注解位置到业务层,减少耦合度,让Controller减少臃肿。做到对扩展开放,对修改关闭。后续考虑运用**设计模式**进行优化代码和**多线程**知识提高在**高并发**下接口响应的速度。 + +3.对照阿里巴巴手册进行代码修改,将警告进行减少,代码更加**优雅、规范**。 + +4.**修复BUG**: 借阅时间为空,造成服务器被击穿。归还日期为空,仍然显示借书成功。(解决方法:时间参数进行校验,判断是否为空) + +5.**工具类增加情况**:SQLUtils(防止SQL注入),NetUtils(网络工具类) + +**前端更新情况** + +1.将路由加载方式,改为懒加载,利用懒加载可以有效分担首页加载压力,**减少首页加载用时**。 + +2.添加404页面,当用户访问请求地址不存在的页面,直接跳转到404页面,**提高用户体验度**。 + +3.添加按钮的加载中状态,**优化人机交互**,提升用户体验度。修改按钮:登录按钮,其他按钮如果有需要可以自定义去修改,增加:loading="loading"即可。 + +**Bug修复情况** + +1.11位图书编号可以借,但却**无法进行逾期检查**,发现方法参数还是Integer,上次把借书和还书的改成Long了,逾期查看还没改成Long,因此出现问题,现在已经修复。 + +### 2023-6-10 + +**前端更新情况** + +1.在某些页面添加全屏功能按钮,**方便用户放大查看表格数据**。 + +2.增加了GitHub和Gitee的地址图标,**方便进行项目拉取和克隆**。 + +3.读者留言组件,留言功能进行强化,防止无意义的数字、字母、空格出现在数据,后续考虑 + +4.读者留言组件,**利用lodash进行节流**,5秒内只可发送一次网络请求,防止恶意刷无效留言。 + +**后端更新情况** + +1.后端添加利用EasyExcel进行图书的**批量导入功能**,实现与实际生活中利用Excel存储一些图书数据的交互功能,**提高导入效率**,和爬虫功能效果相同,都可以实现大数据量情况下的导入,推荐利用EasyExcel进行批量导入,时间会比爬虫会更快。 + +**Bug修复情况** + +1.修改用户页面的修改密码功能,因为上次更新已经加了盐值,但是后端代码逻辑没有进行更改,本次修复"在修改密码后无法登录的情况",原因是因为后端没有加盐值,已修复。 + +2.修复系统管理员修改借阅证的密码然后就登录不上了,原因跟第一条Bug是一样的,因为后端的盐值没有进行添加,已修复。 + +3.修复系统管理员在书籍管理功能时候,直接点击修改书籍,发现书籍的分类是错误的,因为前端只在添加书籍的对话框发了获取分类的请求,修改对话框的时候忘记添加了获取分类的请求,已修复。 + +### 2023-9 + +**前端更新情况** + +1.增加**智能推荐页面**,能够与AI进行交流,**用户输入自己喜欢xxx类的书籍,AI能够在现有数据库中进行分析**,然后给用户作出推荐,调用的是国内AI模型,底层是OpenAI。 + +2.增加**智能分析页面**,输入分析目标和图标类型和Excel文件,AI生成分析结论和可视化图标,大大提高效率,**减少人力分析成本**。 + +3.增加系统管理员可以利用在前端**利用Excel文件批量上传图书**的功能(测试中),仅供参考。 + +**后端更新情况** + +1.增加智能分析的接口和获取最近5条聊天记录的接口,利用**线程池**和**Future**进行**超时请求处理**,如果接口调用超过40秒直接返回错误信息。 + +2.利用Google的Guava中的RateLimiter进行限流控制,**每秒钟只允许一个请求通过**,防止刷量行为。 + +### 2023-11 + +**后端更新情况** + +1.将用户聊天的AI模型切换为阿里的通义千问Plus模型,并且**支持多轮会话的历史记录**,**不再使用讯飞星火的AI模型**,但仍保留工具类。主要是为了可以更快的得到响应,而且阿里的**文档更加详细**,可以**定制话术**,在用户输入无关图书推荐的内容时候,直接**拒绝回答**。 + +2.添加一个 IncSyncDeleteAIMessage **定时任务**,每天将会**删除由于系统错误等原因AI回复失败**,导致内容为空的记录,并且会为这些用户**恢复接口的次数**,**后续可能会选择 RabbitMQ**,将失败的消息放入消息队列,然后**确保失败的消息被消费**。 + +3.登录加密由前端改到后端,由于前端可以被撞库,因此加密依然放到后端。**方案:**前端传输,用 HTTPS 进行密文加密,后端采用盐值+算法进行加密,数据库存密文。 + +4.将留言页面存放在 Redis 中,**减少数据库的 IO 查询**,QPS 是原来的数百倍! + +**前端更新情况** + +1.将三个登录页面的背景图和头像改为存储在 assets 文件夹的 images 中,**主要是为使用项目的人考虑**,很多人不懂图床技术,我这边暂时将登录页面改成静态图。 + +2.权限切换的提示优化,**在图标上面现在有登录权限切换的文字样式**,提示用户有多个登录页面可以切换。 + +3.登录加密由前端改到后端,由于前端可以被撞库,因此加密依然放到后端。**方案**:前端传输,用 HTTPS 进行密文加密,后端采用盐值+算法进行加密,数据库存密文。 + +### 2024-3 + +**后端更新情况** + +1)为 Knife4J 添加 @ ApiOperation 注解,标注每个接口的作用,**方便开发者阅读和测试接口**。 \ No newline at end of file diff --git a/README_en.md b/README_en.md new file mode 100644 index 0000000..528b30a --- /dev/null +++ b/README_en.md @@ -0,0 +1,881 @@ +# OpenSource DocumentationForSmart Libraries + +>作者:[Programmer's Little White Bar](https://luoye6.github.io/) +> +>[Gitee Homepage](https://gitee.com/falle22222n-leaves) +> +>[Github Homepage](https://github.com/luoye6) + +Language:**[English](README_en.md)**| **[中文](README.md).** + +## ☀️Must Read For Beginners + ++ This project has a complete API backend interface document (at the end of the text) (key points⭐) ++ The project deployment video has been recorded ++ If the project is helpful to you, Star ⭐ Once encouraged, I will continue to work hard. ++ [Project online demonstration address](https://www.xiaobaitiao.top) ++ [Project front-end address](https://gitee.com/falle22222n-leaves/vue_-book-manage-system) ++ [Project backend address](https://gitee.com/falle22222n-leaves/vue_-book-manage-system_backend) ++ [Project deployment video](https://www.bilibili.com/video/BV1Zh4y1z7QE/?spm_id_from=333.999.0.0) + +[![star](https://gitee.com/falle22222n-leaves/vue_-book-manage-system/badge/star.svg?theme=dark)](https://gitee.com/falle22222n-leaves/vue_-book-manage-system) [![gitee](https://badgen.net/badge/gitee/falle22222n-leaves/red)](https://gitee.com/falle22222n-leaves) [![github](https://badgen.net/badge/github/github?icon)](https://github.com/luoye6) + +## ☀️Personal Introduction + +![](https://pic.yupi.icu/5563/202403092057479.png) + +![](https://pic.yupi.icu/5563/202403092057529.png) + +## ☀️Introduction + +**AI Intelligent Library** is a system that uses AI models and data analysis to accurately recommend books that users like, and provides AIGC's online generation of BI charts for borrowing volume analysis, which can serve as a data analyst. It mainly has three major users: users (borrowers), librarians, and system administrators. + +> Ps: If you want simplicity and novelty, then this project will be a good choice~ + +![](https://pic.yupi.icu/5563/202403092057798.png) + +![](https://pic.yupi.icu/5563/202403092057400.png) + +## ☀️Function And Features + +### User functions + +1)Book query function: The pagination constructor alleviates the pressure of excessive data, and the backend can set the number of requests to prevent excessive crawler requests and server load. Fuzzy query for field search. The tables can be exported to both PDF and Excel. + +2)Reader rule function: Query existing borrowing rules, borrowing rules include: borrowing number, number of books that can be borrowed, number of days that can be borrowed, library that can be borrowed, overdue fee deduction/day. + +3)View announcements: You can check the list of announcements published by the librarian, with a text scrolling effect. + +4)Personal information: You can view an individual's borrowing card number, borrowing card name, rule number, status, and modify the password of your personal account. + +5)Borrowing information: You can view the records and return status of books you have borrowed. + +6)Violation information: You can check whether the returned books contain any violation information. + +7)Reader's message: Implement the message function and display it in the form of a bullet screen. + +8)**Intelligent recommendation** Users input their preferences, and AI recommends books to users based on the database book list and user preferences. + +### Library Administrator Function + +1)Borrowing Books: The librarian enters the borrowing card number (user), the book number to be borrowed, and the current time, and clicks to borrow. + +2)Returning books: Enter the book number to check if the book is overdue, and set violation information, then choose whether to return the book. + +3)Book Borrowing Report: Used to query the list of books that have been borrowed and returned. It also uses a pagination constructor and fuzzy query fields to display the borrowing card number, book number, borrowing date, deadline, return date, violation information, and handler. + +4)Book Return Report: Used to query the list of books that have been borrowed but not yet returned, displaying the borrowing card number, book number, borrowing date, and deadline. + +5)Announcement: You can query the current list of announcements and delete, modify, and add features. The pagination constructor is used to alleviate the situation of large data volume. + +### System Administrator Function + +1)Book management: It can query all current books, display book numbers, book nicknames, authors, libraries, classifications, locations, status, and descriptions. You can add, modify, and delete books. Implement batch queries using a paging constructor. Utilize fuzzy queries to achieve book search functionality **Utilize plugins to export PDF and Excel**. + +2)Book Types: Display and query all current book types, which can be added, modified, or deleted. Use a pagination constructor to achieve batch queries and alleviate data pressure. + +3)Borrowing Card Management: It is possible to query the current list of all borrowing cards, that is, the number of users, and perform operations such as adding, modifying, and deleting. Implement pagination as well. + +4)Borrowing information query: can query the current completed borrowing and returning records, display the borrowing card number, book number, borrowing date, deadline, return date, violation information, and handler. Paging function, PDF and Excel export. + +5)Borrowing Rule Management: You can query all current borrowing rules, display restricted borrowing days, restricted book count, restricted library, overdue fees, and perform add, delete, and modify operations. + +6)Librarian Management: Display the current list of librarians, including accounts, names, and email addresses, allowing for adding, deleting, and modifying operations. + +7)System management: It is possible to query the borrowing volume within a month, calculate the borrowing volume at a weekly interval, and use Echarts to display various charts. + +8)System analysis: You can upload the borrowing volume and date for a certain time period, and input the analysis target and the type of chart you want to generate. After waiting for a period of time, **AI will provide analysis conclusions and visual charts**. + +### Features (highlights) + +1)This project adopts a front-end and back-end separation mode, with the front-end building the page and the back-end serving as the data interface. The front-end calls the back-end data interface to obtain data and re render the page. + +2)The front-end provides a Token token in the Authorization field, API authentication uses Token authentication, HTTP Status Code represents status, and data return format uses JSON. + +3)The backend has enabled CORS cross domain support, using permission interceptors for permission verification and checking login status. + +4)Add a global exception handling mechanism to capture exceptions and enhance system robustness. + +5)The front-end uses the Echarts visualization library to implement analysis icons (line charts, pie charts) for book borrowing, and improves the loading experience through loading configuration. + +6)The message component adopts bullet screen format, which is in line with user preferences. + +7)Introduce the knife4j dependency and use Swagger+Knife4j to automatically generate interface documents for the OpenAPI specification. The front-end can use plugins to automatically generate interface request codes based on this, reducing the cost of front-end and back-end collaboration + +8)By using the ElementUI component library for front-end interface construction, we can quickly generate pages and achieve unified permission management and multi environment switching capabilities for both front-end and back-end. + +9)The QueryWrapper based on the MyBatis Plus framework enables flexible querying of MySQL databases and, in conjunction with the MyBatisX plugin, automatically generates backend CRUD basic code to reduce repetitive work. + +10)Front end routing lazy loading, CDN static resource cache optimization, and image lazy loading effect. + +## ☀️Operation Mode + +### 2 Minutes To Quickly Get Started Using The Project + +1)Find the SpringBoot startup class, click on Run + +![](https://pic.yupi.icu/5563/202403092057483.png) + +2)Open Knife4J to register as a user, or you can directly contact me to obtain database simulation data (simple). + +![](https://pic.yupi.icu/5563/202403092057462.png) + +![](https://pic.yupi.icu/5563/202403092057532.png) + +3)After entering the form content in the front-end, click on login to successfully start using the function happily~ + +![](https://pic.yupi.icu/5563/202403092057421.png) + +![](https://pic.yupi.icu/5563/202403092057302.png) + +## ☀️Deployment Method + +### Preconditions + +**Front end** + +Software: Vscore or Webstorm (recommended) + +Environment: Node version 16 or 18 (recommended) **Note: Do not choose versions above 18** + +**Backend** + +Software: Eclipse or IDEA (recommended) + +Environment: MySQL 5.7 or 8.0 (recommended) Redis (optional) + +### Front End Deployment + +1)Clicking on Clone/Download Project will use Git for version control. It is recommended to use Git Clone. If you do not know how to do so, you can choose to download a Zip compressed file and extract it to your computer's D drive. It is recommended to use Star directly, and then directly obtain the database simulation file and API interface documentation from me. + +![](https://pic.yupi.icu/5563/202403092057115.png) + +2)Open the front-end page using Vscode or Webstorm and configure the Configuration. Configure the Node environment and package management tools. The package management tool I have chosen is Npm, while other package management tools such as Yarn, Cnpm, and Pnpm are all available**Note: Please change the image address of Npm to the new image address on Taobao, otherwise Npm Install will keep getting stuck in the progress bar**. + +3)Simply click on the run of dev or open the console and enter npm run serve to successfully launch the front-end project. + +```shell +npm config set registry https://registry.npmmirror.com/ +``` + +![](https://pic.yupi.icu/5563/202403092057497.png) + +![](https://pic.yupi.icu/5563/202403092057168.png) + +![](https://pic.yupi.icu/5563/202403092057855.png) + +4)Customize and switch image links to your own image bed, such as Qiniuyun, GitHub, etc. You can also search for online images and copy Baidu Wenku image links (try multiple times, some images have anti-theft links) **After changing the background, you can see the permission switch icon in the bottom right corner**. + +![](https://pic.yupi.icu/5563/202403092057651.png) + +![](https://pic.yupi.icu/5563/202403092057028.png) + + + +### Backend Deployment + +1)Clicking on Clone/Download Project will use Git for version control. It is recommended to use Git Clone. If you do not know how to do so, you can choose to download a Zip compressed file and extract it to your computer's D drive. It is recommended to use Star directly, and then directly obtain the database simulation file and API interface documentation from me. + +![](https://pic.yupi.icu/5563/202403092057403.png) + +2)After receiving the database simulation file, use software such as Navicat or SQLYog to import the database file. Remember to first create a database named bms_boot, and then right-click to run the SQL file. After running successfully without any errors, reopen the database and check for data. If there is data, it indicates successful import.![](https://pic.yupi.icu/5563/202403092057396.png) + +![](https://pic.yupi.icu/5563/202403092057154.png) + +3) Open the backend project using IDEA, locate the application dev.yml file, modify the MySQL configuration, and ensure that the username and password are correct. Note: Passwords cannot start with the number 0. + +![](https://pic.yupi.icu/5563/202403092057117.png) + +4)mporting Maven dependencies, pay attention to checking if your Maven version is correct. It is recommended to choose the same version as mine, version 3.8 or above. I found that importing dependencies is slow because there is no configuration for domestic images, and the default connection is to foreign servers. Therefore, Alibaba Cloud image configuration can be found in this blog post.[CSDN Maven 配置教程](https://blog.csdn.net/lianghecai52171314/article/details/102625184?ops_request_misc=&request_id=&biz_id=102&utm_term=Maven) + +![](https://pic.yupi.icu/5563/202403092057513.png) + +5)Find the SpringBoot startup class, and I suggest using Debug mode to start the project for better troubleshooting. + +![](https://pic.yupi.icu/5563/202403092057544.png) + +6)If you encounter an error, it is most likely a JDK version issue. My project is using JDK 8, so it is recommended to choose the same version as me. + +![](https://pic.yupi.icu/5563/202403092057723.png) + +![](https://pic.yupi.icu/5563/202403092057850.png) + +7)The successful launch of the project results are shown below + +![](https://pic.yupi.icu/5563/202403092057083.png) + +### Front And Rear End Joint Debugging + +1)If you need to modify the port and prefix (such as/API), you need to modify both the front-end and back-end. + +![](https://pic.yupi.icu/5563/202403092057115.png) + +![](https://pic.yupi.icu/5563/202403092057299.png) + +## ☀️Technical Selection + +### Front End + +| **技术** | **作用** | **版本** | +| ---------------------------- | ------------------------------------------------------------ | ---------------------------------------------------- | +| Vue | 提供前端交互 | 2.6.14 | +| Vue-Router | 路由式编程导航 | 3.5.1 | +| Element-UI | 模块组件库,绘制界面 | 2.4.5 | +| Axios | 发送ajax请求给后端请求数据 | 1.2.1 | +| core-js | 兼容性更强,浏览器适配 | 3.8.3 | +| swiper | 轮播图插件(快速实现) | 3.4.2 | +| vue-baberrage | vue弹幕插件(实现留言功能) | 3.2.4 | +| vue-json-excel | 表格导出Excel | 0.3.0 | +| html2canvas+jspdf | 表格导出PDF | 1.4.1 2.5.1 | +| node-polyfill-webpack-plugin | webpack5中移除了nodejs核心模块的polyfill自动引入 | 2.0.1 | +| default-passive-events | **Chrome** 增加了新的事件捕获机制 **Passive Event Listeners**(被动事件侦听器) | 让页面滑动更加流畅,主要用于提升移动端滑动行为的性能 | +| nprogress | 发送请求显示进度条(人机交互友好) | 0.2.0 | +| echarts | 数据转图标的好工具(功能强大) | 5.4.1 | +| less lessloader | 方便样式开发 | 4.1.3 11.1.0 | + +### 后端 + +| **技术及版本** | **作用** | **版本** | +| ------------------------------------ | ------------------------------------------------------------ | --------------------------------- | +| SpringBoot | 应用开发框架 | 2.7.8 | +| JDK | Java 开发包 | 1.8 | +| MySQL | 提供后端数据库 | 8.0.23 | +| MyBatisPlus | 提供连接数据库和快捷的增删改查 | 3.5.1 | +| SpringBoot-Configuration-processor | 配置处理器 定义的类和配置文件绑定一般没有提示,因此可以添加配置处理器,产生相对应的提示. | | +| SpringBoot-Starter-Web | 后端集成Tomcat MVC | 用于和前端连接 | +| SpringBoot-starter-test | Junit4单元测试前端在调用接口前,后端先调用单元测试进行增删改查,注意Junit4和5的问题,注解@RunWith是否添加 | | +| Lombok | 实体类方法的快速生成 简化代码 | | +| mybatis-plus-generator | 代码生成器 | 3.5.1 | +| MyBatisX | MyBatisPlus插件直接生成mapper,实体类,service | | +| jjwt | token工具包 | 0.9.0 | +| fastjson | 阿里巴巴的 JSON 工具类 | 1.2.83 | +| hutool | hutool工具包(简化开发工具类) | [文档](https://hutool.cn/docs/#/) | +| knife4j-openapi2-spring-boot-starter | Knife4j 在线接口文档测试工具 | 4.0.0 | +| gson | 谷歌的 JSON 工具类 | 2.8.5 | +| Java-WebSocket | 讯飞星火 AI 配置 | 1.3.8 | +| okhttp | 讯飞星火 AI 配置 | 4.10.0 | +| okio | 讯飞星火 AI 配置 | 2.10.0 | +| jsoup | 简易爬虫工具 | 1.15.3 | +| guava | 谷歌工具类 | 30.1-jre | +| spring-boot-starter-data-redis | Redis 的 Starter | | +| broadscope-bailian-sdk-java | 阿里云 AI 模型 | 1.1.7 | +| spring-boot-starter-websocket | WebSocket 的 Starter | | + +## ☀️Architecture + +![](https://pic.yupi.icu/5563/202403092057718.png) + +## ☀️Core Design + +### Intelligent Recommendation Function + +1)Users input their book preference information. + +2)The front-end sends Axios requests. + +3)The backend first checks whether the text is illegal (empty or too long). + +4)Check if the interface exists. + +5)Check if the number of AI interface calls is sufficient. + +6)GuavaRateLimiter performs individual flow limiting to determine if the number of requests exceeds the normal business frequency. + +7)Manually preset the AI model and query the list of books in the database for concatenation. + +8)Query the AI model and the user's latest five historical records for context association. + +9)FutureTask synchronously calls to obtain AI results and sets a timeout (timeout throws an exception) + +10)Persist after obtaining AI model recommendation information and reduce the number of interface calls (to determine if successful) + +12)Return the processed AI recommendation information to the front-end and set the response status code to 200. + +### Intelligent Analysis Function + +1)Users input analysis targets, icon names, select icon types, upload Excel files, click submit, and send Axios requests to the backend. + +2)Verify whether the file is empty, the name is too long, the file size is checked, and the file suffix is checked + +3)Obtain the administrator ID and query the interface owned by the administrator ID from the interface information table. The interface is found to be empty. + +4)Determine if the number of AI interface calls is sufficient + +5)GuavaRateLimiter performs individual flow limiting to determine if the number of requests exceeds the normal business frequency. + +6)Hint words and roles for constructing AI models + +7)Construct user input, concatenate user input information, and use tool classes to convert Excel into CSV string data. + +8)Using the iFlytek Starfire AI model, input the caller ID and input parameters, use FutureTask to synchronously obtain them, and set a timeout time (timeout throws an exception) + +10)Judging the AI generated results, if there is a formatting error, return the front-end error message and prompt for re calling (consider RabbitMQ for retry and compensation mechanisms in the future) + +11)Persist the AI generated results to the database, update the number of interface calls (to determine if successful), and dynamically return icons and data conclusions to the front-end. + +## ☀️What Will You Get After Completing This Project + +1)Simply call the AI model (iFlytek Starfire | Alibaba Bailian) to obtain custom text content. + +2)Simple JWT permission verification, using backend interceptors for login verification. + +3)Upload an Excel file, convert the Excel file to CSV data, and generate visual charts online by AIGC. + +4)Jousp can batch crawl book lists and execute them in conjunction with SpringSchedule scheduled tasks. + +5)How does the front-end and back-end of a simple system for adding, deleting, modifying, and querying work together. + +6)How are front-end routing lazy loading, CDN static resource caching optimization, and image lazy loading implemented + +7)Use Lodash for throttling control to minimize ineffective malicious message brushing. + +8)Use custom thread pools and FutureTasks for timeout request processing. + +9)Utilize Google's GuavaRateLimited for individual flow limiting control. + +10)Combining scheduled tasks with Redis for cache preheating to accelerate query efficiency and improve user experience. + +## ☀️Project Introduction + ++ Mainly implemented using Vue2 and SpringBoot2 + ++ The project permission controls are: user borrowing, librarian, and system administrator + ++ Development tool: IDEA2022.1.3 (I really don't recommend using Eclipse for development. IDEA projects can be exported as Eclipse projects, and they don't affect each other, but you need to learn the tutorial yourself) + ++ [IDEA ->Eclipse]( https://blog.csdn.net/HD202202/article/details/128076400 ) + ++ [Eclipse ->IDEA]( https://blog.csdn.net/q20010619/article/details/125096051 ) + ++ If the school teachers insist on software, they should still follow the requirements. Can you first ask if it is possible to choose other software development options. + ++ User account password: Xiangsi Duan Hongchang 123456 + ++ Librarian account password: admin 123456 + ++ System administrator account password: root 123456 + ++ [Front end style reference]( https://gitee.com/mingyuefusu/tushuguanlixitong )Thank you to the original author **Mingyue Resurrection** + ++ Encountered an interaction function error or the page cannot be opened. Please use the developer tool F12 to check the status code of the request and response. Of course, the novice may not understand, so it's okay. You can add me **QQ: 909088445**. Class during the day, I can only answer when I have time at night. Thank you for your understanding! ⭐⭐⭐ + +## ☀️Project Detailed Introduction (HighLights) + ++ This project adopts a front-end and back-end separation mode, with the front-end building the page and the back-end serving as the data interface. The front-end calls the back-end data interface to obtain data and re render the page. + ++ The backend has enabled CORS cross domain support + ++ API authentication using Token authentication + ++ The front-end provides a Token token in the Authorization field + ++ Using HTTP Status Code to represent status + ++ Use JSON for data return format + ++ The backend uses permission interceptors for permission verification and checks login status + ++ Add a global exception handling mechanism to capture exceptions and enhance system robustness + ++ The front-end uses the Echarts visualization library to implement analysis icons (line charts, pie charts) for book borrowing, and improves the loading experience through loading configuration. + ++ The message component adopts bullet screen format, which is in line with user preferences. + ++ Introduce the knife4j dependency and use Swagger+Knife4j to automatically generate interface documents for the OpenAPI specification. The front-end can use plugins to automatically generate interface request codes based on this, reducing the cost of front-end and back-end collaboration + ++ By using the ElementUI component library for front-end interface construction, we can quickly generate pages and achieve unified permission management and multi environment switching capabilities for both front-end and back-end. + ++ The QueryWrapper based on the MyBatis Plus framework enables flexible querying of MySQL databases and, in conjunction with the MyBatisX plugin, automatically generates backend CRUD basic code to reduce repetitive work. + ++ Front end routing lazy loading, CDN static resource cache optimization, and image lazy loading effect. + +### ⭐Introduction To User Module Functions + +![](https://pic.yupi.icu/5563/202403092057579.png) + ++ Book query function: The pagination constructor alleviates the pressure of excessive data, and the backend can set the number of requests to prevent excessive crawler requests and server load. Fuzzy query for field search. Tables can be exported to both PDF and Excel. + ++ Reader rule function: Query existing borrowing rules, borrowing rules include: borrowing number, number of books that can be borrowed, number of days that can be borrowed, library that can be borrowed, overdue fee deduction/day. + ++ View announcements: You can check the list of announcements published by the librarian, with text scrolling effect. + ++ Personal information: You can view an individual's borrowing card number, borrowing card name, rule number, status, and modify the password of your personal account. + ++ Borrowing information: You can view the records and return status of books you have borrowed. + ++ Violation information: You can check whether the returned books contain any violation information. + ++ Reader's message: Implement the message function and display it in bullet screen format. + +### ⭐Introduction To The Functions Of The Librarian Module + +![](https://pic.yupi.icu/5563/202403092057247.png) + ++ Borrowing Books: The librarian enters the borrowing card number (user), the book number to be borrowed, and the current time, and clicks to borrow. + ++ Returning books: Enter the book number to check if the book is overdue, and set violation information, then choose whether to return the book + ++ Book Borrowing Report: Used to query the list of books that have been borrowed and returned. It also uses a pagination constructor and fuzzy query fields to display the borrowing card number, book number, borrowing date, deadline, return date, violation information, and handler. + ++ Book Return Report: Used to query the list of books that have been borrowed but not yet returned, displaying the borrowing card number, book number, borrowing date, and deadline. + ++ Announcement: You can query the current list of announcements and delete, modify, and add features. The pagination constructor is used to alleviate the situation of large data volume. + +### ⭐Introduction To The Functions Of The System Administrator Module + +![](https://pic.yupi.icu/5563/202403092057930.png) + ++ Book management: It can query all current books, display book numbers, book nicknames, authors, libraries, classifications, locations, status, and descriptions. You can add, modify, and delete books. Implement batch queries using a paging constructor. Utilize fuzzy queries to achieve book search functionality. Use plugins to export PDF and Excel. + ++ Book Types: Display and query all current book types, which can be added, modified, or deleted. Use a pagination constructor to achieve batch queries and alleviate data pressure. + ++ Borrowing Card Management: It is possible to query the current list of all borrowing cards, that is, the number of users, and perform operations such as adding, modifying, and deleting. Implement pagination as well. + ++ Borrowing information query: can query the current completed borrowing and returning records, display the borrowing card number, book number, borrowing date, deadline, return date, violation information, and handler. Paging function, PDF and Excel export. + ++ Borrowing Rule Management: You can query all current borrowing rules, display restricted borrowing days, restricted book count, restricted library, overdue fees, and perform add, delete, and modify operations. + ++ Librarian Management: Display the current list of librarians, including accounts, names, and email addresses, allowing for adding, deleting, and modifying operations. + ++ System management: It is possible to query the borrowing volume within a month, calculate the borrowing volume at a weekly interval, and use Echarts to display a line chart. + +## ☀️Database Table Design + +### t_users表 + +| 列名 | 数据类型以及长度 | 备注 | +| ----------- | ---------------- | ------------------------------------------------- | +| user_id | int(11) | 主键 非空 自增 用户表的唯一标识 | +| username | varchar(32) | 用户名 非空 | +| password | varchar(32) | 密码(MD5加密) 非空 | +| card_name | varchar(10) | 真实姓名 非空 | +| card_number | Bigint(11) | 借阅证编号 固定 11位随机生成 非空(后文都改BigInt) | +| rule_number | int(11) | 规则编号 可以自定义 也就是权限功能 | +| status | int(1) | 1表示可用 0表示禁用 | +| create_time | datetime | 创建时间 Java注解 JsonFormatter | +| update_time | datetime | 更新时间 Java注解 JsonFormatter | + +### t_admins表 + +| 列名 | 数据类型以及长度 | 备注 | +| ----------- | ---------------- | --------------------------------- | +| admin_id | int(11) | 主键 非空 自增 管理员表的唯一标识 | +| username | varchar(32) | 用户名 非空 | +| password | varchar(32) | 密码(MD5加密) 非空 | +| admin_name | varchar(10) | 管理员真实姓名 非空 | +| status | int(1) | 1表示可用 0表示禁用 | +| create_time | datetime | 创建时间 Java注解 JsonFormatter | +| update_time | datetime | 更新时间 Java注解 JsonFormatter | + +### t_book_admins表 + +| 列名 | 数据类型以及长度 | 备注 | +| --------------- | ---------------- | ------------------------------- | +| book_admin_id | int(11) | 主键 非空 自增 管理表的唯一标识 | +| username | varchar(32) | 用户名 非空 | +| password | varchar(32) | 密码(MD5加密)非空 | +| book_admin_name | varchar(10) | 图书管理员真实姓名 非空 | +| status | int(1) | 1表示可用 0表示禁用 | +| email | varchar(255) | 电子邮箱 | +| create_time | datetime | 创建时间 Java注解 JsonFormatter | +| update_time | datetime | 更新时间 Java注解 JsonFormatter | + +### t_books表 + +| 列名 | 数据类型以及长度 | 备注 | +| ---------------- | ---------------- | ------------------------------- | +| book_id | int(11) | 主键 自增 非空 图书表的唯一标识 | +| book_number | int(11) | 图书编号 非空 图书的唯一标识 | +| book_name | varchar(32) | 图书名称 非空 | +| book_author | varchar(32) | 图书作者 非空 | +| book_library | varchar(32) | 图书所在图书馆的名称 非空 | +| book_type | varchar(32) | 图书类别 非空 | +| book_location | varchar(32) | 图书位置 非空 | +| book_status | varchar(32) | 图书状态(未借出/已借出) | +| book_description | varchar(100) | 图书描述 | +| create_time | datetime | 创建时间 Java注解 JsonFormatter | +| update_time | datetime | 更新时间 Java注解 JsonFormatter | + +### t_books_borrow表 + +| 列名 | 数据类型以及长度 | 备注 | +| ----------- | ---------------- | ------------------------------------------------------------ | +| borrow_id | int(11) | 主键 自增 非空 借阅表的唯一标识 | +| card_number | int(11) | 借阅证编号 固定 11位随机生成 非空 用户与图书关联的的唯一标识 | +| book_number | int(11) | 图书编号 非空 图书的唯一标识 | +| borrow_date | datetime | 借阅日期 Java注解 JsonFormatter | +| close_date | datetime | 截止日期 Java注解 JsonFormatter | +| return_date | datetime | 归还日期 Java注解 JsonFormatter | +| create_time | datetime | 创建时间 Java注解 JsonFormatter | +| update_time | datetime | 更新时间 Java注解 JsonFormatter | + +### t_notice表 + +| 列名 | 数据类型以及长度 | 备注 | +| --------------- | ---------------- | ----------------------------------- | +| notice_id | int(11) | 主键 非空 自增 公告表记录的唯一标识 | +| notice_title | varchar(32) | 公告的题目 非空 | +| notice_content | varchar(255) | 公告的内容 非空 | +| notice_admin_id | int(11) | 发布公告的管理员的id | +| create_time | datetime | 创建时间 Java注解 JsonFormatter | +| update_time | datetime | 更新时间 Java注解 JsonFormatter | + +### t_violation表 + +| 列名 | 数据类型以及长度 | 备注 | +| ------------------ | ---------------- | ----------------------------------- | +| violation_id | int(11) | 主键 非空 自增 违章表记录的唯一标识 | +| card_number | int(11) | 借阅证编号 固定 11位随机生成 非空 | +| book_number | int(11) | 图书编号 非空 图书的唯一标识 | +| borrow_date | datetime | 借阅日期 Java注解 JsonFormatter | +| close_date | datetime | 截止日期 Java注解 JsonFormatter | +| return_date | datetime | 归还日期 Java注解 JsonFormatter | +| violation_message | varchar(100) | 违章信息 非空 | +| violation_admin_id | int(11) | 违章信息管理员的id | +| create_time | datetime | 创建时间 Java注解 JsonFormatter | +| update_time | datetime | 更新时间 Java注解 JsonFormatter | + +### t_comment表 + +| 列名 | 数据类型以及长度 | 备注 | +| --------------------- | ---------------- | ----------------------------------- | +| comment_id | int(11) | 主键 非空 自增 留言表记录的唯一标识 | +| comment_avatar | varchar(255) | 留言的头像 | +| comment_barrage_style | varchar(32) | 弹幕的高度 | +| comment_message | varchar(255) | 留言的内容 | +| comment_time | int(11) | 留言的时间(控制速度) | +| create_time | datetime | 创建时间 Java注解 JsonFormatter | +| update_time | datetime | 更新时间 Java注解 JsonFormatter | + +### t_book_rule表 + +| 列名 | 数据类型以及长度 | 备注 | +| ------------------ | ---------------- | ------------------------------------- | +| rule_id | int(11) | 主键 非空 自增 借阅规则记录的唯一标识 | +| book_rule_id | int(11) | 借阅规则编号 非空 | +| book_days | int(11) | 借阅天数 非空 | +| book_limit_number | int(11) | 限制借阅的本数 非空 | +| book_limit_library | varchar(255) | 限制的图书馆 非空 | +| book_overdue_fee | double | 图书借阅逾期后每天费用 非空 | +| create_time | datetime | 创建时间 Java注解 JsonFormatter | +| update_time | datetime | 更新时间 Java注解 JsonFormatter | + +### t_book_type表 + +| 列名 | 数据类型以及长度 | 备注 | +| ------------ | ---------------- | ------------------------------------- | +| type_id | int(11) | 主键 非空 自增 图书类别记录的唯一标识 | +| type_name | varchar(32) | 借阅类别的昵称 非空 | +| type_content | varchar(255) | 借阅类别的描述 非空 | +| create_time | datetime | 创建时间 Java注解 JsonFormatter | +| update_time | datetime | 更新时间 Java注解 JsonFormatter | + +## 🐼Functional Demonstration Diagram + +### User Module Function Diagram + +**Homepage carousel demonstration** + +![](https://pic.yupi.icu/5563/202403092057819.png) + +**Book search demonstration** + +![](https://pic.yupi.icu/5563/202403092057365.png) + +**Reader Rule Demonstration** + +![](https://pic.yupi.icu/5563/202403092057384.png) + +**View announcement demonstration** + +![](https://pic.yupi.icu/5563/202403092057303.png) + +**Personal information demonstration** + +![](https://pic.yupi.icu/5563/202403092057307.png) + +**Presentation of Borrowing Information** + +![](https://pic.yupi.icu/5563/202403092057741.png) + +**Violation information demonstration** + +![](https://pic.yupi.icu/5563/202403092057862.png) + +**Reader's message demonstration** + +![](https://pic.yupi.icu/5563/202403092057854.png) + +**Intelligent recommendation demonstration** + +![](https://pic.yupi.icu/5563/202403092057828.png) + +### Library Administrator Function Diagram + +**Borrowing book demonstration** + +![](https://pic.yupi.icu/5563/202403092057329.png) + +**Demo on returning books** + +![](https://pic.yupi.icu/5563/202403092057296.png) + +**Presentation of borrowing report** + +![](https://pic.yupi.icu/5563/202403092057841.png) + +**Presentation of return report** + +![](https://pic.yupi.icu/5563/202403092057469.png) + +**Announcement demonstration** + +![](https://pic.yupi.icu/5563/202403092057504.png) + +### System Administrator Function Diagram + ++ Due to space limitations, the main functions of the system are displayed. + +**System Management Demonstration** + +![](https://pic.yupi.icu/5563/202403092057554.png) + +![](https://pic.yupi.icu/5563/202403092057566.png) + +**Intelligent analysis demonstration** + +![](https://pic.yupi.icu/5563/202403092057486.png) + +## 🐼Deployment Projects + +![](https://pic.yupi.icu/5563/202403092057711.png) + ++ You can download ZIP compressed packages or use clones (Git clone) + ++ Copy HTTP or SSH links (Github suggests SSH, Gittee can do both) + ++ Create a new folder on drive D, click to enter the folder, right-click on Git Bash Here + +![](https://pic.yupi.icu/5563/202403092057608.png) + ++ If you haven't downloaded Git yet or don't know Git, it is recommended to read the basic tutorial first (about 30 minutes) + ++ Enter git init to initialize the git project and a. git folder will appear + ++ Enter git remote add origin xxxxxx (xxx is the HTTP or SSH link just copied) + ++ Enter git pull origin master to pull code from a remote code hosting repository + ++ Successfully pulled the project (both front-end and back-end are like this) + ++ Front end projects should pay attention to relying on downloading and using npm install or yarn install (Vscode or Webstorm) + ++ Backend projects should pay attention to Maven dependency downloads (IDEA (recommended) or Ecplise) + ++ Suggest Taobao image source for front-end NPM image source, and Alibaba Cloud image source for back-end Maven image source (optional, but quick download after replacement) + +## 🐼Deployment Project Issues + +⭐ + ++ The UFT-8 used in the garbled code problem project + ++ Generally, garbled characters are the opposite of UTF-8 and GBK + ++ Please provide a clear description of Baidu IDEA garbled code and Eclipse garbled code issues + +⭐ + ++ Clicking the interaction button did not result in any response. + ++ It is obvious that the request has failed. The browser opens the developer tool, and Edge browser directly uses Ctrl+shift+i, while other browsers press F12 + ++ View red request and response status code issues + +⭐ + ++ Read the document first before querying or asking questions + ++ Skilled questioning and vague statements make it difficult for senior architects to identify bugs + +⭐ + ++ **QQ: 909088445** + ++ Usually online at night, it is recommended to find the problem yourself first!!! + ++ Open source is free, customized and debugging projects are paid for. + +## 🐼Requirement Analysis And Design + +Requirement analysis and design documents. For those with (**paid**) requirements, you can add QQ: 909088445. It is suitable for those who have completed the design and course design. For those who want to save time, please feel free to contact me. + +![](https://pic.yupi.icu/5563/202403092057586.png) + +## 🐼Project API Interface Document + ++ The interface document is too lengthy + ++ I originally intended to completely adopt the RESTFUL style, but forgot halfway through it + ++ Read the reference address of the document clearly + ++ To combine the detailed content of the API backend interface document with the database structure and content, the front-end and back-end **star will be added** ⭐ Take a screenshot of it and add it to my QQ: **909088445** Send it to me to collect it~Thank you for your support + +#### **Sample Screenshot Of Database Retrieval (Gitee&GitHub):** + +![](https://pic.yupi.icu/5563/202403092057726.png) + +![](https://pic.yupi.icu/5563/202403092057307.png) + +![](https://pic.yupi.icu/5563/202403092057245.png) + +![](https://pic.yupi.icu/5563/202403092057226.png) + +## 🐷 Other + ++ Personal blog address: https://luoye6.github.io/ + ++ Personal blog is hosted on Hexo+Github + ++ Using the butterfly theme can achieve customization + ++ It is recommended that those who have free time can spend 1-2 days building a personal blog to take notes. + +## ☕Please Treat Me To coffee + +If this project is helpful to you, may I have a cup of coffee with the author :) + +
+ +## **Version Iteration** + +### March 19, 2023 + + + +1)Introduce the knife4j dependency and use Swagger+Knife4j to automatically generate interface documentation for the OpenAPI specification. The front-end can use plugins to automatically generate interface request code on this basis, reducing the cost of front-end and back-end collaboration. + +2)Introducing JSOUP dependencies allows for custom addition of crawler functionality, allowing for batch addition of books with relatively real data. + +3)Add a transaction manager to enable @ Transactional to specify exception types for rollback and transaction propagation behavior. + +### April 13, 2023 + +1)In manually adding and deleting database operations with complex logic, @ Transactional annotations have been added. When encountering runtime exceptions, the database can be rolled back directly to prevent logical errors in borrowing and returning books. + +2)Fix the bug where the 11 digit book number cannot be borrowed, as it exceeds the integer's 2147483647 (10 digits). Solution: Change the database to BigInt and Java to Long. + +3)**Note**: Do not delete users and announcements casually!!! It can cause logical errors when others experience it!!! Please understand the project logic before proceeding with the deletion operation!!! Thank you for your cooperation!!! + +4)In the next issue, we are preparing to optimize the display of charts and functions such as alarm notifications after overdue books. Thank you for your support. I will continue to maintain and optimize the functions. If there are any bugs, you can add me on QQ or raise an issue. Do not maliciously exploit the bugs. Thank you again. + +5)A video of a deployment project for Labor Day will be posted on Bilibili, and the deployment will be explained clearly to facilitate the completion of course or final projects. This project includes database table design, API interface documentation, content and function introduction, and highlight introduction. The only missing ones may be data flow diagrams, ER diagrams, and so on. As there are many people on Star, I will add them. + +### May 1st, 2023 + +1)Add the system management function with "System Administrator" permissions, and **add a borrowing type analysis and statistical chart (pie chart)** using Echarts. + +2)Optimize the display lag when no data is received, add a "loading" status, **use v-loading** (ElementUI component library), **optimize user human-machine interaction experience**, and provide **good interaction** when the server calls the interface slowly. + +3)Rotation image optimization: **Compress image volume**, and also use Swiper's **Lazy loading** to achieve image loading status, and then display the image only after it is fully loaded, **optimizing the user experience process**. + +4)Add a custom error code enumeration class to the backend, which allows for custom status codes to be returned while retaining the original enumeration class. + +5)The front-end optimization section displays table content. When the vertical content is too long and the maximum height of the table is set, a sliding window will be displayed if it exceeds the limit. Optimize table column width and improve table aesthetics. + +6)Add the **Batch Delete Books** function of the Book Management component to optimize the administrator experience, eliminating the need for individual book deletions and improving efficiency. + +7)Jmeter conducted stress testing, and the server interface achieved a QPS of 50 or above when **100 users simultaneously sent requests**. + +### May 20th, 2023 + +**Backend updates** + +1)To prevent the front-end from capturing packets and obtaining plaintext passwords, the front-end inputs the password and performs MD5 encryption (mixed salt values to prevent collisions). The back-end directly compares the encrypted password with the database, and equality represents successful login. Improve system security. + +2)Rectify the Controller layer by placing all business code into the Service layer, where the Controller calls the Service service and modifies the @ Transactional annotation position to the Business layer, reducing coupling and reducing bloating for the Controller. Be open to extensions and closed to modifications. In the future, we will consider using **design patterns** to optimize code and **multi threading** knowledge to improve interface response speed under **high concurrency**. + +3)Modify the code according to the Alibaba manual, reduce warnings, and make the code more elegant and standardized. + +4)**Fix bug**: Borrowing time is empty, causing server breakdown. If the return date is empty, it still shows that the borrowing was successful. (Solution: Verify the time parameter to determine if it is empty) + +5)**Tool class increase**: SQLUtils (preventing SQL injection), NetUtils (network tool class) + +**Front end update status** + +1)Change the route loading method to lazy loading, which can effectively alleviate the pressure of homepage loading and reduce the time required for homepage loading. + +2)Add a 404 page, and when the user visits a page with a request address that does not exist, they will be redirected directly to the 404 page to improve the user experience. + +3)Add the loading status of the button, **optimize human-computer interaction**, and improve user experience. Modify button: Login button, other buttons can be customized and modified if needed. Add: loading="loading". + +**Bug fixing status** + +The 1.11 digit book number can be borrowed, but **cannot be checked for overdue payment**. It was found that the method parameter is still Integer. Last time, the borrowing and returning books were changed to Long, but the overdue payment check has not been changed to Long, resulting in a problem. It has now been fixed. + +### June 10, 2023 + +**Front end update status** + +1)Add full screen function buttons on certain pages to facilitate users to zoom in and view table data. + +2)Added address icons for GitHub and Gitee to facilitate project pulling and cloning. + +3)The reader's comment component and comment function will be strengthened to prevent meaningless numbers, letters, and spaces from appearing in the data. Further considerations will be made in the future + +4)Reader message component, **using lodash for throttling **, can only send network requests once within 5 seconds to prevent malicious brushing of invalid messages. + +**Backend updates** + +1)Add a batch import function for books using EasyExcel on the backend to achieve interaction with storing some book data using Excel in real life, improve import efficiency, and achieve the same effect as the crawler function. It can also import large amounts of data. It is recommended to use EasyExcel for batch import, which will take faster time than the crawler function. + +**Bug fixing status** + +1)Modify the password modification function on the user page, as the last update already added salt values, but the backend code logic has not been changed. This fix "inability to log in after password modification" is due to the backend not adding salt values, which has been fixed. + +2)Fixed the issue where the system administrator changed the password for the borrowing certificate and was unable to log in. The reason is the same as the first bug, as the backend's salt value was not added and has been fixed. + +3)Fixed the issue where the system administrator directly clicked on "modify book" in the book management function and found that the classification of the book was incorrect. This was because the front-end only sent a request to obtain the classification in the "add book" dialog box, and forgot to add a request to obtain the classification when modifying the dialog box. This issue has been fixed. + +### September 2023 + +**Front end update status** + +1)Add an intelligent recommendation page that can communicate with AI. Users can input their favorite xxx books, and AI can analyze them in existing databases to make recommendations. The domestic AI model is called, and the underlying layer is OpenAI. + +2)Add an intelligent analysis page, input analysis objectives, icon types, and Excel files, generate analysis conclusions and visual icons through AI, greatly improving efficiency and reducing labor analysis costs. + +3)Add the function of system administrators to upload books in bulk using Excel files on the front-end (under testing), for reference only. + +**Backend updates** + +1)Add interfaces for intelligent analysis and obtaining the last 5 chat records, and use **Thread Pool** and **Future** for timeout request processing. If the interface call exceeds 40 seconds, an error message will be returned directly. + +2)Utilize RateLimiter in Google's Guava for flow limiting control, allowing only one request per second to pass through, to prevent brushing behavior. + +### November 2023 + +**Backend updates** + +1)Switch the AI model for user chat to Alibaba's Tongyi Qianwen Plus model, and **support multi round session history**, no longer use iFlytek Starfire's AI model, but still retain the tool class. The main purpose is to receive a faster response, and Alibaba's documents are more detailed, allowing for customized scripts . When users input irrelevant book recommendations, they can directly **refuse to answer **. + +2)Add an IncSyncDeleteAIMessage **scheduled task**, which will delete records with empty content due to system errors and other reasons every day. The number of times the interface will be restored for these users will also be. In the future, RabbitMQ may be selected to put the failed messages into the message queue, and then ensure that the failed messages are consumed . + +3)Login encryption has been changed from the front-end to the back-end. As the front-end can be compromised, encryption will still be placed in the back-end Solution: Frontend transmission, encrypted with HTTPS for ciphertext, backend encrypted with salt value+algorithm, and database storing ciphertext. + +4)Store the message page in Redis, **reduce database IO queries**, and increase QPS by hundreds of times! + +**Front end update status** + +1)Change the background images and avatars of the three login pages to be stored in the images folder of the assets folder, mainly for the sake of users of the project. Many people do not understand graphic bed technology, so I will temporarily change the login pages to static images. + +2)Optimization of permission switching prompt, There is now a text style for login permission switching on the icon , indicating that users have multiple login pages to switch between. + +3)Login encryption has been changed from the front-end to the back-end. As the front-end can be compromised, encryption will still be placed in the back-end **Solution**: Frontend transmission, encrypted with HTTPS for ciphertext, backend encrypted with salt value+algorithm, and database storing ciphertext. + +### March 2024 + +**Backend updates** + +1)Add @ ApiOperation annotations to Knife4J to indicate the purpose of each interface, making it easier for developers to read and test the interfaces. \ No newline at end of file diff --git a/doc/swagger.png b/doc/swagger.png new file mode 100644 index 0000000000000000000000000000000000000000..0df005d42577ea613d5c7bdea71ae7b3e4432c48 GIT binary patch literal 263468 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%0Dj5~J9@vk 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 z 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≫hPJ7ZaSwOrgGqPW)KTam9v+GlN`ca(QV%{y-|%3~CpD8`KB@8#?##{Z_A zne1~BMhJOUaP2Qu7H4@qkSLx4dHpf)c>w~MST@s-9PRey)8PBBlkq5BNdbQ4KqRCK zfGEguAvEZl773#TVpQe1CBTG{j%6#H5=8?=p)1gZlVucpVSut5o)O`3wdHrWO6lxY z!9(NXejK#-CjQs1E+Fju4n{+XZ6XdGa4T+f(G*Sw&I*Y@drU(`#Rvoscd`=s-FTdf zt>xY6si>fUtU^x!RafMJei|t57I0f0$ik*@Uk7T1GIR$EnAU8HuY750>IC{_Z)|e8 zxsvp~#PIO6O;5PD^W0Z_lnBrS&4z`A0pBTUop0>sm3hg4DDWL9eXM(upB=K+8Nm_h zD~0Gf;Ghj0NJa67fgsDz-+wHLeyPxNwi>ip1M%}o(&+)kguXa)=708e28Tfja7L9M z1f^rCv%QJ6lravAU83z1F`qoZ(!6PB4A1 z+_3!?$^f3PHs(i@lMjxSp4btf8;0_ArkvWG3aqCD$QWSi1JTvFG(YEvf2&bK1h4?12FQSs+Rst+3CulOG+<0}bt6 z$Ly^42;nbo#Bjeec(s>V1cf^|j;MQ!i+U5P4%18zP zWeB~eMLEHF+!J`6mFWb6cju#Zs2@Je0YY6cLJ4SO*p|0OM8N#Q@30t_arN4@5_Y?P zW_kbRm0a*eO=|+VV5`^ApNnWDb@F20nd06?K~5YG>2jF^+WlGYTTSDj|O1!ltk}HSoGe!RREfD+KA%<2_ZuRgT~}c`Kygo)F_aq;|c6P)mXhw!UhAhBA|Vb z)>9ajmh~9m0!}*`3oD+F&4+S8{`mSGR-rr0!q>??_jUcQS=H<)L$8BoZ~KYpmh<4` z-xBt47T8H@Yik1x30?2?L>@xS6m1nvni&(g5<5VS#*gcd17=f2?qM#ZNswYd=2){%>_bLIXfnk?*lFt`nZ0 zpU=_Nk1L(zu^V$Pn<9t=kn#AdsAMg9ug7(xF_ZwBY4@dv;eKs1+Y84!jErQj*0)!g9U=_jAzso#U#66GTl- zO>YQo^O8^#78c$CbD&XY_9=x#TYK$R7w_mdYruC{BM$7d4nG5N0X^%WUu6SWDZzT) z=ZV;Dk%(M!?gUz>2u4M(T%miFb6{4?SIJNGkeyY%H@X91K#n?Og8LqJ1T=P$jKw+! zKw7`OVlh$aICI8*3ev4|SV|@3jO*-b?E96ZQz-)s#I^w=roYBv)OpuG1cBIx-w?SB zpbM&i6HOYdtrWcnJ7P-_N3_F%Px z#T7>P!{pQASL97E&^a6B^a#-BY@VJ>oifnU$`LW}D++oZm6LaXmAdXZjiC*g_e`-O z-T}(cGH^Za;|^fS(vEm8f5uk1PwS6D0kA;s-OyxpfEp{%eUWes^jSI)8Zt5=b}~HX z6x+QT9w>7WtVAF}w5n3>KO^k0d`vDC!3x^$tA*(|2g$1rO{@4`Iz3}x={8I?m!Sa^pb_-GO)_fC-fd@$IPR{Fzi!STR+E@3QQjN*qazN%>N{gKqt0& z6ZWU9W^T`E5Is}!GX$Xkw4WA3a<&JxBKHRB4UgbpJnKu3rhmiYCDk16F zlpGo=n<x`vs)0?_1sBV7>&|kzM)K$)C%~zsz4ypbvuJurd9Jp1tG5o|0ZO#iRC1-M)7O zqMCk1CkO-G&T)40dow!|=kz)t9duRAqnE2Y*~ur{y{%sBUhzQMRV<%JAwZnF8_K^^ zVj5vzl`D+5WZ6RGd@n#jsRLfV^O*Jg<#E{gHge$lwk~aBQ(zm~M!wXphP52Q0D0Ob zaAfc$6DaVEBo7FiQe#C#A}NX3`Z%j|<320k2-7_`5cIJVYr|WLQ`3m0$2W9Pwpc73 zv&x2KN2TWB$S;-ozMz$uC@G#i!z|T@g+wy3rsf6YEeEPsAx&5K$EvN!P0R(U!j&nxGA2@hJbkw#aCOKp(YHoo?8B=!yELyus3f0KcW^5DTI$1x`qz zg0fUuJ}lf5dAszGA4(b>#9=*XqF&KPVfNzb4zf8bb`-PtVKXb4R;2>|YOE5e@9$UK3EL)g0V|Q~rJPD)*O!{hgLOcf_rf1{P;nj5 zmoQ+GR{4N8_QX?P%Ceiyxa%&}AKTwFWpgC?<*77b*SwRGbf~1h5P~o+{OS=9xPhvu}}+p1@(Mnd6&uu ze+I2sC6Iw*L36jap~e(RtcTfgx57SrNO2@#EUkl((ev<(dfFUw$X8IKk~4)qM5zbJ zMbNTEXDlM~lRmsFl13Kliyo`10dI|5VU~=!IfFvliR@)x+^UQ8%22WJWiit!{@|Z-9B6@m*D$@rHf|KC$c&ttWKXw}PGQBa zW_VR$VK*>cB#I4`k-laC=98xx2kfWgQ|g)V|$QVf)8&z3w+kAGS8itWr({=K^&p(b1l&^@zmo zD}#kV%Eg{hBrr;d>_7z$7b&%10(%qAN%rm%W6KGAE(-mo9jTY(Uy02}D_Kb#z=3z@ zsefQuVux}AF<|!#DxR5M|s2_rxf0_P34y^UO z@Ck(rgA19|Pk#<24{vpZ8Mnpl%*?NC!lv_ZtUfR~rCL1;-3sI}ELlQ?^+$@ENS~rMe>j?*d zbS0XCT{H-x2KLfx-^eELK;L~lgc*EjPybMB3L!ED>R}Sa$Z+yk-_9*etZYZ-y$LtF z$GvI^*NtTAn{N(k=Bzt*)g&h-Vq_hKP`3#fnv*{BJ#*3qYQaLpZS2t+nGb6IBK~Eg zn}eP-y#B>q)4JA1xUEk^-Xi z1`McxzY!ADE)r_>NjjVB#H7LEkS}IZ$-}ar4gk?;&b!EuB1v8+6&$@!-mle#>@NIFH0+;E0s|F2Vq?_Ri>KaYGoJPa@UI`y!TzWl1_ws0si| zEf3|I2a4AidT816EZ~-kMYV;~QpmZp<5OKwI?Jh33I;sq)<27GBPo;fg*-5H3v<;m z33ieJoOFac0PSXq&3~L*AHF-#uWXBJ-p>g(r&~%5VyF)vKEP`-gHd-6Z(HxwWZsVA zcwL;oo{8=NSCJkcif;TsDGG=Ii;`nb6=sxKzNp+|?+5VFlWyJl<(!GV0cqdzYr;zm zdmuh8rD!=B-lvXk1+h##r#j^QGsCYIfyn*lwO5A{DynS<&S5;p#>VFE`l`p&tN|7k zLH1dnqjtjvXVOwrOPmueAvj&|f++2Pb^6k5S9Klt4L=_9%;(SLIjDXSIVNwOi4JSI zpZthi>Vql6Vq@Xg<03TZ1qdT!V$_cKSJdplMg;`X`JZV>aslyxp5M?7u62Z1OB(!i zos?t1XNOQz7U8JbfuJwfiywU9jA=P-()>(!HtV}%ZA&|Tt3PKMzp%-xj8B2`iUtB@ z)sHtNm$UMvEd`fvJTIb3t*)+qd+iP@;7Ucut(eZ^JVpU^voBzzHlK(PdJYnr-Ff`- zwTR8TTXgXTa{;T&EFnbk?4`TKg3$4g@+jGZ+*D<~si|pI_QH#)b5XTWNM_qfj-D(o zR`xb37p}6L7P;6yRRK%!3^e6m)}PCE_wa!11Kg8VrQv+uUC-;LBW6hdRI!*#10rwQ zWWde#*o9UQPD56uC`~YeP44-3>Jd7Eze;62J-``wG&Ozu43Gb3KDgZFq7vQ<4Xh>4 zop+=27u-nw$V*mK@#=O;hKEeNlJS*1XQ9#BXZX{%xMc94h{iM%Ojox6CWri#cq-e0)&m(-Lo~wm4VtneMSUMIKQxvIq>25-XU%s z*!-(2-E5u4$X&S)EwYF_TFvh(gsbN2)J=K{^7_3|&QVQq@*riny3#Y1P*y2AfpGN% z6d;kT=VK5FE^hZSb3}8OFOC~XD0S&(@*R`+-mBECBMX|+Tb@Fgs~n&2lcV3CE3muI zfJLRdqp~SwN^jT%kYzb>MY+$g;Yg}mWG3MaGm$vDY{^|ul(4*+fozdEs z8E1qIN=@jOKCmH1i1|w@HH%*z;)w>F;3CNAw9H-JB4INLTYgS=nqC$FF`aLVTGHN*dp;4drjW)lp%|~y2$%0IIX&5{YkRq5 z2_*52dcVxK{Oe}E-1g$OkXp9~f;o|#2E@rIT*QmJYyAgwh!@z=*6e;}QMB>tneBj| zqO#Eh(+v660}CK?bY^AW~ zUI+MrLBzA7gg$SMl>R82(Z@1Za!-qEEsP_FNcde#WazA*Uq93eqrAvW+;}}2=w;HQ zkEsG~2#bo2?Uf`rE<8oVuGQ>K0-&(!)UeknX38?!ghf5m({NbbA=&x_kRY=ABKsuU z{Fe;9l(DkeB76)>MRrsIC+Ue1^@rel_kkzy=n{lcz1r$o-9^Yd$@dI3784H1$px;r zP8&w{7HBo?_nY{|zZvp=74oM)^QPK*Yu^t|F7I#}9s{0UD zNhbhZR)jx`{Pe50Spz7((@0PFkoEbRWnd-ud`IV*EJOk+@Sdsw?0 z_#f(^O4}ooK@LzMi(+i7dHSrlMeFyPqgZj|KGZ4R`Aac{G!}}mw%0kjwk?S4IjWof zg4t>5Cw;1CgKECFfWTjVEG_Kkgtt-C{c)D@@=GNQRv;6= zBLn4lLXS~PS4g_6d zhj7AgYf_fJGt~-^gXr>I0(FRt&-SIp=OCVk?K&qj<7+*sXvjS8j-3hT zBz<^#bh~0#Fg$5t+{9ddTy${9!WxCA^hkmkb8Dx{ z0?7P2NxK5_Q%SyJHhsJpixII|V*~?3f`vio}SScE^(q&*;tzo34?I9QE0Sloo zhjo#ojX=+ujM>1{S&H8=nFT~ncPmW;Qih^mVA!VwEIf$%QL$tJ6;X=+c`f?6K1H0l zKiKIS{6!8ZZ@>ci#p8p8BtWd?WMq^sgZsz^oR~;bW41ryFw?O@2)_XI2gExIRvezg z9Bu538fCYh;LWZfaLGo{PP^uUbcLfW-{L1 z*>D<>nRO%vMO|cwV~y*M-5vkRwRZh;zqx7mUNS$0(z;y_`I$9Bo*zA60)@;SZuOtP z`^BJ54!AOqqqm~V{kt#u+u1yg0eH*E_o0Em`|y9Z9VEj+g%EC*ba~~s#rB^E>dgxN zhMs_e_@9OMf4P$X_Ty6s0Z2*?3g8d<{_1xB?L2MO8bUBassP{m^OwJQ6#)!vlw$VFf3-&c zoTmPnc8ABFO<23{3&{U=xc~g`|M-d)I6J{uH*9|u-v0Y#zO2N4)@Fku(B=QlDg5nJ z<*32m7|zV<{qIKxGXuqU_$bY1*Z+9n|Ka?>mQD)%P1~22rvDd5Hh9K(MaaI{`Pi4iRZI*YMg z)b1DSA+%uF_hvDN|79NdHu~pOS?k2?`j5*fNAJCnhOTy7{j{FQpXcJ(H)IJKSPy)J zCG$qMnimW7x2U5zXI5rWMG1f9FaP8H{_%Y8fkZ)tweX+*KHfJkUn6CGHA+xtMlq_4 zjPE>k>2ElSpj_ye(d=lW;|li@4+bhFHcqz^3r&U`;RJ#EiIa&@WLiHumCst*d}83n zD!_7T(H>M%RN2)TCI5TV3XJDG*vDq`1XheFKc`(b!hEUF0Rugm_2S9y0`XdfdAt$N ztJH6S5ycF9@l^(`&xj7-@{MeuRa;(n@_sNQrm`!cZoQ28sN(A+=a(yI->m--z=Q-I zMg3JG_>Th`u#cb1BR6J23Y=^sh6F6O<>#_@{9R<{QtDp&?`S>PX!%_$7?1h=&1Pdy+*%2 z>5Qw66_ z@7n!b2DAR>q02XpR(EtgjP2BFcm<61{luzf)03EN`?gQ-SgoG>n!wIFeGANimi8bZ z5{dr)>3^W(B*$J+s*@X7lxM-OaicihXhG4@xhd>vKPtaHl%;>_=>hDN3T*BBKnrU2 z2u;0RcGgpVbR`15)ThRAp6J12)O#J%@sSn#z!CO*-Pe1}uCGMd@*&;(-3v2NeYFJv zsSYhn_zION?R75h&!WBr0d|%`|AW=>YX)z#TLG?{g*v5{6U*U#N%>RdtxmkHyiSX+ zzP&!tU%Pu}D68LEXTKOlh>d)4&t>S8S|`*c2G|bf#ntckI+x;EW(RNGvJ})9ctBEW z{dlXC=eoIi*FsXALxTZaB~zsM)@1iMgobZpF5Au1$tC4dkeP~#uXzAgU{v{^nE&4o zXWa9^GzZrb^4;>~)4jV6_qdNLR46YpPmp&>@So5YX}k`N-cS?U+dpBSE&0?BO)hoG^MAS}?gezj5JG?Pl}S{>V7T*(aMTKco*?8gFSgXKVRtHBQS9C>;)H zxSveNG-2UM2VJvVgtRN!d{iC<- z9uvlF9wj^RM6ukcrkr(Z{ zp@JM-Vjl{!ncKOhXVuHlBzx0w=6(4k7zZ-mgA>o)c8-{b->2x7C!HpEm^{$4J8CMG z+6PF=mor;?+ZL}J-yg2#IFA-J2^xPH5vlE~dFK^)7^IFHe`kw{P`&!}mN>faJ!jc^X)vEXs#6#CMb2`{QMyNs^ScINa|#l}dAlW$Z8)pmX1v z^TW#rzRk}3uG8ykipkEwhm*qE*}Cles1)tkug7u``S~MAq>AtE;t<4(cOf<6U0)Wc zoGs??5Oed>{0}iRJf(hcRWwrF-G3Ja_@^ayJ0$uLUMJ6J&qW(2nd#5s!a1+@64Cy6 zWI4|jMT^XEBZg#;**}%oNVH# zhkUY=6?!f3CeQ_&^9!STQiRj^K(*oi>klj!e5$L}Mfe>~TklXj8jD;;_~QYZN+3N*IOc*i60q#B%IY%}26Jf`J*==5B_I zv*i1O_PT=(#*Gafs(#K9FE9J%x+(7?4vu5HTDW0Hq)>$6 zNcPQA4cF4uG3)L1cVVvN!Ie`7Lcl`#lRcs~+R)Cv3YJK{VWmlwlsvY*gC-HMIJ(jF z2|#W_Nb8O%uHt6*_uc7!#7?9l*fF>Hr08*TN%2EwrtfQ7#1v&}6lDq~(~WQOmS+}H zLfzUfWFU>2t9)Oo!8hnTL&J2QCJXrOY%MU3*iF`1zlw{G44Fsd1$EV3E~k2#9-EeG<0Rsr$LS5 z{*b(q=2Mex#8g0?x=l@EZ+ui|oZrq$6yN>ifa7{iHEz-(Zw}~_*qiWtq%JT_lB@ORS4sQPG*GyS!hD+>*2IJ<%QN;10miTH~`n%oJH@KOR4z z0if^gn5&WR6pTp&=RbeHKQUf0Il#SJI;8xP@jFGaGW|k&dk9*cd!Y$ysOieMF3X@K{IJc&**8 zTU;(hP;0*7LUv7_bF`0}32T-QI#S6*oy*_{+W8nLLh5oYeGr zNfX>ZFg}Po6|`6>L0h=*X&n`ft0cL9)rb4H$pE%{#)2CJ0RZUTW8X4g{=bHYE-24TP=VrYQ`q5Lbb|NdawLuOL?4wv^j%{YXln=}MUYh)F<6>BwJ63-`GP2^^ z;1u66hB(p9QXF0wQV-K+ggk7EDC5;t{`ev*aXdIMJeSDV{kfZiv*zvrWftk(F^SDd z8n|MZ;f&7xl7cMbiw%m4St?C-%FJn2kg z31B$`s0)X6yyi~LAEexRVPg;Y`FFODHnVlm>-ox5M;l(Ju7}$(U=_dqZm~?){-Z6^ z#)`Fap0=BD2U$4HgfrC5@VE*+EH3%Dp7P#i?~<%63B^baR0m(N?5c?cw_;di?WAz% zssZIA1FPQJw8kX@(NeCGh*itIr~2Zec+|IKPH#6pToWt1^wvGLtVD?s=kiWjvx5xT zaN-)9tkrm4$IZEWT}d6ovs@z02Mia=HyUr>=o_f~H11pF#Di;4d6U;oKTFTJ<|sIh z&-114i}S6n6K^9f7(c@PZq(rSLJW&0go|;*rU6U8h@@H2>Ik)c2M|`s7b4@N{0VA{ zPYT@Ftfu#EiJxeI3tv=(V@;ykK^XtM{<`-eX!JqG97$SjBUbDrkL&QZtRU?MJZ>jb z+1-gnytD2m;PTJ8mwvkCuNl!-a;QJXF8hJ*rhL<)?Vswx|C$Sg)t--1=YcFkEfimM zuRWP^L7PO$CoF6(^EfWv;I;cwce3@&-w8D^P>(D#6k8|jeL<0MR1ZQS^45-^Rf>&) zCv_~Y*0Lj6f^K{0q{mhkLQ?`OoL2B^w~i2tDKQ=Cz*Y0F#wDi#|(5&RSbWJFR z9-EjEC)dk0a!@PUZ5=x1YIV(X`T0FUW!iPaGq>(BX>=__TFz%M1hyV;sArMeYh_z? z(XE`gc4dzM#?3P9p6GFN3U#>F*VS=ubI`Tn^AW-2A)=yEJ2)>^p(Z#Vb5tX>;~rbZ zhlUq!3qBXD!U?E|_2IZkFP5xpoO&0i~gFk~TjBJE0*fw5c=RcCKrq(XGUC&hSxlns#GY!R6!k zts_b6t<_vBYo?Ql^KU75cOqd=$2`VxSqTzjDu#j1!}s%i+IbSyWnsu<_Cop+0WWuE zj-7(QK_fO&;(6rD$cl_Wszt3UT34CicL(PwZgXZbv{2mVGzKLBCQwJ=*5BR8T2Pza zb}0)GMxckjIeRR#DcP3yFHB+3vXWkQ+-4~v*YoybVRG4H)={QDY`HN zL(&>coe6b4yF*T4QHcWIhAxkd;c$U4YEo7vKWC20nwpvA0Ba9*bF#wHMyw!dgdU}9 z#qY40e$~s3SGystecF4Rj6yaE5@Je`*%VRUP}W0qM{qv&JOd{&#N-5rW?ZA`uU-Jx zSpd3efbv~fg`xqD(-I~a7Fv9QbcQtHeG*~!CkL0k2co&IxG-?bV8`+pmaJY^?_*`>g? z7PeM~*mupVB(k-(f;i6-0RVRAwuQ>hl+Hj-xwf3-qRN3r57Y#UYjR&-5G*cz$*}AQ zq6UfM!g#Am7Q02V-kMl*m73%{{EU6W3`W5@8CE(ACFFQHY-Peyhi{9MD(iBj+gf$2 zQj3ArH){^6s^RWD^Q-#UW$}ub5}%y@^8-8{u?|QhR~bpO#o6+zB*o~D>=p@I{<*^1 zwj6c!u3bBNJTmT{5-Lg6KHFaekDe^VySkcOt^Y%(Kk73gKZnu*k8K#xZ5ZuJH~DNM z3o~2iW-c8Au1&j8LaU?TwtR@-^Ldx-AjhpM>1$(1ffCF1W<(xQz~*h1>gk9Zkav|1 z7U(6xd_K2fONQ%twXt7O44Mw(r5>U^t$!qNIdC;Lna2JP>yFjEbNumn>iLbLAwt_r zTH5Bzy@>qRmIvmcT(maZu-il~^lAI+DKAF-N(WyX-fbyCzri;$n{2yZ1IRIT2`>`q zvZMS*Bpc$tX{Y&+eP`!n51=QSi!Zpv%N-wK#)vbAPv|KtNQtlSEZmOYU31h%%6#h5 zJliWWL0x5U$k!pgzZTX;n`H!sjccU2j(W+c=3wXbQNG7yrF0-X>drq~uM;l*BKx1b3_(7Pp*24d)q54e@J(|CBM#x<>qS!3~_ z8=$hq&duJh_b!KtJM4uF1DBJOb$gQ=-h%Lj%pe_$LJd&Y0ElNrBRcwy@GLcM)k{NS{F10(+!%Mxer+D`M zyh(06Tvkn^jC3*Fo(&$9dd=Hci!%J|oO9k44|o<<1BlFq*5X7$$G<>i(62vzI+shv zs@sf3bg;YFph7P6Ityy4@Cq-@l1gm}gASjO&_wP)ZdqihU87Pz| zdMtlsmvU|wXX%yVrql0pDeq zsuHk_HYX$|b*T0RSeoU}Z?BWmQ)*{N8Dhj{p+BV6rI+61v)$UV22RwV&l@6zE%O^H zoJ|@Imys*{vqzrzk-B(BflLCZDxfHO^9dy-ct6$BIHFgqYr<{QHFw3@Gz)mL`uEpG z$jO60r)Itp9*a`4;j#^+D?$U8o1yQkU+|*d@NwpaS462i-GAr*S#+331GmdKPorAQ zF^H+IFRJ<=Yp-9w%5=_1w-;|D*a8*iY%}kVri!GcvNxPFKZ>i}KXyS-#B$Y7H8$31 ztR7G3p)fif6NuGDHK(ob4?7Q>i?gC$%k!Vsrjf7mwhdL?o3*ien#{iZ(meZ{Z~Pap z+LhydeP7?|yTtm_(x09%Pq&57l$4K948*v533`R{oYaUMry>UpHKINhyDilTiqSt) zqxQ#)h9^cMb3a6jHxy0PxMtEVRB@m3E-cfm^o$%1eO9&Je9<@-V}4LV#$`2HZ@XKA z!;17;Kk2(kBak;18g@L&EmFU|WSqo6$WGr;^-fM{0ykJ9KV8?cFoX8^hUWXHeplue z7QBFkl!TCrn_DOedmG3Lya6JsI$)d)SzZDLY$SZI3MMNn0)S*VU&A=dex%Op6i9_f zbt0pqIe{NyBm;9xHcGaFFOkdkK?-d&>u;J$fZhPYLP+J{7qe{)2$S$O>;lK>K>xi^ z3Q{ui@t$@)Ww|hqqp=-H;`~u!cJ@Bq4{fza_iNl%*S~689+yNW+LoRHCI>p(RbXvO zDQn!~TiQ~xA>^zu9hg^|s|wG;e4?178sd>|8e9s}!{D~ygVdg!6y)Jy&4K1e`y(z3 z%LfkJ?3*#_;$nU9eRqxQ*qyJWBM{4FWJ$dJ7PASwc5UNn6l4r2E)ev~&mIY4-gs zE-8NHU1rP9Rl(Jw6N@*8F)OOfkN#rGY>3A|B~7%-DDFo)i_Lb@7rgBK=uc6uBRy5^lgWe<=J#eklC4|Do`Q7xV+aI)Rh@ z*}K2cwqCbG)bU8US4bk zXq}lnFZ!G3qcu?XZxa(cuZ?qC(wS(ym$2tQJ=!|F?BnC3l-7Pc9AkO$7k%D;ZOp7* zdXqdu@lJsmB^Ofuv}1tt(qWub3$pX5#ix--fRy0g>!*=Kw>b>fPirxWP1P|wwoxRCb?#=Pn?!3-&2-UO47y=tahw(LO~@97JAo>Q8^d4(KI!Hw25zLl zW)m2s)fjgt;qmhQfS9S zJI?*r>g8+cJ#_=Ek^_6p!I5$x%m00Q2=y%WPJ8kp~hibbj8z)m{+XGasMx{wJJ`8j)SBy9CvD ztfJh6{_vi`Qeu;t%05$#Suu5gpPO3@+z$0RN_zk&5sKV80SZkZ8!Xge6|oPMXC$!T!hiT>IWLss;~4`ajd&Cq?2;K z^$rbsjc^8_TQm&D-+pLwd(ONa|RgIS>J{5I%v9c6^FiJk8k&>jYD`Dk>y+%}b9z(sg6`5C}`X}H9LSGLI zrPPAqgt9@v4J?)0O{mb#=?vEcM(oR#%25=p7vQ&1`H$z}&}H96Qw!dWtor_UIq=tU z_}Az?4GT*Vk%#5Mq-H5CJ$bQmAal+1;=$&DlJ0>!>(T~b&U~viXK7Be#^;RbPBxQZ z$!fW*-8kWusPySucItRkZB;*83j6p+sl>^cP*IXa8yw_wE`tU1DdscG@}l2w2=A|n zt9kShja>U0*SMdVoo&L=kKoR{LCefkVf#gX7KxfvSz7ia8HN`#yXBM)Jma|g^z0g# z35$={=#k45x_a`^=0yGI4R#W=c70RyLqemn9x_p_av@4VScyiaE;(;&0z@yaIDIqe;-AxEax zpHyaQf4FdA1$!{D*1o^PKDVNKFjY|KH{t}uvFn9D)tY8kh7Vu3C>4c0;Bs|eMXs30 zW^mNzZ-2&+{h`c_Dx5r~uTAR4P%VFql!Zewlx~L>v-#*?xbe{b(W6J-qL?Q#l$Eu# zv~1@(rRC*4=Hg9IlMuhJj*m_;TfW!^8^|mGu!yRDJMUf#iN3{1dD~D9+Xj{o)XEZe_0=XfX2tyOKwnxtv=l zk;+j`phQMS1`YKZ3ftK%x(dYUE(hO-WgXf6ER9P<58N~+vXPZDrBU|}=wE7d7U!%M zCJt|}7h8?1gvYtkuo(}Q`i8^{rd5$sk&?iEXy9yAVATf zZ4@h4)=JCD)M_NOm!bK;6m%R=^IVH}wd)oZ3_!;OxU#k;$kNT;@!X)8_DqY3`j1Q| zne`eFC&q|WA8d_U%G#&UFf!hX@CI@*Q&F00^i%6W81oy$^JukbFY*nF->r)lBJgrH zR{MsyxSH~V*6_<)0-7(uLIeG@EM(1bN%rBS+lCy@i+~T%PTOcR+YZwZ$vE%ZeVrH| z>gRp`R#PShCa$^wTX(U*ufnYVKB(*jCC7K&_wR-vv~TUL;>fh3#zGcqraJNBjf(la z>5G);pFt2Uy6AC3`X+sPll(4jjRbA#zer2O23-3oEfL6MT(}wX881jV*jxo6qXv!V z1YJ`9n56ojXWxsN1{O5P4NuLJ>|IrJ*GB@4@560a_@Zet7)*WTTT2hKgudm2-eM}b zsRu9eFw18LajQE!x%yd*$|Hu!$nd-`TIn?((?J&3kZo}j^V{ZD`SN@o+Xg5CC(p8| zs|Su6;2(iJ_lbUF{kCv}g;%DJ6h0aPOF~TSl{ZZgwL^(72J$R+FR7d!HiHWn9~C#p z7Abd$;Vst7q!c+!%ruhGc}N;5942O|2)5UcrUNM_+wa552mUxiFWvgFIFzbrDo|~uEa$%yhp!`C%$O$nv})4Wx-AJHEQzo z4^UfBIE4^2-s1@h4o-Djmwq8xWr%z3X-_mlCB zIx@gAI=md-9!RH>$Yn;*Syght)IMct6M$COm>1S6kqi0kq{v4P}}4JnfoHfT`-?Z0SO@Cw*0( zO+h2DRzd)(D@R97B*DlTqNA8_v zM^3_W6u5}uvNKL671(GdN;yv~qU$<*G#*e0YS_Qh^$MduymD%M!?s80R;l^&hmwN% z=609*UXv+0J)W{!mG`(YQKoNQA2g)c^+^YQ&uL|jnG4Fa5I1^U&;x2vW<5yN`ob^V zd>jQI%S=?R29g}@XHX7YI{NWW^U5MhR+}oGXFM&6r{?rQMk)-Ygo6|=JlSda* zTK1eqCZXqU%+-n5Dk^S1ic#T-w+Gy&zvpg$s#9H$fhYsztII~W-3~tXc-$moD6-)9qC1fFul>Ox49HG#P7{cTT*!$X;dPhspxZ5a3 z+*?P2Yywnqp2&IS$Fgaco|vu_-uMSC_)qmlXK@NeRCrF(lbEiyz-m zESX&i;E9}>A^;!paIBi+*3pK@5$7Or{8%fWH5d8j6idIb2HjfoGJZ{2I|oH}GOc951$5wVX$$OtAIKBW>$nD2wpxOkzyfeE-Li{MNp+PL?YU8TDtqs~3nrqgw;$nDvpkMUVP z2~N?by1Tg7N$onCg(1w+%wS#a!a07gD5e>mIVY+1v`j+@c2ZvMiOYbUj2# z-Lmdj>S!H(x=_-pr^Su?Kq2UKbtYS(8z~n~gLDr^r#-YEw;ztUj^+QlX3hH?cTEl` zcPZKWrQa+2L8k7CG`*l^*i@!$iAVG}j$}iyKp!-NSY1iZqnabdYS@EBls*Mv3`6_! zr4>eu>&1FgFhTv|g_xZM14>0l_h@e^UySl(}fexpA&e|pXT$aLp$p%p33m?%a;B2iq zV;;YQ+1j@wNupam{<>(Mt-A2>DX=iV|6!5(_<7EX?FK7q%XwMiQLi;0QF-jQS@OQi z!n*hL#1!d}0Zql5;58U6A6d1m%JOlwS82vKfs#nH7UBmdEqP+5SexKN4lb)PUK^?U zjOR>kIfy>$iKoO5}%7ojdeYuFD)RTCfk@w?6u7ifDg(y5?&x=OS znRf5p5cra0i*c?*u9hjFmO$4S$*|kj=Q?FCTO%eXFlWiY|HK`X@S2(eH+!UVp2k#5 zAZQucB`LhMI$w#&v5s(F?KalZG%~zx{Q-UBvRXlFkoDf}uM$e9wGZX3XIifk_EFH9 zjC^IwCu%Nx)!HZ>`Cx?v{cV5_&xosy?B@5=dg0*6|x=aCh%HGtdaoop7>l82^~o zhZ`12RX>+4RFm3lzx$zQP4j-JB9Ru2vJTfw@^$tZpD|*NQ=vC!FkWYB;XQ(Gu)88V zm$CebNJwliyhc;E;{O~jPT+q_R#_`>ugV_xxv#-TF=h1v!DqZzQy7YNq#dn?Su=0N z$7?nPQcGHpA>N7x7Exb)ZTmcJxH7bT3yVqdrWa^s1=Zur2Crk*(wM(_$nu=YBM$LQ zLLKozAt1{5`-l4%WG=lktR>|(Cvv7Xx-H@9{a*P!rVx9s!*D8>E{Jr|!55kK>5h)~0vJudINJHPRN^5) zoNc_jMlXE)?AQm--pxNlkWbSmaS?H?U{ksUF%rd@jzzuV`hAf&SrTC`c_FCr@z{Bs zA3mfyewK4P)QZH*&#zrbeRR(1xKoMxnSkso#ewd~?b@H`R?+N?(|1>h+Bn7@A*xDo zlap{d-nQ&MuYC))P(v?vV=-a(Qg2mnNvyzU7WPI&?hOqhfy;W~i#GLWjC!wFY_FZU zhvu{$_6E8%c_Sp0xVZ2_gQ{jcf4sTWRAg~(ay@W0(2}t*koZB_V3F@R?_ITX&z5iW zZJi-+#lg#N?F$CBKkDbm@ksi+s{CUJ90P6*L> z56^5t6Sam@fK@zSd{H%?aVjz*w^%L#=e_|~fH;SKOG1P;O(zkKBs8gq@{)8_)7WPi zt?bC`h%@hST|1IwYy6Mea|pR@*B{T*GT9B?v+?kd*VxdyH@c;I=30S=1(zL zjuXUS#1B`AiT{q!(n_(y%WA;i=eiP2oXGTX5*M_dmQeS+YyDESH@+>3QQ!Uh;FOm1 zb*I<2KN1XGomS)O=vfI5SM0;~6L8*<^CNDEw3Q*gj1`Jr)Q33Rk(s#m;{l9Du?0;*a+Bdf8^ zy~;tta&TphUu5vPfymc&P`_va>5cX8PdU2{=mjR0Hq=w%y6D0+?L!AQ`?eKh)9WJ{ znWOX0hN8DIP84ilrQW|s=iHU~%pl?j%pc~ciFicAPo_N@Z54l-PpH}m@#U3jFVa9ndBJ4(7Z<=7iA0R9 ztwTn9EH*AoQ&y5(QbZ>Z6b-AfusU-2!=t#M*VK_DF&m zPH?AA&~tBxp2QVMtT+D0kWdHhoMa43Q8LTl&Rvw4gu1rbKk=L`kD1l8xaM5Fa24vs ztVAoCbn(Jmj=OJ*1N7coz8}S8Ou}1ut#u&4Mv_~oZW{DTRBt3RZ!Ht(RJU^sGgSbEC5D`CU8*Xa4 zaUYs&-Vt&dck_Y!d*(op7*S-jA~-F~cBedmivFFnVxXu=an$Rpj!GZGBBeGrT4}@1 zI2X(#EEpNseb8TfOeYG|^Iwc&OZ{-Q&&{~4v03e?uRQGH^aUX7N*o5NVSfQhBS6Md zPCdWUP%3))TS4;;of0Zk@PF*Ci$(=E>tz)tKiy%@?y36i+3$SYZTa5{C)m(9l?OWw zF4MIu_MZM4>6}h!C+Uu`QFS}dKTplf=f{y-SLIgeQqy-m?GC0`zUm5@a&fyJB;j`v zUaHx2tDdtl8r}mW{v#C+ZZ&ufobuD;w_j|&;Z5?fdVOlrAtM2 zFmR6;6~I3uh(CD7K|oS=_a@DR1QSXiwIJ=C!YPG<+;_^>B_$C7Mo*r5l`X$*nSz1F7J{ho?^VZ|hdxy_AYTzX~qaSqOo zug5VlmBw&&OZu@5g&VJd&a3fC{Ug31!q$a_ou12MPzN~BuLNn#jnVpJ+(*m5hR^%SgD<%1YjarS3$^ReYBrJFzrI}7 zGne@TlqJ3SHA!^sdf~}X(7i&&rB}@luZ*5wYIv{RH+*E*@fh=LaW8=U0B5h%g_|JK zL|ZVz8+Rb+(d1=cppc%H{-ycXdBWx}y* zHTfS>FF)z>pu&o{#((ef4Tdc+<2Pk(zr}ffAQLLyzZ8FGt;&qAaUP}|AHM=v(Mr^H zPrta<{#qYJOSByS*UDmJ!XV7oP>gHr)Z5L&-sr4Nx%$-V{b_up$o>@Wmt@C@-dmUC zZZdB%^N%<+2jJHqDr}GmIz1|P&s(8Zz@tnSa&^cfjsq#;mVCzq4(5rtGBFDlCNjx0 zUfa`RxV@=%En4?5oP>JVI0w@92Tz`*e8nsq7|1lju3gDG@@C7j%wh<@madv}Pgynk z=n|9^xqk+hjRe3a`9I2Dayl&4P5d@2%z4@e(qVG0TnRv|@s~^LQD)MFt zegimUPzk>G1^*W1Yocfde+g?^niyy(q_rnn@#U+pu6`yIEfGYIl){K;$70!$>@iAY zRtF#=wEKmxub%4)_ZDYFq+dt{dzZ#4$Fq4r5ymxBVto4nEh1g8Rvs74sTPA7!b0D+`S~hLtkWb7n8)3I6TH&Q{gmKq6b-#t4%4^y-6LgCA}M)x zQNt4;Y?IDs5xnR0oIohPW7&j{aVQE)@1bqk>pHhS$@=lI25wvz>Zmj4N9%cEPqDZ9 z+)-iu9Q`Z+`4IrFS9M#T#!C0;Dv1Xia|X2fJL(=q34faT2Kr4dM2uq;$i)3)i>wxSe{4-U*naC_Rk@l4Q`d9_h>!WD>55r z+S4hT!05tYVKMIpWq^ zbN^@n{mK;mb}Cdm zs99TVq{i-FwXoW1CRF$=$y@ZH&G!eR5ST!qgC+IW7}xM1aF*KDesrZ zzN_gZ451%X4Jfnm(Dnf3FvpC_Ng2humvG{HJkzyX%LojCEOI>n&K*F|$75+nS}jA9 zGwWgyRY?>gfQ;IKYhQ*rz@wUyJGGkfb^00}1?!D>lu~2j|LmSj|HUFe*P@g^zKLqYKyvD#Lc3Wisoa$-37 z3B=DoTg|a+$&eF%s^AoW)1+o6SDpHtwJ^h>q@61y(8g&-J*46jGo$(cIybHUeP!nPJ z=1D}vVsH9caeC=BpkCPoOtg|g!Lu>o#r&;=MP%@mS`dlBq#1kk2FH}ren4qJ{W3#K zhqd~XY1WCj4Tcn)iC|l7Is()Ey2*o3W`qtBB+Y$aFx55LBdTw-5G_V1Dl!*iJ@mQ{ zRId9XMRUD(wOG)_&#_qGjZ))10xUpT$nSx!>q-Tv)fxd3nq%5jdq?7=w_m1|e3Iwk zj%r&_@{$G`Q7hBQ3uQCc;|9)?rE#66ooS_eQzYn@W=i=$dYZG6aZrPShP+?8m{+%e ztR=S9@tT^){i*i29iN@}#aJ^cBF6W;6A_%DG9eze&iQIhht^ujIY6sk7g+>^It zzSBg!H1y|O0h=@^&)^Yz_RkHVCib{x4$8rL6KN_h7a$vua!aRtDLoN!wwGoi)PIRk zl47kPewV&p5m!aAH(e|~b2BcWa&pMB9ia=R^h^rVy&HiP{98`gEir~TwEk~*&yEnlNhMoeuKrA1!&(zILP*lJK#C$f zw265>ByNeQ2>A+A<9Qm9z^$jGVi^dR!g-#DOM%qk8cv_Q&PqMAmK5$qzlVRNao;Ad z$@lB`^jqWVqF$Z&)Qzpn<%8d@79E;*Fq+9SoDr7BrC+UIF6|(2eB$~HT{_~4emX^` zlN-{t$x>N{(PGEfmEd}SAxiTlaIgRCWlkAEMffuszuF(D!3@4mo4UupPm<_@hQ#1nrw}Pv0l9@ivyXC3Vb1Jq< zqLda2cNgp+(vm2vT$dXsaa|a22Y{hK-5{RCBxwB33Qrd~(hqgTROh*P7^D!+Q6v;39W=g6GmzT>#46SMlx_3wA4DPwe|Y(30; z(8O`QK%a5Uf%Y_7j)5K6)o@DWO`_h+vTpt*h!wN2q^_zqDP#oyXuoz-%rG0sA4owI zx*gnJfL6IEI;?YXC2o36rlIspRHiBSN!Ju-S~`* zgEInTnAmIh@>uEt={>5$a{Yn+f8BDWw!!DthykLEK7yFFAFd_i2Ts4@=-;A>R}R>k z2=lrHL8YJHH!tE96vlIGxXcG~K(*3lRF%&N?RxCJpsY9s`6Q`(%*0rdF&Lbg{4QIu z3VrF9+&6nw6oiO{B;E%p*eUc`+9*dl!>4~orsYzc-dZYdmwhSVi|5)|~L~Ma|WTpNNjM#hF zhwxXp(+<>3!(-OH4*kr>TR@5W7HAHq-wMmH&Pf&)Jp1>9_%9!;bdp6Mw>^v6xYuq|3%q$d^XShz`)}atUp7Ce36X-DP%7h}Jn6r# z=uhY%3x1`&oN?Oee|{ZMl6eA3GGE$$|F}OrjBqpfm1`@3yj1_?r2fbEVM5uiYu}tp zj}NeaJ05WDdkOF>w_RyM|AhMe$0bnzZKs3u^|o6|!JqE^_hpV}FkoWtPYIa$@0RCZ zFZRDn^S?{;|J_p~YL$i4`t2Vlij2M(*gsFn!+iex2G3zV1p~H}3Z%}BA@$&Ed2`ckj|!$-jB%lzL%{=fG3f9>!8)2?w)?h^>s zYL^(HiOAZ{pa@YQP%9=TW=5{@h+-AVby^#E^H81M1lDKl!GB{}vNL7A&qr_MMmV=}-G`ul5xn zn9j}kx7KX)PrHM-wF3V_#w0Kez5<6>okHW04-CKKC&R$JfA%l8BpDWLgv zsN1PqsRXiQhWUkY5#W}I9PRX&?LOr>UkTljt{-D@PnI^PFKpKXxe^GQZjBxX%E_|t z&3~*-p@37MPW3yg?WWTqNx$}Z9&SK8SIi-+HuLYn!oLJbsu>WT1PyH6`S~*#Xa+ad z2(!7A6y^a`*#RmG=VEvE?f7fJMgxiVWsqrw1|WIvN??kXxo)pgy2Hg&pkxyn8L7(V zbAXx~6{Df&4Bt4iV#F&ZURk%ixOfeQh<%v{MNcgIzWZ{CJYC)0hj5`=A>rM zH2VTWs(|9(g*bcB4o`U0?nTJYGUy+cfcZ9cRnv>le!qf5FLV0$8PZYx@&X~(qyTR9 z0O(SIZ#KGcLKrPprsnFTdL2?M{boocdP;RWfQw=@L7F{Ew=TwHl_oYftUJ z?Jvc&dQ?@ds`%hvs%mdhb2eXSS6%rgj4&R6iUMq3pv0c}maLfwWBx$GG4AqT)CTQkmJ)5-!(=*W9tjGP!zQ^RQ4K3)hLi`jRDJ+tP((Oy z6~PGQP{xBYYUQnb{R8F*n#yd@E*JDu^(xf7+xXvi;j$jUOwViAMB| zxt*ESA&jWF?NYAa=mDUoj7v_E!?h9U1dX5loU=1f^>FeN^TKISvBa}gZjqrub=3=5 zi7-H`N6AJB-n7F#tg`_WZ$apnfxRmTG?IS0*hmM>gyiN#DMx&H{!fojOI=L%*j-q- zbkbd~aW*P%V_rAIe8%IyCIw#YG!UVe3L@V9N>Ry}UoqQ5{RiJL-M|eRDza*o^Ge=^ zMv}Gn^e9-|_M%r74&0M1^Cyr4LYHG+Y=4#Wlls%Z{(4E8=C2rSED>ag}EPx2L2gBmHG-Rn=M{~mghMQR|X;o^V zj!DkbQ~er5!rTvM1Z1&E3K3UE>N&qsUl+)idy_Qb`Wa`U%Asd`-C<6#{`e9B88u`P zr>VTff945Z=PvvJ1K_zgI@tiX)ypAVF-y3NsgcWqKp2Kp?yc56gt?IKE;{OzyBEv( zK#g2JiT9f~_7&dE_u~Bn3Pw{kD|)tUI#1cHPmFayJCC^f0^ret(*d+i=rB6uzUq_) z59#83Z+cCKm$q=-a;fa5sYT-K#^PW;$vBE`U;2Imwz#HDn2da{xPbW{s1PGbVWQUx zlXDBrV<^S2cM!#9ZZy|1$v#ZUN99Y3a%Zl%y05&k^=J)Gq>uc*Q`NE`b~(A9{%Fes z=h{7k^Ww)HIj>|`QSkiUH1oqm8OpepB2P z0z#nt)6UYdR(iBlCQ@kF1evo%&1dW2;KQ{K<@?nEV94$=GG;5 zJ3!HbUMRrmKynB4mXIqiH~WGVB%p})G~yw!5`e~!Y8~-0X%pE}95lo{5AmaEHi%5nu=V9PDS16B_11y?S$H^!ix5olhD;YKDJ%L)0}Ty3TojZ*5g4 zW>^1QmrrK3SbEHq+bBCYsPmXyi7YVyiwft>-SDB{ytyOAm+rrPXT>yr6nfjOFzNc& z*#KH$A-3XXfQ90RD@^XqJsB-gt+>fR&Kuw$7x8d|Ne)kNFHt2)nK7A5D2)ZYkW!() z5P`MFo{fWk6~HW*`h+@i(D?AW-JLc5x>+CHi&i5~vD-nGcZ9b-+=U5{)H`;7pqJw5 z@zS+3pzvPRH^?aH7`pXF*)S37gC>k2Mzvb%FeTI>a|ThcY30t*(W$zVO#vvW3U4Hd4d>Y*p6xiq%J zkIha5G*G;)@7s&NJp14oXkxm>pAFs@RZn($2m%W7T5x zOScgGEi!KV=kB13z@RqQt>v1ID|Kt%Awmnjh@)Q5WLI)Pc(HswOWL%P;2#JxhZC34^LKs7%hasixsw9;QSI&7fx7%#1Ct3AWvQjJz^)3s7cG<}{Hnw+1_c7$@>YRxker zbtY81Z^tT3du%B>1bZnZ2U9o$Oc_}@btRmYnOVk#t4khB3^i57?9=7Hzmw6audHlO zSK2-l#hB)hJ(z#>E9CiCL&ls7ft<|lC(2)z{hseD;BG(z*O>Bozep)ST9pbotW!Uv ztp9*Hx?iAyp-87DvK#XT1e{ML=V4T~XzPM6p*+TmYUmPEmM+*EdZAkf&R6iZjO07< zsT^pF=uF@aj`@)SK>O+QyauqG*(bS^fJfJ^=KZ$V?QrKyFQH>AkL(^k_Z~z-ky|%+ zQL`VVaV@vysmQcmXoePYy?m{iaKG}eBt9qORPYdyMr?!z^11p2<$w6npBwhCxK&XL zMKzCQoaOx)4N?=L1)Lm*u_4m8)HOAq#l^*KD%vq>WzG-VjNB)y17;DRu#cw|B!W@n zWb=fS+5*wmbM#wM!0*OM5G_q}oCwnBO5(E}tG1ppfRV)t?fwE-TZ%t|aJHVp(DEk~ zR%vII)lriEZ?*~^IkwTq`RafCQN)%ypYlk+LeQu=0GQAsW2x2Oz%%wgJ9tNF+ldM_ zpfQ25@?(jsrrsLVjwegpM-oxzQj{2vNv2`hoMA$Xp4^;aosZ$TjoQu%@x(O|p?5ES zLVg0$kI7|Or?()GF9Y9#n&L=!ORRPtBGZ-L^miXkQst&Ho6(8u{bl6Ax;}WM6uX5j zb14}*Dp;f5bwh-XGD;BR7Qw{hdkFt`i(af4yiRMMlq=a~%V5c`wzdS5e1(nx=+k*? zSv7uPDdZVZOFkABmeF3-ZUrO{(T#cFvW#SD9teuXuE+{Td-M$CszAAWVbk)D)t$cq z8$}Nku+%e@@{7rshgrN&ndwu_k_Y1hd^KHBlZ8qIafo9Au)qSFw6wHjC)vo{MBuTx zK%p>>U4H_`c0`J@`oCKv=doxFDW;(^ESfjf5)0#Az_5qGJHhE=y%4~9BEuS($VxfnTH6oCaBQ7t&QZu&kA8g=OOrV}4FJP#$x&21K6*xp5 zoO;_9V!t%Rw$RHZKxUbcPNgjB)rYN^u!@?zS8O2Gr$WwcE|FOaoupT@!G1HFQ8-wE z7l#H*h~VxH8RaT&J(`9w1>L982HrmC%aG0^{HVYt#1IQTs}Eo-+`f64AQK_JRvYAh zbJ%-KDmv)xn2pDXc3v3SaeX{JCk@^(%C;MWPDdR6gZj6>A$OR41 zeLPUJr->$gWEjJDN`bf>Lp5;s)SPGyqw71H{R;8+@itI_`d})7 zAl-nIb_jEV`WalXUk5^4ABBJeDr0wuhT>=ekKQDGZS(hRcY@128WVEL*JeIL4Jt` zrEHBPZi(DIe{-6QEKPmry|FGyFDZf`+ogq7Iaho6;oG0=#Va zE0Q%iI0Xa}w}9i-_MqtH&E*)vTr^2lD0XH|)mSKQJ?wnOL6`c0fk1{SYEkhXQw`Hp zFPQQdcPfGo+hkD{Z@$#~y0vySpCe^IV2Ie=z|~<{_yLm(DJt!bje1cI#NZtkY0yYX z%GR%H6pTOE9^2X386$p>gF|}taVZVZQEtG2m6j-pa-6CsQE=(0w+4fAv2>$Sylhzl9>a$fpSIfM5aLj)XL$dh^|>jx;iTC<{AhLBx#j zkELJ|Bmh}kj>s*e1t8nqwH9*W1rSF%;E84H9VN`LqUcDPXfihXmJ~38$`V1vh>|=Y z(|&@oXFC1DZDB)Pi>o1u!r>vRGhsXqb?w*#UHR5iC+!c@Z4a%!+t`$q4G~Az9~CuG zOzQUZe0X_jvSH|GNh@2nf9y^TH&&2rgHG8FJ138zu;bl=_&*$5>JT`yT)bdTe){NA zCOJExqmJ2hut9&5_e|sAQX`THM$k9F8GR!Z9peJY4n%=0G8|~^t#8CFZYT49hs^K( zLMmW@p0COs1o0V6M&Z{=7gTGZoyi%eC;wY8 zdxH}UHVsRW)cgW|+5}haHq$`#4-cUcC;x|KTa>AqRm~|lr*Px)UeHNDvA(>WzgsPtN@RaI##u-$pU z7Ws)#cG<6SeZU)u({G&*ObEnfdQQWpGm$rm zyn*d#y7`vk;q6r6R z1XmgVJ5P`V&g1FFK15}}xTLlINouf2Ji*c*0zI0;YX$wy@leW#u2M%{;(p@w!MO%F3#ZK?a0Zn)N@)AQF>ixOrhDeOlEB!^VHiJ6YBFqRkWI)Gq5saB(mvg?{7_?x}mgpz2h$8 zWlsa*{9n!5|C$#3!nAYG9DWh0&E#MHCtl~(hp~3!0JvH3El*?f94u-y($L$ET2`Ua z$2IJ)%T9BnNGFh-$+a?&$1=`U%|ZIjYfdBW7qEb?<|X&!)&l{QnwX9KMK4GInCwI_fm zB~c18J$FmBV2@rQHhmj*$rJ2OR224THSjsW?6!RM)(Ut}aEU(~gR=iTg)g#_Kc@F7 zlbRyMSBO*%FV`D5(NV`w=5SX)L_(tHGzAvsuFGHK&8#TCD05pePETe<@{>2@4^nn3 zzYFOfe0%x5cPlw@Zm_LmL{YQ!Q{5$7dHCKcv%Uw(J#`Y6_&B9v(SiF6jhKpSxz{95XNfVi71K8^ zsD>ktz~@1D<9OTA?L*c{zC6f_w;+E|ENC2g{){4%B<)i_d+i|K-PHa1B=Q*&EApe3 zFtd7win67l0;T%QEY*U1_6C*_O$oW&0*8p>Xn3^a}m@swi!8H2p$KB0^sOrY2u0QOl0AfJVbbX@{(ZoDjiea;g zZBlYcyw)(WywidxnnqpiEw;1Ecl`Mg!R?3{iq)FTPb$501=%9IQ;uH?u!jcnW1`eJ z>uxGpw^fPkPZGEe-g+x=>{I+Lrp3BfvPAy7fva?B;%L$0=lL?A;sLIMp<8m~6O^%#bAY?+7AZ`QQle!OzoNr~y*PB9k!%PVfPkQ0P1`EP z#JqoC#3pzuxbv;H&~;PM_^Pz&i&2JsWRm>to;DF+%KqJ|L#tTF@Y1lUVQxEL`_N7B=iiR|Ic4!Mj zquyN+?o4mv6aoQCKx81)O090>a;0)xd#!C%Lq8Nrqoyii6mz{)_$-wZwG!10r70JM zjFY@e5En7Ljkb$#1aT~C`d2Hq55x?DC9|Y<9$j`}@Wj)Lm|&7_8Kx$Dds)Ur7u?%| zMmn|v`yP6QHio@r#ibkaf)S>iRFuXBq-!8cBKr*C60iayTk8RY+x}evo=SwlqM8s( zVS4%Paa`gsI-?J@*w*)gf{RZe4^e)sPapK5g&dUC;)oDY+RJgl5=5ND&zZ6+J|LtU zmA4SjN_k5_kW6~iR{&F_;^R6Wl$I{J%wHZtcLzKSQzi1rIWX{|VQcz^?tLlb6OlfGXg!*=tAOp&kSdm$6Pf3L(k&$#CMFge(84-==ed7YEdbSn z?k!>0^rgu2fMtqeD@q(ofi)ZFLZK@(jRCYl)PVzy(1f3j=$`t^Mn%&!yvZ?oL+wyJ ztmhiBjZ7E-zH~?EMFAU{jx{tBCDWjkH!7{}AmF0?TBB5~JX=(Cxz=!I{ex!7Y}rMj zjbbSkcN7Tt2`$WK(tUgOt=6fpsLCQlaeXPWGBO~@5}=QA7b?iE97Do`MZhX8{7>x2 z293t7OWZeO;A)>!%3ynHlf~z_vp(BN30Y{Ty%?uWIP^Sr0)g^M^(Suw1_i<7t3!Z9 z=aRQ9?0a1$ijn%9vUy71@`Kw)->*x<1=cRv`{E8gwB!|)Y4`3bNslLdtnkzs^xqdCoC%VM-e#d+EA#3IQ}KELzw~FXCS9m*pqgBF+O_|ZAh1w` zhJuOYjnE_oRw0!-2g^Khd|a$_rDz1Q8bk6qGCQuH$3CT%u$iCn!zPGy`AMi2r|zna zbwfuI-h&Hn^tfEc?a`<_PKPM9SPs0OQG7NOS|TnP8G@cqiJKn4;7xCd%fu){*4eaQ zUf>Ia!pkO@Tnud#!o{>ew2H}7?P!&>scAN?t~yl<z%=GVzoMWW0Ur#*S+%UGwBco1@G8Kzw3Ks}~I z-l;JE-Z*=RaZ6AMzD& zflA*=ym41944l-1x4(?v5ou`RqKljF*%V3;^ zipa1)^`oC^GM-iNMVbBfjm*B=yGERWCa6Ys2jq#_Z&C@$ePP85sKOQKpfoqxLjl|a z^oz7MH&7BtYp+g+@lFRkdXZp*qI4(DN`r**a~@`!&C_F^B4o0K?DXP^6lwgFTPSoYsLKb(Y!1JrxI~1 zrriHxX_PXPKt}4ys&4>kswIE&7SU`_a%5bb;|D|k7Kf~zgz_k^6%<)KM*eZ0M*$%} zR0pBSYtm^ZWME^dKO47~DKd>U{??}jfJqX$A6(d$4BY^*@Tnwcn^egzRSXiQ*xEv| zwWOL#@}v)6l(MaFrtsb}XP;^_7xXlV@>lll@O}y+dRAJn2KkbYN2KSzUIi1G{ku(^ zC2U!yv-T^`?f1%GXqEFV#jx{q>FYL!>-})+Z@aiYdcD17>%GeZj~|Bzp2U62Pu?u8 zls#Bo3`soN`Ir?%SvmZV!1ABbK9ORYGOAQM5Z3ehAE);3=R-$gz|jxUkFOs}{Y~!2 zf*)I|W8hFd+2O-r;gTU6)umCt?A2y z#My@RvbPjivg@{GnvM?vGa}sZpuQIKq0IGPZ~UJi5H+g)mBG5I`p+Ms{*{Auiq-YH zQ8UK+Z2-&f6dSyT_(js05h?#U9&}Y?{A#$+j+hYdnw&w>U`EF!O>_Xb2Vzs3;Nwd& z7+P{FD~5E{u70Pglr{J^GX0#rV8?iyKieUvLi=SBdX3iSr2C#~o9_!k(&rBFR@Pr@ zQD_v5?jN}fE%b3!i4~Hgwa~N1Z^w~QP^H#!jCB6#OtUMd+h=z#XgjI?Jk$==ua?bJ zqP9L3dn9McTlkkaEA{&FAo<^(OfyH6+Ioxmpve5Roi$2_bwa|}46Z@FG-f^U@GXh1lE1y0Gj^(K`@y4u6mHHPDpD$+&d>P##}($G#X$`|qh@`i z<~Qzws+`vsdCTaM??_V8H~9&x6EVNN1!dPW30v88Ikk_A?%KzO@ zjJhgsGHv@;@8{38Q$(PtztGV_yjw~8g^vBlM({c!Hl3*7IkMV4QZIiTgJixlwshP! zJuW%{zCaC@CL&MAc}3^nKh{71ZND*XM4necqx7eb5m!2-_{{?S=?bD=%=F+CWQ61&-u&ZxetY4ZS5Aw$+yo!Y=icSHdD*;W(aaNq5u3^?WkM2Yc%uO~^BQT2NqMog<%USss4?DPxe zz$dk*SBI_~d$a-|ND4Bj{%5@WZ=U_QdydhE2yoKd@ju?E$aV($zj>i}2Fy=(t3CHNkHb=10a4G5^e{F{iAjNw}3ROpAw{zb)@iyWxd}2VeeVtKN-&--yuK>gVS-Q{^VbDk(5hYg8GHHQ->#e1hk?>X2AYN2QehRT_1@OX-v7nk zdq+i;HSwY|D1(BEN)nJ@K%$_MGYSZjksMnEBnQd4MFj)|6a*wTL2{A|4KyGEk|k%F zoSK|--_6W8lD_ZGy>H#O?pqK4!qeyMP`h^Rs$bQv0yU*U#KbMh5O8%t3q&`8=>2ex z7efwn#q5UdDd1Yi-bE8KZfl96JWv`Wu2Fn+WJHF+6A#svEHVu0M(9zu#*uJaPcs%y zo7df~GMk);0x_*1u|6}r>Oa^$z)v5}xEFWLk8$|w^83di%2%uM-L*>rFc=aPg8;FU zIlw4;YMATsXkoIycnidlXE<(vsIg1VV(N8i#yz>b4y)&+C9>|rmI)2thi(m{*EpG& ze7_NhPgnSCD!3REa2WZ9tdaFZe#zJF6gPaH#(*s~_qVg5>khS~sZDXo;M!b@cuVPj zKxZ$W(Z)HCbpY@HUW{{d!1v0rv)mHnkudQok<1DtQ_30#87wye3L5R+t4P^$ z+I~9FrsczmYJ1NZ`j%War=iCEM{#{V$KTrpF;yw}0l|$GvWFB1?F_Dda7||ipjaU5 z`B*}XPqd62$;Z8@Opxw2#kd1FZCUDhK9@ESaKs9xl5VvkZ>wDQ^pn*PNt3!tel2Os zSN}(xYy&;S2)_3EyvO<8V|~^yF+6}wxh#43*Y}q&7~J!07oNFgf~)VjAYOFm6Ugt< z0d9-XIg$F(eZj+AH4JnZEwkFnOH^Twe+cj#DeuSLI%}Jp?Bts1#xY^iCA9Dti<>L1 zik@@D6j2jn{92~b) z3%XcuG0{6(u2w#_HoKqKjg|d<3pk(K6G_L?8RZa3wP+y=VKII-YFLOEqS5xUowP?F`j5_Bv&8kHm4T zXP*@PliTZn=nRko#!0M$_66kwax8%nV=ty`FU%bQZn^<6^Kmp6gr@Cf>d0IkD8QEg zQcBEUjm)uL&7AWRff@xTVCNohpDEnmvzTl>`wt#MGE|u0B3GqTs!G-^Wf3N8U*PWC ztd@4Z+{bCRI0B${p|$)}ki-GF7xTtV?9S~L>$uLP)1-X18T@kO#N4A;=N|K0Uq=KT zx&^j~liUS>>tw(S2f|1;L9k|(H>r}c9+$Sm72f#f zv(v@mQdJtxDKoqd3Qb@g==;{mz8vJszs=U*y{kfa5wElkbs)<4BUO#vT=r@xI6J_V zIs|ZPe4YUTxXU2E-gz&!#}_W=QbY9vA#2jY)fMU~KIW((U+{2N?=S)WZ20?C7Yg*; zg`LC7#`|ClJ3Bl6t**u^q)`cgiJd@Ht`l%JgS7x5nH)kh34nWntJ)aczN1%6iBtgv zOE%Z$AYhp~XBgD;za;3KU;fOAd|>m)L}KrOKNz@8Y6r=klrLay z4T@V7tB9D+_7mt}#Cl}mSh7EUa8jpcod4CVaC7kBR zx{m_)!c>p_q$!heZfbnzQFR);?LtQhK*`y%L$mnV&cQkV6j~2}QhCi4r1?D{X8y~6 zWeE0o-!*=M_g{Rn4PBjOeb_KnbZWg}wrr$-Y=`O)_p3OKoL(NyqviQHD-9L--Bd=a-_bFPrm6pWpC&{mAG}e&qlT zB6SW`EP?DyYSb|eDv6H>IMTT+u%tcCpfy|dOtvc8^fBo;} zZ@>T7Kb~E@=;`^?)1Fe`fA;2oYrAwDM;kSsPHQDUHC|Zw>S&(=oER%hx<%%2^V^5V z6Zrtc0xZ7x-84g{4*x&sn2&dDU}$x5HnF1Z(T7KgIh|b*M@N3z zfQb@cVT(B7;GoW2Oki@MZnt)_vNjAcl&TT;l(29$xN&HRfaWr?zk?gqi}oCc=yox_ z)PnTZ=T#@!M^7Kwy?~=Uk5avK;&f(AVV%O!QI$}=FY=s3s8tzv*_ohrU5cFqUze9y z<MYYjhL^s^ybEI8LR}f7{d)3o#k0tg^%VqPT=#a=|(UrS&3YQQ3 zvNv{?PcPFO4ZpY;1|4Z|UmHJXDKHKC$dFcs9v%BJPe>#LXp+kQNYnVQy#8~z88~Nz zEBK!7TsktwgY)@wEP8hU{r@1%w?4{PKik_kpl^Ur3AWy>h#WJeBh!v7JN+h_#Au+K zYCjc!QsD6G7Ykp?$WP<7Q~akF_BZ142i`?bgZ~9$cXg7s9&EWawiIM(oys&3#ygHq z187Wpd;4AJBs%%_fTV02Vcx;dk0Iq>%@7dG#~o$pce14=kt8?5dX6C~Xeu(XIy8El z@IN-XZwW3^UJL+~k{@n8_irfmJK!^CtmAt7y(PbJ5Ap5f`=AaK;blE zC(9p+9^DW#4bV>Ihkv61qW;FzD7si?%F{Dkd{xpqy>poN(=pSz z30nAY?lI}%=~$i4CA*Ta%N>oaCT=ZlK*RG)@^B}BPX#`JU3clN@1d`Mb9s!RW*tFq z1*aQtmJw7S$vJy;kzDR#pAq)H)L?w*V*xE}qdv%19!qSLTV)s`?Whc%5(biA^H!|X zp>04(fZ?#^W}qoTVvxZ33gZ}V`Uo^1I+tPm!=s5*eqxrtF1o@WALTY>V=Qv0;k}v1 z5MA^nDGs1NI-FzrxtMUu#fYpmLFhn3dZI2M%^n{JssY!Ui8Ho97_=RTSxMOnEld7*Q(e`$v8;tCX4u zstK-~gV@SZ5Y{BUOA3t7W3&^%tS;tY$JR9MDR$@VqoRMt0_A52md2I?a;qPN`tEiK*4P_uVSM#(xvoef-4{_2(7B0K2Q zzHb=6L{$PeIMd;z!LRl&;14s5M|%1do&-v{eMlr4n3uPZ57)q*DG*DO1u(Y>a9h=j zQQTY<*1$4_`Ji0yw*^NY{E53tuwcTS05!GjK8~ zf8%aK+987Kp@ave_yRV3n1qa(ow(o5|Eh#7`jQ{*~rBPx=)jD zh*iOvvM-E>?yf)^>K!(}PtQ5$&UOvYT_vA3spZ)oeNpURT)oxp*msPe7{CS{(u-a_ zaJ0F=FYm4KS63W=KM+T43+gVylQc5UUwdD4%V38H?1y_jxaYR}py7PX@1?t?5Ug{s zJQ%OlHU8w}(Y5CzBp46;k6G#@8lZa%8>t=8^aONHKLw%e4y^d3x(g9vPo^=J zZsm?FNj}=0B%*-ie|!sHPH7=>b9W2PtPg%Pv&+Y9t!Z;qsl9kbwiMMRqW$9i0rE{f zqO?=l%PQ+9BM*TE8{;K3VVpYXBJr=ivVa~1S7Thh77yjb=cmK}=h(-{I z)^iL&2WBnMca{NMDFXTF3)E)~0`hWm0brmxGfm@5ot^{-d?H?AVgR6n7)RB~+N2iQ zNs=(KtkNKdOmr^7!P-WA@lDxaoGy|Zou?*Eg**iFlH^QLBg*mj(m8H5}y_O zX3#HTw71zkF&8(s5l&lv7!LOKVo(MV-yqJ!aE+>JU`i z)cpfr{k8s=e}}$UGW^L4e{EJP2CyBcKx*S1kTa^grw1fC>F$~|{61wHk`cHo;#E1b z=}$|+%Vi~K>LBY|U+oZmPx5uSWL3ksm(<3wN=wd}Gr}5P5eJtcG7w{DX9v{y+uo~S ziQm-I;{b93wPuny;N^RkZdGBqFG}KdGoujmYRC`ld_e_<)`n{0WAj~uywq-K=6U7E zOy|ryVM^}6(B4BDb`p)*_WA+{Pfs-~2X|m*`U=^B)eFkjQ&*XSi?JXLS$uMG@_dLm z2*i{IX>ECfYBlVp7C!_A1_Easem~!xJ72)Td_%-X9T8{N)ZGo&Yl_az&8@`}hd?(n zKQ{oh46@6SThYly(xW*GXT(T{-kbOGW!mFKR7vM*2YnTP!OV29i{sqLG;(`(y)8@C zzeA{a`6)Ur=5>b-6Yg=U`+(n`4RHFv#t|(nRU?9?DeCy?+Jn3WX@qQ>D;2QhK8HzL zY3W9QOyYpj?mlEaMRrSVv%s_14=&YW3(Cf(mu!daXb_6BK3M6tj*dbwS+p|mPO81e`r*)<>qtAm-`>4dr$7- zA-y{$PHMPo;nRF{;~bN9TN%WrS;{wNn8&c7zZW6zMXrA5FBM02?g%|@s0ia z$E6i0xv|zP*vIQphxO1W3_u;%lC{7+{KEn1{txjJ2`+ky&FJAjJpSi@-uvs1QsvWc zqAR1v$PW=A@+oE}?Yf~DiGf}x)QE~5z(pK3*8_3Web3cWuR55EQ4 zn34t!>^_fsc4*X*xuCYu%;wZ|dr0g1>CkCS|5RhKqqL9o0fe5?Qx-d{dNp64?{z!d z28GCHYGs*^kd2jj4u3+IF>_QzZ9^?^kZl1vq9zG=593Zor zG?0@gP5+|I|33)gVx+8kfZ6}tLLYB@nJFy|3?I*(rReNoXW>w92#-Oq9m4b2r?%Ap z(F}gci^wP-8%gZ9Lk>HcDjDWyVBy=lOpU%9tFrx{4nuk*quz|i9O^&5?@7%M@#!{) z_QaES7!dRto`R?l0YT98r)f3IAIHH=r_+Gv3f}zfh_NR?>oP(n%Y>eDeXJxut)6L` z=}_XM@uT@p)so|R>6+3F}lAj1SU z^vXS2s{@87Sl+~oU`gedxB!e;e+mEBy8XlYk#E4!aQ15akeP|Gy}p((<*Be-p5gNUGh>^xNIZXx${YC!Oktfiz11;A%I zKo)~%6`o%U{qhucMyUBYt*1LdN@uxczNC@OhK7c(Ua7~Q@03(vxoL^t+&i#p14$U5 zDjq`(9Y`SrG6^|?>wkMCeQz;&?#3HhZ|69N@yXMTh$B}j) zI||Di(ZyRFJfsxbDnM2r3FR} zZddO*I!QwRGpRP?qrRQkUuk_^#;5cK zWKwy}wSCaBrDV6Uz)qSXc&D$io!4PIy!>^aaj<&{Pnu08yIoMsr0Owi8w0Rz5=_6N zFpn02s3u;{u2*X6?cf=#KXzwJq+rC~sinmZ18kL>Mr+6M-krT@gYS$!2X}(TnUSRp z1OoB7)dQ^Jr#n%M28!A-XX`>c{TkV+HsT?Uj)RY{Px+Nj;*<-POLmRbeYS?e^s^z@ zI|my2lm7DR0Aiz;GCFl&VhLfTAeRKFD>yT` zvVyp*QyY7j01xWSaavTzbxM4z+xD{ubaR)Gvp1RE+JiV&O;D#7gs)}iTCN3i(aOFTJ01bma!$xBD16XSc@@ zIN}kUA-(rJ$-%QHuio0G`h9!}3zuA?;g+fA z)bw;ww~h~9LPDt;7SnKj%!+R z`eyl)og0o#v=sZf;OFU>zXwqmHd!$3@AJ%OS;`G~F-TuR9Z(F?@bGUo`GTC_a`HNU zAH{s;00?F$*$S}up6+f>#`zDWR`|B)^#K`#{sV{SeMG7_6x@eEH*9H@<7@{rZ;UR( zBIL|L(MM^F0Zt*1^rRc4>;|WXrrl`3UKVqZ>g|yPU1)s-xDO0cCyM%jTrSsXw?Kga zwE}aU?kOc{?f*6O?1NS6-!V}K&hi|9lq98=^c^5qQUf#Xj9k|Z(uYrqa3wQ_k z3)kiHS_gs;PX5o4kbpRXq^QbNmHiTp<$Wn|=B$HUNOPST0K-B$f&?J^ zAW`J!jhi^EAQrK|zaPko#t>F|yLkCi9(p;}7+X>ob_RX7c8yf3+|;_`Y;rAiLN~FN zxcNvO$HFZ_Z6eY&G^}s^G!c)zO(JU z|8ra}%V#7U7tKgKIT8A*BsDUp4sg+*+tkGpn9rQ|p*}TF% zX?IVfE!HR9FQNZt_G8^Qd-Iwtv{J9X^uMD-*T#>NtM_U;W_!Z?`}|3g@}p7@C~bfB z+zJC)_)fh<#bH7jV@4?Vth9BdvL;^ZV(%)x1**&+CA0t;pmST(myd_hw9mo=1?TaS z(1-->Ds5!;#e~<#eC!GE%QQ*-LtX#ovW$Jx)CfM;{sP1z^uVD{Op!kZ%!I}2+1TqR zO;lKK1Wk{sEzv!gjynA1?-+Zr2^Aa)yoWhl;gaZMu~%XWA99b2XjnY$=%i_<0q35sj{+|s@vj2sY-z{Z#?hB1CMP}Og5ZYVLZhclw6Xu)N=JTt$u1nw{NkW9!+>f4BGV?; zkN8EMeVp)C03dGHuS65>d-e}69R1@A;H9#!$Hapk-7mknoH-5RKTewdM{J)E{4b`R+nCj>bm$g%qDKPu}9hn#T835T3mVE+h&y+2OW z$rE+*M4dcQC-+&Hf1)!d%EF1VaH1@nC<`ab!XY_uLXZ=JoVa}c@!*^&3y1876M~!& z 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 U{Rz#r`U~}=uA;46qG%EPe=Ne>>Hq)$ literal 0 HcmV?d00001 diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..8a8fb22 --- /dev/null +++ b/mvnw @@ -0,0 +1,316 @@ +#!/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 +# +# https://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..1d8ab01 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,188 @@ +@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 https://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 Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..3ecc420 --- /dev/null +++ b/pom.xml @@ -0,0 +1,197 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.8 + + + com.book.backend + vue_book_backend + 0.0.1-SNAPSHOT + vue_book_backend + vue_book_backend + + 1.8 + + + + org.springframework.boot + spring-boot-starter-web + + + + mysql + mysql-connector-java + 8.0.23 + runtime + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + com.baomidou + mybatis-plus-boot-starter + 3.5.1 + + + com.baomidou + mybatis-plus-generator + 3.5.1 + + + io.jsonwebtoken + jjwt + 0.9.0 + + + + + com.alibaba + fastjson + 1.2.67 + + + + com.google.code.gson + gson + 2.8.5 + + + + org.java-websocket + Java-WebSocket + 1.3.8 + + + + com.squareup.okhttp3 + okhttp + 4.10.0 + + + + com.squareup.okio + okio + 2.10.0 + + + + cn.hutool + hutool-all + 5.8.11 + + + + com.github.xiaoymin + knife4j-openapi2-spring-boot-starter + 4.0.0 + + + + org.jsoup + jsoup + 1.15.3 + + + + org.apache.commons + commons-lang3 + + + + com.alibaba + easyexcel + 3.1.1 + + + com.yucongming + yucongming-java-sdk + 0.0.3 + + + com.google.guava + guava + 30.1-jre + + + + org.redisson + redisson + 3.21.3 + + + com.fasterxml.jackson.core + jackson-databind + 2.15.2 + + + + com.aliyun + broadscope-bailian-sdk-java + 1.1.7 + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.apache.commons + commons-pool2 + + + org.springframework.boot + spring-boot-starter-websocket + + + org.openjdk.jmh + jmh-core + 1.23 + + + org.openjdk.jmh + jmh-generator-annprocess + 1.23 + provided + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + 2.7.4 + + + + org.projectlombok + lombok + + + + + + my-app + + + + + + + diff --git a/sql/bms_boot_structure.sql b/sql/bms_boot_structure.sql new file mode 100644 index 0000000..a10d7a2 --- /dev/null +++ b/sql/bms_boot_structure.sql @@ -0,0 +1,247 @@ +/* + Navicat Premium Data Transfer + + Source Server : localhost + Source Server Type : MySQL + Source Server Version : 50716 + Source Host : localhost:3306 + Source Schema : bms_boot + + Target Server Type : MySQL + Target Server Version : 50716 + File Encoding : 65001 + + Date: 18/03/2024 14:11:45 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for t_admins +-- ---------------------------- +DROP TABLE IF EXISTS `t_admins`; +CREATE TABLE `t_admins` ( + `admin_id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '管理员表的唯一标识', + `username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名', + `password` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码(MD5加密)', + `admin_name` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '管理员真实姓名', + `status` int(1) NOT NULL COMMENT '1表示可用 0表示禁用', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`admin_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1624 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for t_ai_intelligent +-- ---------------------------- +DROP TABLE IF EXISTS `t_ai_intelligent`; +CREATE TABLE `t_ai_intelligent` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `input_message` text CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户输入信息', + `ai_result` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT 'AI生成结果', + `user_id` bigint(20) NULL DEFAULT NULL, + `create_time` datetime NULL DEFAULT NULL, + `update_time` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1736624313104711683 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for t_book_admins +-- ---------------------------- +DROP TABLE IF EXISTS `t_book_admins`; +CREATE TABLE `t_book_admins` ( + `book_admin_id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '图书管理员表的唯一标识', + `username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名', + `password` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码md5加密', + `book_admin_name` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '图书管理员真实姓名', + `status` int(1) NOT NULL COMMENT '1表示可用 0表示禁用', + `email` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '电子邮箱', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`book_admin_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1548 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for t_book_rule +-- ---------------------------- +DROP TABLE IF EXISTS `t_book_rule`; +CREATE TABLE `t_book_rule` ( + `rule_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '借阅规则记录的唯一标识', + `book_rule_id` int(11) NOT NULL COMMENT '借阅规则编号', + `book_days` int(11) NOT NULL COMMENT '借阅天数', + `book_limit_number` int(11) NOT NULL COMMENT '限制借阅的本数', + `book_limit_library` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '限制的图书馆', + `book_overdue_fee` double NOT NULL COMMENT '图书借阅后每天逾期费用', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`rule_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for t_book_type +-- ---------------------------- +DROP TABLE IF EXISTS `t_book_type`; +CREATE TABLE `t_book_type` ( + `type_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '图书类别唯一标识', + `type_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '借阅类别的昵称', + `type_content` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '借阅类别的描述', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`type_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for t_books +-- ---------------------------- +DROP TABLE IF EXISTS `t_books`; +CREATE TABLE `t_books` ( + `book_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '图书表唯一标识', + `book_number` bigint(11) NOT NULL COMMENT '图书编号 图书的唯一标识', + `book_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '图书名称', + `book_author` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '图书作者', + `book_library` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '图书所在图书馆名称', + `book_type` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '图书类别', + `book_location` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '图书位置', + `book_status` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '图书状态', + `book_description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '图书描述', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`book_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 122 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for t_books_borrow +-- ---------------------------- +DROP TABLE IF EXISTS `t_books_borrow`; +CREATE TABLE `t_books_borrow` ( + `borrow_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '借阅表唯一标识', + `card_number` bigint(11) NOT NULL COMMENT '借阅证编号 固定11位随机生成 用户和图书关联的唯一标识', + `book_number` bigint(11) NOT NULL COMMENT '图书编号 图书唯一标识', + `borrow_date` datetime NOT NULL COMMENT '借阅日期', + `close_date` datetime NOT NULL COMMENT '截止日期', + `return_date` datetime NULL DEFAULT NULL COMMENT '归还日期', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`borrow_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 45 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for t_chart +-- ---------------------------- +DROP TABLE IF EXISTS `t_chart`; +CREATE TABLE `t_chart` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', + `name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '图标名称', + `goal` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '分析目标', + `chart_data` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '图标数据', + `chart_type` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '图标类型', + `gen_chart` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '生成的图标数据', + `gen_result` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '生成的分析结论', + `status` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'wait' COMMENT 'wait,running,succeed,failed', + `exec_message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '执行信息', + `admin_id` bigint(20) NULL DEFAULT NULL COMMENT '创建管理员 id', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `isDelete` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1736624602977255426 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '图表信息表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for t_chat +-- ---------------------------- +DROP TABLE IF EXISTS `t_chat`; +CREATE TABLE `t_chat` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '聊天记录id\r\n', + `from_id` bigint(20) NOT NULL COMMENT '发送消息者id\r\n', + `to_id` bigint(20) NULL DEFAULT NULL COMMENT '接受消息者id,可以为空', + `text` varchar(512) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '消息内容', + `chat_type` tinyint(4) NOT NULL COMMENT '聊天类型 1-私聊 2-群聊', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `message_type` int(1) NOT NULL COMMENT '消息类型 1 文本 2 撤回消息 3 图片 4 语音 5 视频', + `role` int(11) NOT NULL COMMENT '消息发送者身份 1 用户 2 图书管理员', + `reply_message` varchar(512) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '回复的消息内容', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for t_comment +-- ---------------------------- +DROP TABLE IF EXISTS `t_comment`; +CREATE TABLE `t_comment` ( + `comment_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '留言表唯一标识', + `comment_avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '留言的头像 链接', + `comment_barrage_style` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '弹幕的高度(样式)', + `comment_message` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '弹幕的内容', + `comment_time` int(11) NOT NULL COMMENT '留言的时间(控制速度)', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`comment_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 65 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT; + +-- ---------------------------- +-- Table structure for t_notice +-- ---------------------------- +DROP TABLE IF EXISTS `t_notice`; +CREATE TABLE `t_notice` ( + `notice_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '公告表唯一标识', + `notice_title` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '公告题目', + `notice_content` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '公告内容', + `notice_admin_id` int(11) NOT NULL COMMENT '发布公告的管理员id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`notice_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for t_user_interface_info +-- ---------------------------- +DROP TABLE IF EXISTS `t_user_interface_info`; +CREATE TABLE `t_user_interface_info` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `user_id` bigint(20) NOT NULL COMMENT '用户id或管理员id', + `interface_id` bigint(20) NOT NULL COMMENT '1 表示AI聊天接口 2表示智能分析接口 ', + `total_num` int(11) NOT NULL DEFAULT 0 COMMENT '总共调用接口次数\r\n', + `left_num` int(11) NOT NULL DEFAULT 0 COMMENT '剩余接口可用次数', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for t_users +-- ---------------------------- +DROP TABLE IF EXISTS `t_users`; +CREATE TABLE `t_users` ( + `user_id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '用户表的唯一标识', + `username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名', + `password` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码 MD5加密', + `card_name` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '真实姓名', + `card_number` bigint(11) NOT NULL COMMENT '借阅证编号 固定11位随机生成 非空', + `rule_number` int(11) NOT NULL COMMENT '规则编号 可以自定义也就是权限功能', + `status` int(1) NOT NULL COMMENT '1表示可用 0表示禁用', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`user_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2546 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for t_violation +-- ---------------------------- +DROP TABLE IF EXISTS `t_violation`; +CREATE TABLE `t_violation` ( + `violation_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '违章表唯一标识', + `card_number` bigint(11) NOT NULL COMMENT '借阅证编号 11位 随机生成', + `book_number` bigint(11) NOT NULL COMMENT '图书编号 图书唯一标识', + `borrow_date` datetime NOT NULL COMMENT '借阅日期', + `close_date` datetime NOT NULL COMMENT '截止日期', + `return_date` datetime NULL DEFAULT NULL COMMENT '归还日期', + `violation_message` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '违章信息', + `violation_admin_id` int(11) NOT NULL COMMENT '违章信息管理员的id', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`violation_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 32 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/src/main/java/com/book/backend/VueBookBackendApplication.java b/src/main/java/com/book/backend/VueBookBackendApplication.java new file mode 100644 index 0000000..c132849 --- /dev/null +++ b/src/main/java/com/book/backend/VueBookBackendApplication.java @@ -0,0 +1,23 @@ +package com.book.backend; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * @author 程序员小白条 + */ +// @SpringBootApplication(exclude = {RedisAutoConfiguration.class}) +// todo 如需关闭 Redis,须添加 exclude 中的内容 +@SpringBootApplication() +@MapperScan("com.book.backend.mapper") +@EnableTransactionManagement +public class VueBookBackendApplication { + + public static void main(String[] args) { + SpringApplication.run(VueBookBackendApplication.class, args); + } + +} diff --git a/src/main/java/com/book/backend/common/BaseContext.java b/src/main/java/com/book/backend/common/BaseContext.java new file mode 100644 index 0000000..765ef7e --- /dev/null +++ b/src/main/java/com/book/backend/common/BaseContext.java @@ -0,0 +1,25 @@ +package com.book.backend.common; + +/** + * 基于ThreadLocal封装工具类,用于保存和获取当前登录用户id + * @author 程序员小白条 + */ +public class BaseContext { + private static ThreadLocal threadLocal = new ThreadLocal<>(); + + /** + * 设置值 + * @param id + */ + public static void setCurrentId(Long id){ + threadLocal.set(id); + } + + /** + * 获取值 + * @return + */ + public static Long getCurrentId(){ + return threadLocal.get(); + } +} diff --git a/src/main/java/com/book/backend/common/BasePage.java b/src/main/java/com/book/backend/common/BasePage.java new file mode 100644 index 0000000..f979f5a --- /dev/null +++ b/src/main/java/com/book/backend/common/BasePage.java @@ -0,0 +1,30 @@ +package com.book.backend.common; + +import lombok.Data; + +/** + * @author 程序员小白条 + */ +@Data +public class BasePage { + /** + * 分页参数 当前页 + */ + private int pageNum = 1; + /** + * 分页参数每页条数 + */ + private int pageSize = 3; + /** + * 查询的内容 + */ + private String query; + /** + * 查询的条件 + */ + private String condition; + /** + * 借阅证编号 + */ + private String cardNumber; +} diff --git a/src/main/java/com/book/backend/common/CustomException.java b/src/main/java/com/book/backend/common/CustomException.java new file mode 100644 index 0000000..66c6b90 --- /dev/null +++ b/src/main/java/com/book/backend/common/CustomException.java @@ -0,0 +1,11 @@ +package com.book.backend.common; + +/** + * 自定义业务异常类 + * @author 程序员小白条 + */ +public class CustomException extends RuntimeException{ + public CustomException(String message){ + super(message); + } +} diff --git a/src/main/java/com/book/backend/common/JwtProperties.java b/src/main/java/com/book/backend/common/JwtProperties.java new file mode 100644 index 0000000..3885fff --- /dev/null +++ b/src/main/java/com/book/backend/common/JwtProperties.java @@ -0,0 +1,46 @@ +package com.book.backend.common; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * JWT配置类,读取Application.yml中的配置 + * + * @author AdminMall + */ +@Component +@Data +public class JwtProperties { + /** + * JWT存储的请求头 + */ + @Value("${jwt.tokenHeader}") + private String tokenHeader; + /** + * jwt加解密使用的密钥 + */ + @Value("${jwt.secret}") + private String secret; + /** + * JWT的超时时间 + */ + @Value("${jwt.expiration}") + private long expiration; + + public JwtProperties() { + } + + /** + * JWT负载中拿到的开头 + */ + @Value("${jwt.tokenHead}") + private String tokenHead; + + public JwtProperties(String tokenHeader, String secret, long expiration, String tokenHead) { + this.tokenHeader = tokenHeader; + this.secret = secret; + this.expiration = expiration; + this.tokenHead = tokenHead; + } +} diff --git a/src/main/java/com/book/backend/common/MyMetaObjectHandler.java b/src/main/java/com/book/backend/common/MyMetaObjectHandler.java new file mode 100644 index 0000000..852b1d2 --- /dev/null +++ b/src/main/java/com/book/backend/common/MyMetaObjectHandler.java @@ -0,0 +1,46 @@ +package com.book.backend.common; + +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.reflection.MetaObject; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * 自定义元数据对象处理器 + * 公共填充功能 + * MyBatisPlus + */ +@Component +@Slf4j +public class MyMetaObjectHandler implements MetaObjectHandler { + /** + * 插入操作自动填充 + * @param metaObject + */ + @Override + public void insertFill(MetaObject metaObject) { + +// metaObject.setValue("createTime", LocalDateTime.now()); +// metaObject.setValue("updateTime",LocalDateTime.now()); + // MyBatisPlus3.3.0版本后推荐使用strictInsertFill,当字段值为null时,不进行填充 + this.strictInsertFill(metaObject,"createTime",String.class, + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + this.strictInsertFill(metaObject,"updateTime",String.class, + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + } + + /** + * 更新操作自动填充 + * @param metaObject + */ + @Override + public void updateFill(MetaObject metaObject) { + // MyBatisPlus3.3.0版本后推荐使用strictUpdateFill,当字段值为null时,不进行更新 +// metaObject.setValue("updateTime",LocalDateTime.now()); + this.strictUpdateFill(metaObject,"updateTime", + String.class,LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + } +} diff --git a/src/main/java/com/book/backend/common/R.java b/src/main/java/com/book/backend/common/R.java new file mode 100644 index 0000000..aeda01b --- /dev/null +++ b/src/main/java/com/book/backend/common/R.java @@ -0,0 +1,72 @@ +package com.book.backend.common; + +import lombok.Data; + +import java.util.HashMap; + +/** + * 通用返回结果,服务端响应的数据最终都会封装成此对象 + * @param + */ +@Data +public class R { + /** + * 响应状态码 + * | 200 | OK | 请求成功 + * | ---- | --------------------- | --------------------------------------------------- + * | 201 | CREATED | 创建成功 + * | 204 | DELETED | 删除成功 + * | 400 | BAD REQUEST | 请求的地址不存在或者包含不支持的参数 + * | 401 | UNAUTHORIZED | 未授权 + * | 403 | FORBIDDEN | 被禁止访问 + * | 404 | NOT FOUND | 请求的资源不存在 + * | 422 | Unprocesable entity | [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误 + * | 500 | INTERNAL SERVER ERROR | 内部错误 + */ + private Integer status; + /** + * 返回给前端的自定义信息 + */ + private String msg; + + /** + * 前端发送请求所需要接受到的真实数据 + */ + private T data; + + /** + * 动态数据 + */ + private HashMap map = new HashMap<>(); + + public static R success(T object,String message) { + R r = new R(); + r.data = object; + r.status = 200; + r.msg = message; + return r; + } + + public static R error(String msg) { + R r = new R(); + r.msg = msg; + return r; + } + public static R error(String msg,int code) { + R r = new R(); + r.msg = msg; + r.status = code; + return r; + } + /** + * 自定义类型的添加 加入到动态数据HashMap中 + * @param key string + * @param value Object + * @return R + */ + public R add(String key, Object value) { + this.map.put(key, value); + return this; + } + +} diff --git a/src/main/java/com/book/backend/common/exception/BusinessException.java b/src/main/java/com/book/backend/common/exception/BusinessException.java new file mode 100644 index 0000000..9137872 --- /dev/null +++ b/src/main/java/com/book/backend/common/exception/BusinessException.java @@ -0,0 +1,34 @@ +package com.book.backend.common.exception; + +/** + * 自定义异常类 + * + * @author 程序员小白条 + * + */ +public class BusinessException extends RuntimeException { + + /** + * 错误码 + */ + private final int code; + + public BusinessException(int code, String message) { + super(message); + this.code = code; + } + + public BusinessException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.code = errorCode.getCode(); + } + + public BusinessException(ErrorCode errorCode, String message) { + super(message); + this.code = errorCode.getCode(); + } + + public int getCode() { + return code; + } +} diff --git a/src/main/java/com/book/backend/common/exception/CommonError.java b/src/main/java/com/book/backend/common/exception/CommonError.java new file mode 100644 index 0000000..96bec15 --- /dev/null +++ b/src/main/java/com/book/backend/common/exception/CommonError.java @@ -0,0 +1,19 @@ +package com.book.backend.common.exception; + +public enum CommonError { + UNKOWN_ERROR("系统内部错误"), + PARAMS_ERROR("非法参数"), + OBJECT_NULL("对象为空"), + QUERY_NULL("查询结果为空或图书已借出"), + REQUEST_NULL("请求参数为空"), + USER_NULL("用户为空"); + private String errMessage; + + public String getErrMessage() { + return errMessage; + } + + private CommonError( String errMessage) { + this.errMessage = errMessage; + } +} diff --git a/src/main/java/com/book/backend/common/exception/ErrorCode.java b/src/main/java/com/book/backend/common/exception/ErrorCode.java new file mode 100644 index 0000000..f726153 --- /dev/null +++ b/src/main/java/com/book/backend/common/exception/ErrorCode.java @@ -0,0 +1,42 @@ +package com.book.backend.common.exception; + +/** + * 自定义错误码 + * + */ +public enum ErrorCode { + + SUCCESS(0, "ok"), + PARAMS_ERROR(40000, "请求参数错误"), + NOT_LOGIN_ERROR(40100, "未登录"), + NO_AUTH_ERROR(40101, "无权限"), + NOT_FOUND_ERROR(40400, "请求数据不存在"), + TOO_MANY_REQUEST(42900, "请求过于频繁"), + FORBIDDEN_ERROR(40300, "禁止访问"), + SYSTEM_ERROR(50000, "系统内部异常"), + OPERATION_ERROR(50001, "操作失败"); + + /** + * 状态码 + */ + private final int code; + + /** + * 信息 + */ + private final String message; + + ErrorCode(int code, String message) { + this.code = code; + this.message = message; + } + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } + +} diff --git a/src/main/java/com/book/backend/common/exception/GlobalExceptionHandler.java b/src/main/java/com/book/backend/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..aa6754c --- /dev/null +++ b/src/main/java/com/book/backend/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,26 @@ +package com.book.backend.common.exception; + +import com.book.backend.common.R; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +@ControllerAdvice +public class GlobalExceptionHandler { + + @ResponseBody + @ExceptionHandler(VueBookException.class) + @ResponseStatus(HttpStatus.OK) + public R businessExceptionHandler(VueBookException e) { + return R.error(e.getErrMessage(),e.getCode()); + } + +// @ResponseBody +// @ExceptionHandler(Exception.class) +// @ResponseStatus(HttpStatus.OK) +// public R runtimeExceptionHandler(Exception e) { +// return R.error(CommonError.UNKOWN_ERROR.getErrMessage()); +// +// } +} diff --git a/src/main/java/com/book/backend/common/exception/ThrowUtils.java b/src/main/java/com/book/backend/common/exception/ThrowUtils.java new file mode 100644 index 0000000..d857d8e --- /dev/null +++ b/src/main/java/com/book/backend/common/exception/ThrowUtils.java @@ -0,0 +1,43 @@ +package com.book.backend.common.exception; + +/** + * 抛异常工具类 + * + * @author 程序员小白条 + * + */ +public class ThrowUtils { + + /** + * 条件成立则抛异常 + * + * @param condition + * @param runtimeException + */ + public static void throwIf(boolean condition, RuntimeException runtimeException) { + if (condition) { + throw runtimeException; + } + } + + /** + * 条件成立则抛异常 + * + * @param condition + * @param errorCode + */ + public static void throwIf(boolean condition, ErrorCode errorCode) { + throwIf(condition, new BusinessException(errorCode)); + } + + /** + * 条件成立则抛异常 + * + * @param condition + * @param errorCode + * @param message + */ + public static void throwIf(boolean condition, ErrorCode errorCode, String message) { + throwIf(condition, new BusinessException(errorCode, message)); + } +} diff --git a/src/main/java/com/book/backend/common/exception/VueBookException.java b/src/main/java/com/book/backend/common/exception/VueBookException.java new file mode 100644 index 0000000..e974700 --- /dev/null +++ b/src/main/java/com/book/backend/common/exception/VueBookException.java @@ -0,0 +1,59 @@ +package com.book.backend.common.exception; + +public class VueBookException extends RuntimeException{ + + private static final long serialVersionUID = 5565760508056698922L; + /** + * 错误信息 + */ + private String errMessage; + /** + * 错误码 + */ + private int code; + public VueBookException() { + super(); + } + + public VueBookException(String errMessage) { + super(errMessage); + this.errMessage = errMessage; + } + + public String getErrMessage() { + return errMessage; + } + + public void setErrMessage(String errMessage) { + this.errMessage = errMessage; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + /** + * 新增 + * @param errorCode + * @param message + */ + public VueBookException(ErrorCode errorCode,String message){ + super(message); + this.code = errorCode.getCode(); + } + public VueBookException(ErrorCode errorCode){ + this.code = errorCode.getCode(); + } + + public static void cast(CommonError commonError){ + throw new VueBookException(commonError.getErrMessage()); + } + public static void cast(String errMessage){ + throw new VueBookException(errMessage); + } + +} diff --git a/src/main/java/com/book/backend/config/HttpSessionConfigurator.java b/src/main/java/com/book/backend/config/HttpSessionConfigurator.java new file mode 100644 index 0000000..394cd38 --- /dev/null +++ b/src/main/java/com/book/backend/config/HttpSessionConfigurator.java @@ -0,0 +1,39 @@ +package com.book.backend.config; + +import org.springframework.stereotype.Component; + +import javax.servlet.ServletRequestEvent; +import javax.servlet.ServletRequestListener; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import javax.websocket.HandshakeResponse; +import javax.websocket.server.HandshakeRequest; +import javax.websocket.server.ServerEndpointConfig; +import javax.websocket.server.ServerEndpointConfig.Configurator; + +/** + * 从websocket中获取用户session + * + * @author qimu + */ +@Component +public class HttpSessionConfigurator extends Configurator implements ServletRequestListener { + + @Override + public void requestInitialized(ServletRequestEvent sre) { + HttpSession session = ((HttpServletRequest) sre.getServletRequest()).getSession(); + } + + @Override + public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { + HttpSession httpSession = (HttpSession) request.getHttpSession(); + if (httpSession != null) { + sec.getUserProperties().put(HttpSession.class.getName(), httpSession); + } + super.modifyHandshake(sec, request, response); + } + + @Override + public void requestDestroyed(ServletRequestEvent arg0) { + } +} diff --git a/src/main/java/com/book/backend/config/JsonConfig.java b/src/main/java/com/book/backend/config/JsonConfig.java new file mode 100644 index 0000000..91170f7 --- /dev/null +++ b/src/main/java/com/book/backend/config/JsonConfig.java @@ -0,0 +1,31 @@ +package com.book.backend.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import org.springframework.boot.jackson.JsonComponent; +import org.springframework.context.annotation.Bean; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; + +/** + * Spring MVC Json 配置 + * + * @author 程序员小白条 + * + */ +@JsonComponent +public class JsonConfig { + + /** + * 添加 Long 转 json 精度丢失的配置 + */ + @Bean + public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) { + ObjectMapper objectMapper = builder.createXmlMapper(false).build(); + SimpleModule module = new SimpleModule(); + module.addSerializer(Long.class, ToStringSerializer.instance); + module.addSerializer(Long.TYPE, ToStringSerializer.instance); + objectMapper.registerModule(module); + return objectMapper; + } +} diff --git a/src/main/java/com/book/backend/config/MyBatisConfig.java b/src/main/java/com/book/backend/config/MyBatisConfig.java new file mode 100644 index 0000000..b6737c7 --- /dev/null +++ b/src/main/java/com/book/backend/config/MyBatisConfig.java @@ -0,0 +1,27 @@ +package com.book.backend.config; + +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import com.baomidou.mybatisplus.generator.config.StrategyConfig; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author 程序员小白条 + */ +@Configuration +@MapperScan("com.blog.backend.mapper") +public class MyBatisConfig { + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor(){ + //配置分页插件 + MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); + mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); + return mybatisPlusInterceptor; + } +// @Bean +// public StrategyConfig strategyConfig(){ +// return new StrategyConfig.Builder().entityBuilder().enableTableFieldAnnotation().build(); +// } +} diff --git a/src/main/java/com/book/backend/config/RedisConfig.java b/src/main/java/com/book/backend/config/RedisConfig.java new file mode 100644 index 0000000..8079b8a --- /dev/null +++ b/src/main/java/com/book/backend/config/RedisConfig.java @@ -0,0 +1,31 @@ +package com.book.backend.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializer; + +@Configuration +public class RedisConfig { +// 因为用到了jsonRedisSerializer,所以要导入jackson依赖 + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory){ + // 创建RedisTemplate对象 + RedisTemplate template = new RedisTemplate<>(); + // 设置连接工厂 + template.setConnectionFactory(connectionFactory); + // 创建JSON序列化工具 + GenericJackson2JsonRedisSerializer jsonRedisSerializer = + new GenericJackson2JsonRedisSerializer(); + // 设置Key的序列化 + template.setKeySerializer(RedisSerializer.string()); + template.setHashKeySerializer(RedisSerializer.string()); + // 设置Value的序列化 + template.setValueSerializer(jsonRedisSerializer); + template.setHashValueSerializer(jsonRedisSerializer); + // 返回 + return template; + } +} diff --git a/src/main/java/com/book/backend/config/RedissonConfig.java b/src/main/java/com/book/backend/config/RedissonConfig.java new file mode 100644 index 0000000..500c5f0 --- /dev/null +++ b/src/main/java/com/book/backend/config/RedissonConfig.java @@ -0,0 +1,37 @@ +package com.book.backend.config; + +/** + * @author 小白条 + * @from 个人博客 + */ +import lombok.Data; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +//@Configuration +//@ConfigurationProperties(prefix = "spring.redis") +@Data +public class RedissonConfig { + + private Integer database; + + private String host; + + private Integer port; + + private String password; + + @Bean + public RedissonClient redissonClient() { + Config config = new Config(); + config.useSingleServer() + .setDatabase(database) + .setAddress("redis://" + host + ":" + port) + .setPassword(password); + return Redisson.create(config); + } +} diff --git a/src/main/java/com/book/backend/config/ThreadPoolExecutorConfig.java b/src/main/java/com/book/backend/config/ThreadPoolExecutorConfig.java new file mode 100644 index 0000000..80a7676 --- /dev/null +++ b/src/main/java/com/book/backend/config/ThreadPoolExecutorConfig.java @@ -0,0 +1,34 @@ +package com.book.backend.config; + +/** + * @author 小白条 + * @from 个人博客 + */ +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +@Configuration +public class ThreadPoolExecutorConfig { + + @Bean + public ThreadPoolExecutor threadPoolExecutor() { + ThreadFactory threadFactory = new ThreadFactory() { + private int count = 1; + + @Override + public Thread newThread( Runnable r) { + Thread thread = new Thread(r); + thread.setName("线程" + count); + count++; + return thread; + } + }; + return new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS, + new ArrayBlockingQueue<>(4), threadFactory); + } +} diff --git a/src/main/java/com/book/backend/config/WebMvcConfig.java b/src/main/java/com/book/backend/config/WebMvcConfig.java new file mode 100644 index 0000000..265290a --- /dev/null +++ b/src/main/java/com/book/backend/config/WebMvcConfig.java @@ -0,0 +1,30 @@ +package com.book.backend.config; + +import com.book.backend.interceptor.AuthInterceptorHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * @author 程序员小白条 + */ +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + @Override + public void addCorsMappings(CorsRegistry registry) { + //跨域配置 + registry.addMapping("/**").allowedOriginPatterns("*").allowedMethods("POST","GET","PUT","OPTIONS","DELETE").allowCredentials(true).allowedHeaders("*").maxAge(3600); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(authInterceptorHandler()).excludePathPatterns("/**/login").excludePathPatterns("/doc.html").excludePathPatterns("/**/websocket"); + } + + @Bean + public AuthInterceptorHandler authInterceptorHandler(){ + return new AuthInterceptorHandler(); + } +} diff --git a/src/main/java/com/book/backend/config/WebSocketConfig.java b/src/main/java/com/book/backend/config/WebSocketConfig.java new file mode 100644 index 0000000..175bb4e --- /dev/null +++ b/src/main/java/com/book/backend/config/WebSocketConfig.java @@ -0,0 +1,20 @@ +package com.book.backend.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.server.standard.ServerEndpointExporter; + +@Configuration +public class WebSocketConfig { + + /** + * ServerEndpointExporter类的作用是,会扫描所有的服务器端点, + * 把带有@ServerEndpoint 注解的所有类都添加进来 + * + */ + @Bean + public ServerEndpointExporter serverEndpointExporter(){ + return new ServerEndpointExporter(); + } + +} diff --git a/src/main/java/com/book/backend/constant/ChatConstant.java b/src/main/java/com/book/backend/constant/ChatConstant.java new file mode 100644 index 0000000..6ff87ef --- /dev/null +++ b/src/main/java/com/book/backend/constant/ChatConstant.java @@ -0,0 +1,16 @@ +package com.book.backend.constant; + + +public interface ChatConstant { + /** + * 私聊 + */ + int PRIVATE_CHAT = 1; + /** + * 群聊 + */ + int HALL_CHAT = 2; + + + +} diff --git a/src/main/java/com/book/backend/constant/Constant.java b/src/main/java/com/book/backend/constant/Constant.java new file mode 100644 index 0000000..9c8b952 --- /dev/null +++ b/src/main/java/com/book/backend/constant/Constant.java @@ -0,0 +1,58 @@ +package com.book.backend.constant; + +/** + * @author 程序员小白条 + * 常量类 + * 防止魔法值 + */ +public class Constant { + /** + * 字符串NULL判断 + */ + public static final String NULL = "null"; + /** + * 预检请求 + */ + public static final String OPTIONS = "OPTIONS"; + /** + * 账号为可用状态 + */ + public static final Integer AVAILABLE = 1; + /** + * 账号为禁用状态 + */ + public static final Integer DISABLE = 0; + /** + * 图书已借出状态 + */ + public static final String BOOKDISABLE = "已借出"; + /** + * 图书未借出状态 + */ + public static final String BOOKAVAILABLE = "未借出"; + /** + * 用户可用状态 字符串 + */ + public static final String USERAVAILABLE = "可用"; + /** + * 用户禁用状态 字符串 + */ + public static final String USERDISABLE = "禁用"; + /** + * 密码超过30,判定为md5加密字符 + */ + public static final Integer MD5PASSWORD = 30; + /** + * 升序 + */ + public static final String SORT_ORDER_ASC = "ascend"; + + /** + * 降序 + */ + public static final String SORT_ORDER_DESC = " descend"; + /** + * BI 模型 id + */ + public static final long BI_MODEL_ID = 1659171950288818178L; +} diff --git a/src/main/java/com/book/backend/controller/admin/AdminFunctionController.java b/src/main/java/com/book/backend/controller/admin/AdminFunctionController.java new file mode 100644 index 0000000..40b5198 --- /dev/null +++ b/src/main/java/com/book/backend/controller/admin/AdminFunctionController.java @@ -0,0 +1,414 @@ +package com.book.backend.controller.admin; + + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.book.backend.common.BasePage; +import com.book.backend.common.R; +import com.book.backend.pojo.*; +import com.book.backend.pojo.dto.*; +import com.book.backend.pojo.dto.chart.GenChartByAiRequest; +import com.book.backend.pojo.vo.BiResponse; +import com.book.backend.service.*; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.io.IOException; +import java.util.List; + + +/** + * @author 程序员小白条 + */ +@RestController +@RequestMapping("admin") +public class AdminFunctionController { + @Resource + private BooksService booksService; + @Resource + private BookTypeService bookTypeService; + @Resource + private UsersService usersService; + @Resource + private BookRuleService bookRuleService; + @Resource + private BookAdminsService bookAdminsService; + @Resource + private ViolationService violationService; + + @Resource + private AdminsService adminsService; + @Resource + private ChartService chartService; + + /** + * 获取图书列表 + * + * @param basePage 页码,页数,条件和内容 + * @return R> + */ + @PostMapping("get_booklist") + @ApiOperation("获取图书列表") + public R> getBookList(@RequestBody BasePage basePage) { + return booksService.getBookList(basePage); + } + + /** + * 查询书籍类型的列表 用于添加图书中回显分类 + * + * @return R + */ + @GetMapping("get_type") + @ApiOperation("查询书籍类型列表") + public R> getBookTypeList() { + return bookTypeService.getBookTypeList(); + } + + /** + * 添加图书 利用DTO去接受 书籍类别的id 然后再通过id查询分类表获取分类名称 封装给图书 + * + * @return R + */ + @PostMapping("add_book") + @ApiOperation("添加图书") + public R addBook(@RequestBody BookDTO bookDTO) { + return booksService.addBook(bookDTO); + } + + /** + * 根据图书id删除对应的图书 + * + * @param bookId 图书id + * @return R + */ + @GetMapping("delete_book/{bookId}") + @ApiOperation("根据图书id删除对应的图书") + public R deleteBookByBookId(@PathVariable("bookId") Integer bookId) { + return booksService.deleteBookByBookId(bookId); + } + + /** + * 根据图书id获得相对应的图书信息 + * + * @param bookId 图书id + * @return R + */ + @GetMapping("get_bookinformation/{bookId}") + @ApiOperation("根据图书id获得相对应的图书信息") + public R getBookInformationByBookId(@PathVariable("bookId") Integer bookId) { + return booksService.getBookInformationByBookId(bookId); + } + + /** + * 根据前端传输的图书信息更新图书 + * + * @param books 图书 + * @return R + */ + @PostMapping("update_book") + @ApiOperation("更新图书") + public R updateBookByEditForm(@RequestBody Books books) { + return booksService.updateBookByEditForm(books); + } + + /** + * 书籍类别 获取书籍类别的列表 + * + * @return R> + */ + @PostMapping("get_booktype_page") + @ApiOperation("获取书籍类别的列表") + public R> getBookTypeListByPage(@RequestBody BasePage basePage) { + return bookTypeService.getBookTypeListByPage(basePage); + } + + /** + * 添加书籍类别 + * + * @param bookType 书籍类别 + * @return R + */ + @PostMapping("add_booktype") + @ApiOperation("添加书籍类别") + public R addBookType(@RequestBody BookType bookType) { + return bookTypeService.addBookType(bookType); + } + + /** + * 根据书籍类别id 获取书籍类别信息 + * + * @param typeId 书籍类别id + * @return R + */ + @GetMapping("get_booktype/{typeId}") + @ApiOperation("获取书籍类别信息") + public R getBookTypeByTypeId(@PathVariable("typeId") Integer typeId) { + return bookTypeService.getBookTypeByTypeId(typeId); + } + + /** + * 更新书籍类别 + * + * @param bookType 书籍类别 + * @return R + */ + @PostMapping("update_booktype") + @ApiOperation("更新书籍类别") + public R updateBookType(@RequestBody BookType bookType) { + return bookTypeService.updateBookType(bookType); + } + + /** + * 删除书籍类别 根据书籍类别的ID + * + * @param typeId 书籍类别的id + * @return R + */ + @GetMapping("delete_booktype/{typeId}") + @ApiOperation("删除书籍类别") + public R deleteBookTypeByTypeId(@PathVariable("typeId") Integer typeId) { + return bookTypeService.deleteBookTypeByTypeId(typeId); + } + + /** + * 获取借阅证列表(用户列表) + * + * @param basePage 用于接受模糊查询和分页构造的参数 + * @return R> + */ + @PostMapping("get_statementlist") + @ApiOperation("获取借阅证列表") + public R> getStatementList(@RequestBody BasePage basePage) { + return usersService.getStatementList(basePage); + } + + /** + * 添加借阅证 + * + * @param usersDTO 用户DTO + * @return R + */ + @PostMapping("add_statement") + @ApiOperation("添加借阅证") + public R addStatement(@RequestBody UsersDTO usersDTO) { + return adminsService.addRule(usersDTO); + } + + /** + * 获取用户信息 根据用户id 用于回显借阅证 + * + * @param userId 用户id + * @return R + */ + @GetMapping("get_statement/{userId}") + @ApiOperation("获取用户信息用于回显借阅证") + public R getStatementByUserId(@PathVariable("userId") Integer userId) { + return usersService.getStatementByUserId(userId); + } + + /** + * 修改借阅证信息(用户信息) + * + * @param usersDTO 用户DTO + * @return R + */ + @PostMapping("update_statement") + @ApiOperation("修改借阅证信息") + public R updateStatement(@RequestBody UsersDTO usersDTO) { + return usersService.updateStatement(usersDTO); + } + + /** + * 删除借阅证信息 根据用户id + * + * @param userId 用户id + * @return R + */ + @DeleteMapping("delete_statement/{userId}") + @ApiOperation("删除借阅证信息") + public R deleteStatementByUserId(@PathVariable("userId") Integer userId) { + return usersService.deleteStatementByUserId(userId); + } + + /** + * 获取规则列表(分页) + * + * @param basePage 分页构造器用于接受页数和页码 + * @return R> + */ + @PostMapping("get_rulelist_page") + @ApiOperation("获取规则列表") + public R> getRuleListByPage(@RequestBody BasePage basePage) { + return bookRuleService.getRuleListByPage(basePage); + } + + /** + * 添加规则 + * + * @param bookRule 图书规则 + * @return R + */ + @PostMapping("add_rule") + @ApiOperation("添加规则") + public R addRule(@RequestBody BookRule bookRule) { + return bookRuleService.addRule(bookRule); + } + + /** + * 根据规则编号 查询规则 + * + * @param ruleId 规则编号 + * @return R + */ + @GetMapping("get_rule_ruleid/{ruleId}") + @ApiOperation("根据规则编号查询规则") + public R getRuleByRuleId(@PathVariable("ruleId") Integer ruleId) { + return bookRuleService.getRuleByRuleId(ruleId); + } + + /** + * 修改规则 + * + * @param bookRuleDTO 图书规则 + * @return R + */ + @PutMapping("update_rule") + @ApiOperation("修改规则") + public R updateRule(@RequestBody BookRuleDTO bookRuleDTO) { + return bookRuleService.updateRule(bookRuleDTO); + } + + /** + * 删除规则 + * + * @param ruleId 规则编号 + * @return R + */ + @DeleteMapping("delete_rule/{ruleId}") + @ApiOperation("删除规则") + public R deleteRule(@PathVariable("ruleId") Integer ruleId) { + return bookRuleService.deleteRule(ruleId); + } + + /** + * 获取图书管理员的列表 + * + * @param basePage 分页构造器传参 + * @return R> + */ + @PostMapping("get_bookadminlist") + @ApiOperation("获取图书管理员的列表") + public R> getBookAdminListByPage(@RequestBody BasePage basePage) { + return bookAdminsService.getBookAdminListByPage(basePage); + } + + /** + * 添加图书管理员 + * + * @param bookAdmins 图书管理员 + * @return R + */ + @PostMapping("add_bookadmin") + @ApiOperation("添加图书管理员") + public R addBookAdmin(@RequestBody BookAdmins bookAdmins) { + return bookAdminsService.addBookAdmin(bookAdmins); + } + + /** + * 获取图书管理员信息 通过图书管理员id + * + * @param bookAdminId 图书管理员id + * @return R + */ + @GetMapping("get_bookadmin/{bookAdminId}") + @ApiOperation("获取图书管理员信息") + public R getBookAdminById(@PathVariable("bookAdminId") Integer bookAdminId) { + return bookAdminsService.getBookAdminById(bookAdminId); + } + + /** + * 删除图书管理员 根据图书管理员id + * + * @param bookAdminId 图书管理员id + * @return R + */ + @DeleteMapping("delete_bookadmin/{bookAdminId}") + @ApiOperation("删除图书管理员") + public R deleteBookAdminById(@PathVariable("bookAdminId") Integer bookAdminId) { + return bookAdminsService.deleteBookAdminById(bookAdminId); + } + + /** + * 修改图书管理员 + * + * @param bookAdmins 图书管理员 + * @return R + */ + @PutMapping("update_bookadmin") + @ApiOperation("修改图书管理员") + public R updateBookAdmin(@RequestBody BookAdmins bookAdmins) { + return bookAdminsService.updateBookAdmin(bookAdmins); + } + + /** + * 获取借阅量 + * + * @return R + */ + @GetMapping("get_borrowdata") + @ApiOperation("获取借阅量") + public R getBorrowDate() { + return violationService.getBorrowDate(); + } + + /** + * 获取借书分类统计情况 + * + * @return R> + */ + @GetMapping("get_borrowtype_statistics") + @ApiOperation("获取借书分类统计情况") + public R> getBorrowTypeStatistic() { + return booksService.getBorrowTypeStatistic(); + } + + /** + * 批量删除图书 + * + * @param booksList 图书列表 + * @return R + */ + @DeleteMapping("delete_book_batch") + @ApiOperation("批量删除图书") + public R deleteBookByBatch(@RequestBody List booksList) { + return booksService.deleteBookByBatch(booksList); + + } + + /** + * 从Excel批量导入图书 + * + * @param file 文件 + * @return R + * @throws IOException IO异常 + */ + @PostMapping("/updown") + @ApiOperation("从Excel批量导入图书") + public R upload(@RequestParam("files") MultipartFile file) throws IOException { + return adminsService.upload(file); + } + + /** + * 根据用户输入信息,生成图表 + * @param multipartFile 文件 + * @param genChartByAiRequest 用户输入信息(分析目标,图标类型,名称),用户id + * @return R + */ + @PostMapping("/gen") + @ApiOperation("根据用户输入信息,生成图表") + public R genChartByAi(@RequestPart("file") MultipartFile multipartFile, + GenChartByAiRequest genChartByAiRequest) { + return chartService.genChartByAi(multipartFile,genChartByAiRequest); + } +} diff --git a/src/main/java/com/book/backend/controller/admin/AdminLoginController.java b/src/main/java/com/book/backend/controller/admin/AdminLoginController.java new file mode 100644 index 0000000..f9762ad --- /dev/null +++ b/src/main/java/com/book/backend/controller/admin/AdminLoginController.java @@ -0,0 +1,44 @@ +package com.book.backend.controller.admin; + +import com.book.backend.common.R; +import com.book.backend.pojo.Admins; +import com.book.backend.service.AdminsService; +import io.swagger.annotations.ApiOperation; +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 程序员小白条 + */ +@RestController +@RequestMapping("/admin") +public class AdminLoginController { + @Resource + private AdminsService adminsService; + + /** + * 系统管理员登录 + * @param users 系统管理员 + * @return 返回R通用数据 + */ + @PostMapping("/login") + @ApiOperation("管理员登录") + public R login(@RequestBody Admins users){ + return adminsService.login(users); + } + + /** + * 返回给前端系统管理员的数据 + * @param admin 系统管理员 + * @return R + */ + @PostMapping ("/getData") + @ApiOperation("获取管理员数据") + public R getUserData(@RequestBody Admins admin){ + return adminsService.getUserData(admin); + } +} diff --git a/src/main/java/com/book/backend/controller/bookadmin/BookAdminFunctionController.java b/src/main/java/com/book/backend/controller/bookadmin/BookAdminFunctionController.java new file mode 100644 index 0000000..7953de8 --- /dev/null +++ b/src/main/java/com/book/backend/controller/bookadmin/BookAdminFunctionController.java @@ -0,0 +1,165 @@ +package com.book.backend.controller.bookadmin; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.book.backend.common.BasePage; +import com.book.backend.common.R; +import com.book.backend.pojo.BooksBorrow; +import com.book.backend.pojo.Notice; +import com.book.backend.pojo.Violation; +import com.book.backend.pojo.dto.BooksBorrowDTO; +import com.book.backend.pojo.dto.ViolationDTO; +import com.book.backend.service.*; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +/** + * @author 程序员小白条 + */ +@RestController +@RequestMapping("/bookadmin") +public class BookAdminFunctionController { + @Resource + private BookAdminsService bookAdminsService; + + @Resource + private BooksService booksService; + @Resource + private BooksBorrowService booksBorrowService; + + @Resource + private NoticeService noticeService; + + + /** + * 借阅图书根据借阅证号和图书编号 + * + * @return R + */ + @PostMapping("borrow_book") + @ApiOperation("根据借阅证号和图书编号借阅图书") + public R borrowBookByCardNumberAndBookNumber(@RequestBody BooksBorrowDTO booksBorrowDTO) { + return booksService.borrowBookByCardNumberAndBookNumber(booksBorrowDTO); + } + + /** + * 查看图书是否有逾期(查看是否借出) + * + * @param bookNumber 图书编号 + * @return R + */ + @GetMapping("query_book/{bookNumber}") + @ApiOperation("查看图书是否有逾期") + public R queryBookExpireByBookNumber(@PathVariable("bookNumber") Long bookNumber) { + return booksService.queryBookExpireByBookNumber(bookNumber); + } + + /** + * 获取图书逾期信息 + * + * @param bookNumber 图书编号 + * @return R + */ + @GetMapping("query_expire/{bookNumber}") + @ApiOperation("获取图书逾期信息") + public R queryExpireInformationByBookNumber(@PathVariable("bookNumber") Long bookNumber) { + return booksBorrowService.queryExpireInformationByBookNumber(bookNumber); + } + + /** + * 归还图书 + * + * @param violation 违章表 + * @return R + */ + @PostMapping("return_book") + @ApiOperation("归还图书") + public R returnBook(@RequestBody Violation violation) { + return booksBorrowService.returnBook(violation); + } + + /** + * 获取还书报表 + * + * @param basePage 接受分页构造器和模糊查询的传参 + * @return R> + */ + @PostMapping("get_return_statement") + @ApiOperation("获取还书报表") + public R> getReturnStatement(@RequestBody BasePage basePage) { + return booksBorrowService.getReturnStatement(basePage); + } + + /** + * 获取借书报表 + * + * @param basePage 接受分页构造器和模糊查询的传参 + * @return R> + */ + @PostMapping("get_borrow_statement") + @ApiOperation("获取借书报表") + public R> getBorrowStatement(@RequestBody BasePage basePage) { + return bookAdminsService.getBorrowStatement(basePage); + } + + /** + * 获取公告列表 + * + * @return R + */ + @PostMapping("get_noticelist") + @ApiOperation("获取公告列表") + public R> getNoticeList(@RequestBody BasePage basePage) { + return noticeService.getNoticeList(basePage); + } + + /** + * 添加公告 + * + * @param notice 公告 + * @return R + */ + @PostMapping("add_notice") + @ApiOperation("添加公告") + public R addNotice(@RequestBody Notice notice) { + return noticeService.addNotice(notice); + } + + /** + * 删除公告根据指定的id + * + * @param noticeId 公告id + * @return R + */ + @GetMapping("delete_notice/{noticeId}") + @ApiOperation("删除公告根据指定的id") + public R deleteNoticeById(@PathVariable("noticeId") Integer noticeId) { + return noticeService.deleteNoticeById(noticeId); + } + + /** + * 根据指定id获取公告 + * + * @param noticeId 公告id + * @return R + */ + @GetMapping("get_notice/{noticeId}") + @ApiOperation("根据指定id获取公告") + public R getNoticeByNoticeId(@PathVariable("noticeId") Integer noticeId) { + return noticeService.getNoticeByNoticeId(noticeId); + } + + /** + * 更新公告根据公告id + * + * @param noticeId 公告id + * @param notice 公告 + * @return R + */ + @PutMapping("update_notice/{noticeId}") + @ApiOperation("更新公告根据公告id") + public R updateNoticeByNoticeId(@PathVariable("noticeId") Integer noticeId, @RequestBody Notice notice) { + return noticeService.updateNoticeByNoticeId(noticeId, notice); + } +} diff --git a/src/main/java/com/book/backend/controller/bookadmin/BookAdminLoginController.java b/src/main/java/com/book/backend/controller/bookadmin/BookAdminLoginController.java new file mode 100644 index 0000000..b080d2b --- /dev/null +++ b/src/main/java/com/book/backend/controller/bookadmin/BookAdminLoginController.java @@ -0,0 +1,48 @@ +package com.book.backend.controller.bookadmin; + +import com.book.backend.common.R; +import com.book.backend.pojo.BookAdmins; +import com.book.backend.service.BookAdminsService; +import io.swagger.annotations.ApiOperation; +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 程序员小白条 + */ +@RestController +@RequestMapping("/bookadmin") +public class BookAdminLoginController { + @Resource + private BookAdminsService bookAdminsService; + + + /** + * 图书管理员登录 + * + * @param users 图书管理员 + * @return 返回R通用数据 + */ + @PostMapping("/login") + @ApiOperation("图书管理员登录") + public R login(@RequestBody BookAdmins users) { + return bookAdminsService.login(users); + } + + /** + * 返回给前端图书管理员的数据 + * + * @param bookAdmins 图书管理员 + * @return R + */ + @PostMapping("/getData") + @ApiOperation("获取图书管理员数据") + public R getUserData(@RequestBody BookAdmins bookAdmins) { + return bookAdminsService.getUserData(bookAdmins); + } + +} diff --git a/src/main/java/com/book/backend/controller/user/UserFunctionController.java b/src/main/java/com/book/backend/controller/user/UserFunctionController.java new file mode 100644 index 0000000..326a0e1 --- /dev/null +++ b/src/main/java/com/book/backend/controller/user/UserFunctionController.java @@ -0,0 +1,165 @@ +package com.book.backend.controller.user; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.book.backend.common.BasePage; +import com.book.backend.common.R; +import com.book.backend.pojo.*; +import com.book.backend.pojo.dto.CommentDTO; +import com.book.backend.pojo.dto.ViolationDTO; +import com.book.backend.service.*; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * @author 程序员小白条 + */ +@RestController +@RequestMapping("/user") +public class UserFunctionController { + @Resource + private BooksService booksService; + @Resource + private BookRuleService bookRuleService; + @Resource + private NoticeService noticeService; + @Resource + private UsersService usersService; + @Resource + private BooksBorrowService booksBorrowService; + @Resource + private ViolationService violationService; + + @Resource + private CommentService commentService; + @Resource + private AiIntelligentService aiIntelligentService; + + /** + * 图书查询 分页和条件查询 (模糊查询) + * + * @param basePage 用于接受分页传参 + * @return R> + */ + @PostMapping("/search_book_page") + @ApiOperation("图书查询 分页和条件查询") + public R> searchBookPage(@RequestBody BasePage basePage) { + return booksService.searchBookPage(basePage); + } + + /** + * 读者规则查询 + * + * @return R> + */ + @GetMapping("get_rulelist") + @ApiOperation("读者规则查询") + public R> getRuleList() { + return bookRuleService.getRuleList(); + } + + /** + * 查询公告信息 + * + * @return R> + */ + @GetMapping("get_noticelist") + @ApiOperation("查询公告信息") + public R> getNoticeList() { + return noticeService.getNoticeList(); + } + + /** + * Rest接受参数 查询个人用户userId + * + * @param userId 用户id + * @return R + */ + @GetMapping("get_information/{userId}") + @ApiOperation("查询个人用户") + public R getUserByUserId(@PathVariable("userId") Integer userId) { + return usersService.getUserByUserId(userId); + } + + /** + * 修改密码 + * + * @return R + */ + @PostMapping("update_password") + @ApiOperation("修改密码") + public R updatePassword(@RequestBody Users users) { + return usersService.updatePassword(users); + } + + /** + * 借阅信息查询 根据用户id,条件及其内容 + * + * @param basePage 用于接受分页传参和用户id + * @return R> + */ + @PostMapping("get_bookborrow") + @ApiOperation("借阅信息查询") + public R> getBookBorrowPage(@RequestBody BasePage basePage) { + return booksBorrowService.getBookBorrowPage(basePage); + } + + /** + * 查询违章信息(借阅证) + * + * @param basePage 获取前端的分页参数,条件和内容,借阅证 + * @return R> + */ + @PostMapping("get_violation") + @ApiOperation("查询违章信息") + public R> getViolationListByPage(@RequestBody BasePage basePage) { + return violationService.getViolationListByPage(basePage); + } + + /** + * 获取弹幕列表 + * + * @return R + */ + @GetMapping("get_commentlist") + @ApiOperation("获取弹幕列表") + public R> getCommentList() { + return commentService.getCommentList(); + + } + + /** + * 添加弹幕 + * + * @return R + */ + @PostMapping("add_comment") + @ApiOperation("添加弹幕") + public R addComment(@RequestBody CommentDTO commentDTO) { + return commentService.addComment(commentDTO); + } + + /** + * 调用AI模型,获取数据库中有的,并且推荐图书给用户 + * @param aiIntelligent AI实体类 + * @return R + */ + @PostMapping("ai_intelligent") + @ApiOperation("推荐图书") + public R aiRecommend(@RequestBody AiIntelligent aiIntelligent){ + return aiIntelligentService.getGenResult(aiIntelligent); + } + + /** + * 根据用户ID 获取该用户和AI聊天的最近的五条消息 + * @param userId 用户id + * @return R> + */ + @GetMapping("ai_list_information/{userId}") + @ApiOperation("获取该用户和AI聊天的最近的五条消息") + public R> getAiInformationByUserId(@PathVariable("userId") Long userId){ + return aiIntelligentService.getAiInformationByUserId(userId); + } +} diff --git a/src/main/java/com/book/backend/controller/user/UserLoginController.java b/src/main/java/com/book/backend/controller/user/UserLoginController.java new file mode 100644 index 0000000..44b5c96 --- /dev/null +++ b/src/main/java/com/book/backend/controller/user/UserLoginController.java @@ -0,0 +1,46 @@ +package com.book.backend.controller.user; + +import com.book.backend.common.R; +import com.book.backend.pojo.Users; +import com.book.backend.service.UsersService; +import io.swagger.annotations.ApiOperation; +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 程序员小白条 + */ +@RestController +@RequestMapping("/user") +public class UserLoginController { + @Resource + private UsersService usersService; + + /** + * 借阅用户登录 + * + * @param users 借阅者用户 + * @return 返回R通用数据 + */ + @PostMapping("/login") + @ApiOperation("用户登录") + public R login(@RequestBody Users users) { + return usersService.login(users); + } + + /** + * 根据用户id传给用户所需的信息 + * + * @param users 用户 + * @return R + */ + @PostMapping("/getData") + @ApiOperation("获取用户数据") + public R getUserData(@RequestBody Users users) { + return usersService.getUserData(users); + } +} diff --git a/src/main/java/com/book/backend/interceptor/AuthInterceptorHandler.java b/src/main/java/com/book/backend/interceptor/AuthInterceptorHandler.java new file mode 100644 index 0000000..6b0dae6 --- /dev/null +++ b/src/main/java/com/book/backend/interceptor/AuthInterceptorHandler.java @@ -0,0 +1,89 @@ +package com.book.backend.interceptor; + +import com.alibaba.fastjson.JSONObject; +import com.book.backend.constant.Constant; +import com.book.backend.common.JwtProperties; +import com.book.backend.utils.JwtKit; +import io.jsonwebtoken.Claims; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.PrintWriter; + +/** + * @author 程序员小白条 + */ +public class AuthInterceptorHandler implements HandlerInterceptor { + @Autowired + private JwtProperties jwtProperties; + + @Autowired + private JwtKit jwtKit; + + /** + * 前置拦截器 + */ + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + + if(!(handler instanceof HandlerMethod)){ + return true; + } + + HandlerMethod handlerMethod=(HandlerMethod)handler; + //判断如果请求的类是swagger的控制器,直接通行。 + if(("springfox.documentation.swagger.web.ApiResourceController").equals(handlerMethod.getBean().getClass().getName())){ + return true; + } + + if ((Constant.OPTIONS).equals(request.getMethod())) { + response.setStatus(HttpServletResponse.SC_OK); + return true; + } + // 获取到JWT的Token + String jwtToken = request.getHeader(jwtProperties.getTokenHeader()); + // 截取中间payload部分 +1是Bearer + 空格(1) + String payloadToken = null; + // 创建json对象 + JSONObject jsonObject = new JSONObject(); + + if (jwtToken != null) { + payloadToken = jwtToken.substring(jwtProperties.getTokenHead().length() + 1); + } + if (payloadToken != null && (!(Constant.NULL).equals(payloadToken))) { + // 解析Token,获取Claims = Map + Claims claims = null; + try { + claims = jwtKit.parseJwtToken(payloadToken); + } catch (Exception e) { + //token过期会捕捉到异常 + jsonObject.put("status", 401); + jsonObject.put("msg", "登录过期,请重新登录"); + String json1 = jsonObject.toJSONString(); + renderJson(response, json1); + } + return claims != null; + // 获取payload中的报文, + } + // 如果token不存在 + jsonObject.put("status", 401); + jsonObject.put("msg", "登录非法"); + String json2 = jsonObject.toJSONString(); + renderJson(response, json2); + + return false; + } + + private void renderJson(HttpServletResponse response, String json) { + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/json;charset=UTF-8"); + try (PrintWriter printWriter = response.getWriter()) { + printWriter.print(json); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/com/book/backend/job/cycle/ConvertCommentListToRedis.java b/src/main/java/com/book/backend/job/cycle/ConvertCommentListToRedis.java new file mode 100644 index 0000000..7ba4df3 --- /dev/null +++ b/src/main/java/com/book/backend/job/cycle/ConvertCommentListToRedis.java @@ -0,0 +1,59 @@ +package com.book.backend.job.cycle; + +import com.book.backend.pojo.Comment; +import com.book.backend.pojo.dto.CommentDTO; +import com.book.backend.service.CommentService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; + +import javax.annotation.Resource; +import java.time.LocalDate; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @author 小白条 + * @from 个人博客 + */ +// todo 取消 @Component 注释开启任务 +// 程序启动时执行一次,然后每隔1天自动执行定时任务 +//@Component +@EnableScheduling +@Slf4j +public class ConvertCommentListToRedis { + + @Resource + private CommentService commentService; + @Resource + private RedisTemplate redisTemplate; + + public ConvertCommentListToRedis() { + } + + /** + * 每天凌晨三点将数据库中的留言列表转到 Redis + * + */ + @Scheduled(cron = "0 0 3 * * ?") + public void run() { + log.info("准备将数据库中的留言列表转移到Redis"); + LocalDate localDate = LocalDate.now(); + String key = "comment:"+localDate; + List list = commentService.list(); + List commentDTOList = list.stream().map((item) -> { + CommentDTO commentDTO = new CommentDTO(); + commentDTO.setAvatar(item.getCommentAvatar()); + commentDTO.setId(item.getCommentId()); + commentDTO.setMsg(item.getCommentMessage()); + commentDTO.setTime(item.getCommentTime()); + commentDTO.setBarrageStyle(item.getCommentBarrageStyle()); + return commentDTO; + }).collect(Collectors.toList()); + redisTemplate.opsForList().rightPushAll(key,commentDTOList); + redisTemplate.expire(key,7, TimeUnit.DAYS); + log.info("留言列表成功转移到Redis"); + } +} diff --git a/src/main/java/com/book/backend/job/cycle/IncSyncDeleteAIMessage.java b/src/main/java/com/book/backend/job/cycle/IncSyncDeleteAIMessage.java new file mode 100644 index 0000000..5033aa6 --- /dev/null +++ b/src/main/java/com/book/backend/job/cycle/IncSyncDeleteAIMessage.java @@ -0,0 +1,83 @@ +package com.book.backend.job.cycle; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.book.backend.common.exception.BusinessException; +import com.book.backend.common.exception.ErrorCode; +import com.book.backend.pojo.AiIntelligent; +import com.book.backend.pojo.UserInterfaceInfo; +import com.book.backend.pojo.Users; +import com.book.backend.service.AiIntelligentService; +import com.book.backend.service.UserInterfaceInfoService; +import com.book.backend.service.UsersService; +import com.book.backend.service.impl.UserInterfaceInfoServiceImpl; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 删除用户调用AI模型的无效回复 + * + * @author 程序员小白条 + * + */ +// todo 取消 @Component 注释开启任务 +// 程序启动时执行一次,然后每隔1天自动执行定时任务 +//@Component +@EnableScheduling +@Slf4j +public class IncSyncDeleteAIMessage { + + @Resource + private AiIntelligentService aiIntelligentService; + @Resource + private UserInterfaceInfoService userInterfaceInfoService; + + + /** + * 每天执行一次 + * + */ + @Scheduled(fixedRate = 60 * 60 * 24 * 1000) + public void run() { + log.info("正在查询无效的AI回复数据"); + // 查询无效的数据,并进行删除操作 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.isNull(AiIntelligent::getAiResult).or().eq(AiIntelligent::getAiResult, ""); + List list = aiIntelligentService.list(queryWrapper); + // 先为用户恢复次数 + log.info("正在为用户恢复次数"); + list.forEach(item -> { + Long userId = item.getUserId(); + UserInterfaceInfo user = userInterfaceInfoService.getById(userId); + synchronized (IncSyncDeleteAIMessage.class) { + // 用户恢复次数 + if (user != null) { + user.setLeftNum(user.getLeftNum() + 1); + boolean save = userInterfaceInfoService.save(user); + if (!save) { + log.info("定时任务执行失败"); + throw new BusinessException(ErrorCode.OPERATION_ERROR, "操作定时任务失败"); + } + } + } + }); + // 将无效的记录删除 + log.info("正在删除无效的AI回复记录"); + boolean remove = false; + try { + remove = aiIntelligentService.remove(queryWrapper); + if (!remove) { + log.info("未发现无效的AI回复记录"); + } + } catch (Exception e) { + log.info("定时任务执行失败"); + throw new RuntimeException(e); + } + log.info("定时任务执行成功,删除无效的AI的结果集:"+list.size()+"个"); + + } +} diff --git a/src/main/java/com/book/backend/job/once/EasyExcelBatchImportBookList.java b/src/main/java/com/book/backend/job/once/EasyExcelBatchImportBookList.java new file mode 100644 index 0000000..6e74b80 --- /dev/null +++ b/src/main/java/com/book/backend/job/once/EasyExcelBatchImportBookList.java @@ -0,0 +1,67 @@ +package com.book.backend.job.once; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import com.book.backend.pojo.Books; +import com.book.backend.pojo.dto.BookData; +import com.book.backend.service.BooksService; +import com.book.backend.utils.NumberUtil; +import org.springframework.beans.BeanUtils; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.ArrayList; + +/** + * @author 小白条 + * @from 个人博客 + */ +// 取消注释后,每次执行springboot项目,都会执行一次run方法 +//@Component +public class EasyExcelBatchImportBookList implements CommandLineRunner { + @Resource + private BooksService booksService; + + @Override + public void run(String... args) throws Exception { + // 读取的excel文件路径 + String filename = "D:\\IDEAproject\\vue_book_backend\\src\\main\\resources\\test_excel.xlsx"; + // 读取excel + EasyExcel.read(filename, BookData.class, new AnalysisEventListener() { + ArrayList booksList = new ArrayList<>(); + // 每解析一行数据,该方法会被调用一次 + @Override + public void invoke(BookData bookData, AnalysisContext analysisContext) { + Books books = new Books(); + // 生成11位数字的图书编号 + StringBuilder stringBuilder = NumberUtil.getNumber(11); + long bookNumber = Long.parseLong(new String(stringBuilder)); + BeanUtils.copyProperties(bookData,books); + books.setBookNumber(bookNumber); + booksList.add(books); +// System.out.println("解析数据为:" + bookData.toString()); + } + // 全部解析完成被调用 + @Override + public void doAfterAllAnalysed(AnalysisContext analysisContext) { + // 全部加入到容器list中后,一次性批量导入,先判断容器是否为空 + if(!booksList.isEmpty()){ + // 可以将解析的数据保存到数据库 + boolean flag = booksService.saveBatch(booksList); + // 如果数据添加成功 + if(flag){ + System.out.println("Excel批量添加图书成功"); + }else{ + System.out.println("Excel批量添加图书失败"); + } + }else{ + System.out.println("空表无法进行数据导入"); + } + System.out.println("解析完成..."); + + } + }).sheet().doRead(); + } +} diff --git a/src/main/java/com/book/backend/job/once/FetchInitBookList.java b/src/main/java/com/book/backend/job/once/FetchInitBookList.java new file mode 100644 index 0000000..9549191 --- /dev/null +++ b/src/main/java/com/book/backend/job/once/FetchInitBookList.java @@ -0,0 +1,81 @@ +package com.book.backend.job.once; + +import cn.hutool.http.HttpRequest; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.book.backend.pojo.Books; +import com.book.backend.service.BooksService; +import com.book.backend.utils.NumberUtil; +import com.book.backend.utils.RandomNameUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * @author 小白条 + * @from 个人博客 + */ +// 取消注释后,每次执行springboot项目,都会执行一次run方法 +//@Component +@Slf4j +public class FetchInitBookList implements CommandLineRunner { + @Resource + private BooksService booksService; + + @Transactional(rollbackFor = Exception.class) + @Override + public void run(String... args) throws Exception { + String[] bookLibraries = {"南图", "北图", "教师之家"}; + String url = "https://api.ituring.com.cn/api/Search/Advanced"; + // 可以手动修改page来改变获取不同的图书 + String json = "{\n" + + " \"categoryId\": 1,\n" + + " \"sort\": \"new\",\n" + + " \"page\": 1,\n" + + " \"name\": \"\",\n" + + " \"edition\": 1\n" + + "}"; + String result2 = HttpRequest.post(url) + .body(json) + .execute().body(); + Map map = JSONUtil.toBean(result2, Map.class); + JSONArray bookItems = (JSONArray) map.get("bookItems"); + List booksList = new ArrayList<>(); + for (Object record : bookItems) { + int randomLibrary = NumberUtil.getLibraryInt(); + JSONObject tempRecord = (JSONObject) record; + Books books = new Books(); + // 生成11位数字的图书编号 + StringBuilder stringNumber = NumberUtil.getNumber(11); + long bookNumber = Long.parseLong(new String(stringNumber)); + String bookName = tempRecord.getStr("name"); + String author = tempRecord.getStr("translatorNameString"); + String bookDescription = tempRecord.getStr("abstract"); + if(bookDescription.length()>=255){ + bookDescription = bookDescription.substring(0,254); + } + books.setBookNumber(bookNumber); + books.setBookName(bookName); + books.setBookAuthor(author); + books.setBookLibrary(bookLibraries[randomLibrary]); + books.setBookType("计算机"); + String location = RandomNameUtils.getRandomLocation(); + books.setBookLocation(location); + books.setBookStatus("未借出"); + books.setBookDescription(bookDescription); + booksList.add(books); + } + boolean b = booksService.saveBatch(booksList); + if(b){ + log.info("批量添加图书成功,成功插入的条数为:{}",booksList.size()); + }else{ + log.error("批量添加图书失败"); + } + } +} diff --git a/src/main/java/com/book/backend/manager/AiManager.java b/src/main/java/com/book/backend/manager/AiManager.java new file mode 100644 index 0000000..609a6e6 --- /dev/null +++ b/src/main/java/com/book/backend/manager/AiManager.java @@ -0,0 +1,39 @@ +package com.book.backend.manager; + +import com.book.backend.common.exception.BusinessException; +import com.book.backend.common.exception.ErrorCode; +import com.yupi.yucongming.dev.client.YuCongMingClient; +import com.yupi.yucongming.dev.common.BaseResponse; +import com.yupi.yucongming.dev.model.DevChatRequest; +import com.yupi.yucongming.dev.model.DevChatResponse; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * 用于对接 AI 平台 + */ +@Service +public class AiManager { + + + + /** + * AI 对话 + * + * @param modelId + * @param message + * @return + */ + public String doChat(long modelId, String message) { + DevChatRequest devChatRequest = new DevChatRequest(); + devChatRequest.setModelId(modelId); + devChatRequest.setMessage(message); + YuCongMingClient yuCongMingClient = new YuCongMingClient("xxxxxxxx", "xxxxxx"); + BaseResponse response = yuCongMingClient.doChat(devChatRequest); + if (response == null) { + throw new BusinessException(ErrorCode.SYSTEM_ERROR, "AI 响应错误"); + } + return response.getData().getContent(); + } +} diff --git a/src/main/java/com/book/backend/manager/AiPost.java b/src/main/java/com/book/backend/manager/AiPost.java new file mode 100644 index 0000000..439d8b7 --- /dev/null +++ b/src/main/java/com/book/backend/manager/AiPost.java @@ -0,0 +1,79 @@ +package com.book.backend.manager; + +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import org.springframework.stereotype.Component; + +@Component +public class AiPost { + + private static final String baseUrl = "https://openai.933999.xyz"; + private static final String apiKey = "sk-1PBIyxIdJ42yyC11XRNqbEXYDt2eZRNVNbd8XxmKjnPXGh5S"; + private static final String model = "gpt-4o-mini"; + + /** + * 发送消息并获取AI响应 + * @param message 用户消息 + * @param maxTokens 最大token数 + * @return AI响应内容 + * @throws Exception 发送请求异常 + */ + public String sendMessageAndGetResponse(String message, int maxTokens) throws Exception { + // 构造 messages 数组 + JSONArray messagesArray = new JSONArray(); + JSONObject userMessage = new JSONObject(); + userMessage.put("role", "user"); + userMessage.put("content", message); + messagesArray.add(userMessage); + + // 构造请求体 + JSONObject requestBody = new JSONObject(); + requestBody.put("model", model); + requestBody.put("messages", messagesArray); +// requestBody.put("max_tokens", maxTokens); +// requestBody.put("temperature", 0.2); // 更稳定输出 + + // 打印请求体 + System.out.println("请求地址:" + baseUrl + "/v1/chat/completions"); + System.out.println("请求体:" + requestBody.toStringPretty()); + + try { + HttpResponse response = HttpRequest.post(baseUrl + "/v1/chat/completions") + .timeout(60_000) // 60秒超时 + .header("Authorization", "Bearer " + apiKey) + .header("Content-Type", "application/json") + .body(requestBody.toString()) + .execute(); + + String responseText = response.body(); + + System.out.println("响应状态码:" + response.getStatus()); + System.out.println("响应内容:" + responseText); + + if (response.getStatus() != 200) { + throw new RuntimeException("AI服务请求失败:" + responseText); + } + + // 安全解析响应 + if (!JSONUtil.isJsonObj(responseText)) { + throw new RuntimeException("返回不是合法JSON:" + responseText); + } + + JSONObject responseJson = JSONUtil.parseObj(responseText); + JSONArray choices = responseJson.getJSONArray("choices"); + + if (choices == null || choices.isEmpty()) { + throw new RuntimeException("AI响应为空:" + responseText); + } + + JSONObject messageObj = choices.getJSONObject(0).getJSONObject("message"); + return messageObj.getStr("content"); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("调用AI接口发生错误", e); + } + } +} diff --git a/src/main/java/com/book/backend/manager/AlibabaAIModel.java b/src/main/java/com/book/backend/manager/AlibabaAIModel.java new file mode 100644 index 0000000..8e1c274 --- /dev/null +++ b/src/main/java/com/book/backend/manager/AlibabaAIModel.java @@ -0,0 +1,84 @@ +package com.book.backend.manager; + +import cn.hutool.core.date.StopWatch; +import com.aliyun.broadscope.bailian.sdk.AccessTokenClient; +import com.aliyun.broadscope.bailian.sdk.ApplicationClient; +import com.aliyun.broadscope.bailian.sdk.models.BaiLianConfig; +import com.aliyun.broadscope.bailian.sdk.models.CompletionsRequest; +import com.aliyun.broadscope.bailian.sdk.models.CompletionsResponse; +import com.aliyun.broadscope.bailian.sdk.utils.UUIDGenerator; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 阿里AI模型 通义千问-Plus 工具类 + * + * @author 小白条 + * @from 个人博客 + */ +@Service +@Slf4j +public class AlibabaAIModel { + // todo 替换你自己的密钥 阿里百炼官网:https://account.aliyun.com/login/login.html + public static final String accessKeyId = "xxxx"; + public static final String accessKeySecret = "xxxxxx"; + public static final String agentKey = "xxxxx"; + public static final String appId = "xxxxx"; + /** + * 阿里云百炼也支持调用侧自己维护上下文对话历史, 同时传入sessionId和history,会优先采用调用侧传入的上下文历史 + * CompletionsRequest.ChatQaPair chatQaPair = new CompletionsRequest.ChatQaPair("我想去北京", "北京的天气很不错"); + * CompletionsRequest.ChatQaPair chatQaPair2 = new CompletionsRequest.ChatQaPair("北京有哪些景点", "北京有故宫、长城等"); + * history.add(chatQaPair); + * history.add(chatQaPair2); + * request.setHistory(history); + */ + public static final List history = new ArrayList<>(); + + public static String doChatOnce(String prompt) { + log.info("进入AI对话"); + AccessTokenClient accessTokenClient = new AccessTokenClient(accessKeyId, accessKeySecret, agentKey); + String token = accessTokenClient.getToken(); + + BaiLianConfig config = new BaiLianConfig() + .setApiKey(token); + CompletionsRequest request = new CompletionsRequest() + .setAppId(appId) + .setPrompt(prompt); + + ApplicationClient client = new ApplicationClient(config); + CompletionsResponse response = client.completions(request); + log.info("AI对话结束"); + return response.getData().getText(); + } + + public static String doChatWithHistory(String prompt, List recentHistory) { + log.info("进入AI对话"); + AccessTokenClient accessTokenClient = new AccessTokenClient(accessKeyId, accessKeySecret, agentKey); + String token = accessTokenClient.getToken(); + + BaiLianConfig config = new BaiLianConfig() + .setApiKey(token); + // 通过sessionId 判断是否为同一个用户 + String sessionId = UUIDGenerator.generate(); + // 将该用户最近的五条历史记录加入到绘画中 + recentHistory.forEach(item -> { + CompletionsRequest.ChatQaPair chatQaPair = new CompletionsRequest.ChatQaPair(item[0], item[1]); + history.add(chatQaPair); + }); + + CompletionsRequest request = new CompletionsRequest() + .setAppId(appId) + .setPrompt(prompt) + .setSessionId(sessionId) + .setHistory(history); + ApplicationClient client = new ApplicationClient(config); + CompletionsResponse response = client.completions(request); + log.info("AI对话结束"); + return response.getData().getText(); + } +} diff --git a/src/main/java/com/book/backend/manager/GuavaRateLimiterManager.java b/src/main/java/com/book/backend/manager/GuavaRateLimiterManager.java new file mode 100644 index 0000000..05165c4 --- /dev/null +++ b/src/main/java/com/book/backend/manager/GuavaRateLimiterManager.java @@ -0,0 +1,27 @@ +package com.book.backend.manager; + +import com.google.common.util.concurrent.RateLimiter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * @author 小白条 + * @from 个人博客 + */ +@Service +@Slf4j +public class GuavaRateLimiterManager { + public boolean doRateLimit(Long userId) { + // 每秒允许的最大访问速率为1个许可 + RateLimiter rateLimiter = RateLimiter.create(1); + if (rateLimiter.tryAcquire()) { + // 可以进行处理的代码,表示限流允许通过 + log.info("用户id: " + userId + "请求一个令牌成功"); + return true; + } else { + // 限流超过了速率限制的处理代码 + log.info("用户id: " + userId + "请求一个令牌失败"); + return false; + } + } +} diff --git a/src/main/java/com/book/backend/manager/SparkAIManager.java b/src/main/java/com/book/backend/manager/SparkAIManager.java new file mode 100644 index 0000000..ffbbf77 --- /dev/null +++ b/src/main/java/com/book/backend/manager/SparkAIManager.java @@ -0,0 +1,307 @@ +package com.book.backend.manager; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.google.gson.Gson; +import okhttp3.*; +import org.springframework.stereotype.Service; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.concurrent.TimeUnit; +@Service +public class SparkAIManager extends WebSocketListener { + // 地址与鉴权信息 https://spark-api.xf-yun.com/v1.1/chat 1.5地址 domain参数为general + // 地址与鉴权信息 https://spark-api.xf-yun.com/v2.1/chat 2.0地址 domain参数为generalv2 + // todo 目前已经有三种版本 请根据您的模型替换 hostURL 讯飞星火官网地址:https://xinghuo.xfyun.cn/sparkapi + public static final String hostUrl = "https://spark-api.xf-yun.com/v2.1/chat"; + // todo 替换成你自己的 appid 图书管理系统 1.1 版本 1.2版本请找 AiIntelligentServiceImpl 替换代码块即可 + public static final String appid = "xxxxx"; + // todo 替换成你自己的 apiSecret 图书管理系统 1.1 版本 + public static final String apiSecret = "xxxxx"; + // todo 替换成你自己的 apiKey 图书管理系统 1.1 版本 + public static final String apiKey = "xxxxx"; + + public static List historyList=new ArrayList<>(); // 对话历史存储集合 + + public static String totalAnswer=""; // 大模型的答案汇总 + + // 环境治理的重要性 环保 人口老龄化 我爱我的祖国 + public static String NewQuestion = ""; + + public static final Gson gson = new Gson(); + public static MyThread myThread; + // 个性化参数 + private String userId; + private Boolean wsCloseFlag; + + private static Boolean totalFlag=true; // 控制提示用户是否输入 + // 构造函数 + public SparkAIManager(String userId, Boolean wsCloseFlag) { + this.userId = userId; + this.wsCloseFlag = wsCloseFlag; + } + public SparkAIManager(){ + + } + // 主函数 + public String sendMessageAndGetResponse(String message,Integer sleepTime) throws Exception { + // 个性化参数入口,如果是并发使用,可以在这里模拟 + + while (true){ + if(totalFlag){ + System.out.print("我:"); + totalFlag=false; + NewQuestion=message; + // 构建鉴权url + String authUrl = getAuthUrl(hostUrl, apiKey, apiSecret); + OkHttpClient client = new OkHttpClient.Builder().build(); + String url = authUrl.replace("http://", "ws://").replace("https://", "wss://"); + Request request = new Request.Builder().url(url).build(); + for (int i = 0; i < 1; i++) { + totalAnswer=""; + WebSocket webSocket = client.newWebSocket(request, new SparkAIManager(i + "", + false)); + } + }else{ + TimeUnit.SECONDS.sleep(sleepTime); + return totalAnswer; + + + } + } + } + + public static boolean canAddHistory(){ // 由于历史记录最大上线1.2W左右,需要判断是能能加入历史 + int history_length=0; + for(RoleContent temp:historyList){ + history_length=history_length+temp.content.length(); + } + if(history_length>12000){ + historyList.remove(0); + historyList.remove(1); + historyList.remove(2); + historyList.remove(3); + historyList.remove(4); + return false; + }else{ + return true; + } + } + + // 线程来发送音频与参数 + class MyThread extends Thread { + private WebSocket webSocket; + + public MyThread(WebSocket webSocket) { + this.webSocket = webSocket; + } + + public void run() { + try { + JSONObject requestJson=new JSONObject(); + + JSONObject header=new JSONObject(); // header参数 + header.put("app_id",appid); + header.put("uid",UUID.randomUUID().toString().substring(0, 10)); + + JSONObject parameter=new JSONObject(); // parameter参数 + JSONObject chat=new JSONObject(); + chat.put("domain","generalv2"); + chat.put("temperature",0.5); + chat.put("max_tokens",4096); + parameter.put("chat",chat); + + JSONObject payload=new JSONObject(); // payload参数 + JSONObject message=new JSONObject(); + JSONArray text=new JSONArray(); + + // 历史问题获取 +// if(historyList.size()>0){ +// for(RoleContent tempRoleContent:historyList){ +// text.add(JSON.toJSON(tempRoleContent)); +// } +// } + + // 最新问题 + RoleContent roleContent=new RoleContent(); + roleContent.role="user"; + roleContent.content=NewQuestion; + text.add(JSON.toJSON(roleContent)); +// historyList.add(roleContent); + + + message.put("text",text); + payload.put("message",message); + + + requestJson.put("header",header); + requestJson.put("parameter",parameter); + requestJson.put("payload",payload); + // System.err.println(requestJson); // 可以打印看每次的传参明细 + webSocket.send(requestJson.toString()); + // 等待服务端返回完毕后关闭 + while (true) { + // System.err.println(wsCloseFlag + "---"); + Thread.sleep(200); + if (wsCloseFlag) { + break; + } + } + webSocket.close(1000, ""); + myThread.interrupt(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @Override + public void onOpen(WebSocket webSocket, Response response) { + super.onOpen(webSocket, response); + System.out.print("大模型:"); + myThread= new MyThread(webSocket); + myThread.start(); + + } + + @Override + public void onMessage(WebSocket webSocket, String text) { + // System.out.println(userId + "用来区分那个用户的结果" + text); + JsonParse myJsonParse = gson.fromJson(text, JsonParse.class); + if (myJsonParse.header.code != 0) { + System.out.println("发生错误,错误码为:" + myJsonParse.header.code); + System.out.println("本次请求的sid为:" + myJsonParse.header.sid); + webSocket.close(1000, ""); + } + List textList = myJsonParse.payload.choices.text; + for (Text temp : textList) { +// System.out.print(temp.content); + totalAnswer=totalAnswer+temp.content; + } + if (myJsonParse.header.status == 2) { + // 可以关闭连接,释放资源 + System.out.println(); + System.out.println("*************************************************************************************"); + if(canAddHistory()){ + RoleContent roleContent=new RoleContent(); + roleContent.setRole("assistant"); + roleContent.setContent(totalAnswer); +// historyList.add(roleContent); + }else{ +// historyList.remove(0); + RoleContent roleContent=new RoleContent(); + roleContent.setRole("assistant"); + roleContent.setContent(totalAnswer); +// historyList.add(roleContent); + } + wsCloseFlag = true; + totalFlag=true; + } + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, Response response) { + super.onFailure(webSocket, t, response); + try { + if (null != response) { + int code = response.code(); + System.out.println("onFailure code:" + code); + System.out.println("onFailure body:" + response.body().string()); + if (101 != code) { + System.out.println("connection failed"); + System.exit(0); + } + } + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + + // 鉴权方法 + public static String getAuthUrl(String hostUrl, String apiKey, String apiSecret) throws Exception { + URL url = new URL(hostUrl); + // 时间 + SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); + format.setTimeZone(TimeZone.getTimeZone("GMT")); + String date = format.format(new Date()); + // 拼接 + String preStr = "host: " + url.getHost() + "\n" + + "date: " + date + "\n" + + "GET " + url.getPath() + " HTTP/1.1"; + // System.err.println(preStr); + // SHA256加密 + Mac mac = Mac.getInstance("hmacsha256"); + SecretKeySpec spec = new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "hmacsha256"); + mac.init(spec); + + byte[] hexDigits = mac.doFinal(preStr.getBytes(StandardCharsets.UTF_8)); + // Base64加密 + String sha = Base64.getEncoder().encodeToString(hexDigits); + // System.err.println(sha); + // 拼接 + String authorization = String.format("api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", apiKey, "hmac-sha256", "host date request-line", sha); + // 拼接地址 + HttpUrl httpUrl = Objects.requireNonNull(HttpUrl.parse("https://" + url.getHost() + url.getPath())).newBuilder().// + addQueryParameter("authorization", Base64.getEncoder().encodeToString(authorization.getBytes(StandardCharsets.UTF_8))).// + addQueryParameter("date", date).// + addQueryParameter("host", url.getHost()).// + build(); + + // System.err.println(httpUrl.toString()); + return httpUrl.toString(); + } + + //返回的json结果拆解 + class JsonParse { + Header header; + Payload payload; + } + + class Header { + int code; + int status; + String sid; + } + + class Payload { + Choices choices; + } + + class Choices { + List text; + } + + class Text { + String role; + String content; + } + class RoleContent{ + String role; + String content; + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + } +} diff --git a/src/main/java/com/book/backend/manager/SparkClient.java b/src/main/java/com/book/backend/manager/SparkClient.java new file mode 100644 index 0000000..03a74be --- /dev/null +++ b/src/main/java/com/book/backend/manager/SparkClient.java @@ -0,0 +1,109 @@ +package com.book.backend.manager; + + +import com.book.backend.manager.constant.SparkApiVersion; +import com.book.backend.manager.exception.SparkException; +import com.book.backend.manager.listener.SparkBaseListener; +import com.book.backend.manager.listener.SparkSyncChatListener; +import com.book.backend.manager.model.SparkSyncChatResponse; +import com.book.backend.manager.model.request.SparkRequest; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * XfSparkClient + * + * @author briqt + */ +public class SparkClient { + + public String appid; + + public String apiKey; + + public String apiSecret; + + public OkHttpClient client = new OkHttpClient.Builder().build(); + + public void chatStream(SparkRequest sparkRequest, SparkBaseListener listener) { + sparkRequest.getHeader().setAppId(appid); + listener.setSparkRequest(sparkRequest); + + SparkApiVersion apiVersion = sparkRequest.getApiVersion(); + String apiUrl = apiVersion.getUrl(); + + // 构建鉴权url + String authWsUrl = null; + try { + authWsUrl = getAuthUrl(apiUrl).replace("http://", "ws://").replace("https://", "wss://"); + } catch (Exception e) { + throw new SparkException(500, "构建鉴权url失败", e); + } + // 创建请求 + Request request = new Request.Builder().url(authWsUrl).build(); + // 发送请求 + client.newWebSocket(request, listener); + } + + public SparkSyncChatResponse chatSync(SparkRequest sparkRequest) { + SparkSyncChatResponse chatResponse = new SparkSyncChatResponse(); + SparkSyncChatListener syncChatListener = new SparkSyncChatListener(chatResponse); + this.chatStream(sparkRequest, syncChatListener); + while (!chatResponse.isOk()) { + try { + Thread.sleep(200); + } catch (InterruptedException ignored) { + } + } + Throwable exception = chatResponse.getException(); + if (exception != null) { + if (!(exception instanceof SparkException)) { + exception = new SparkException(500, exception.getMessage()); + } + throw (SparkException) exception; + } + return chatResponse; + } + + /** + * 获取认证之后的URL + */ + public String getAuthUrl(String apiUrl) throws Exception { + URL url = new URL(apiUrl); + // 时间 + SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); + format.setTimeZone(TimeZone.getTimeZone("GMT")); + String date = format.format(new Date()); + // 拼接 + String preStr = "host: " + url.getHost() + "\n" + + "date: " + date + "\n" + + "GET " + url.getPath() + " HTTP/1.1"; + // SHA256加密 + Mac mac = Mac.getInstance("hmacsha256"); + SecretKeySpec spec = new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "hmacsha256"); + mac.init(spec); + + byte[] hexDigits = mac.doFinal(preStr.getBytes(StandardCharsets.UTF_8)); + // Base64加密 + String sha = Base64.getEncoder().encodeToString(hexDigits); + + // 拼接 + String authorization = String.format("api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", apiKey, "hmac-sha256", "host date request-line", sha); + // 拼接地址 + HttpUrl httpUrl = Objects.requireNonNull(HttpUrl.parse("https://" + url.getHost() + url.getPath())).newBuilder().// + addQueryParameter("authorization", Base64.getEncoder().encodeToString(authorization.getBytes(StandardCharsets.UTF_8))).// + addQueryParameter("date", date).// + addQueryParameter("host", url.getHost()).// + build(); + + return httpUrl.toString(); + } +} diff --git a/src/main/java/com/book/backend/manager/constant/SparkApiVersion.java b/src/main/java/com/book/backend/manager/constant/SparkApiVersion.java new file mode 100644 index 0000000..29187a2 --- /dev/null +++ b/src/main/java/com/book/backend/manager/constant/SparkApiVersion.java @@ -0,0 +1,50 @@ +package com.book.backend.manager.constant; + +/** + * SparkApiVersion + * + * @author briqt + * @date 2023/8/31 + */ +public enum SparkApiVersion { + + /** + * 1.5版本 + */ + V1_5("v1.1", "https://spark-api.xf-yun.com/v1.1/chat", "general"), + + /** + * 2.0版本 + */ + V2_0("v2.1", "https://spark-api.xf-yun.com/v2.1/chat", "generalv2"), + + /** + * 3.0版本 + */ + V3_0("v3.1", "https://spark-api.xf-yun.com/v3.1/chat", "generalv3"), + ; + + SparkApiVersion(String version, String url, String domain) { + this.version = version; + this.url = url; + this.domain = domain; + } + + private final String version; + + private final String url; + + private final String domain; + + public String getVersion() { + return version; + } + + public String getUrl() { + return url; + } + + public String getDomain() { + return domain; + } +} diff --git a/src/main/java/com/book/backend/manager/constant/SparkErrorCode.java b/src/main/java/com/book/backend/manager/constant/SparkErrorCode.java new file mode 100644 index 0000000..0a4c203 --- /dev/null +++ b/src/main/java/com/book/backend/manager/constant/SparkErrorCode.java @@ -0,0 +1,43 @@ +package com.book.backend.manager.constant; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * SparkErrorCode + * + * @author briqt + */ +public class SparkErrorCode { + public static final Map RESPONSE_ERROR_MAP = new LinkedHashMap<>(32); + + static { + RESPONSE_ERROR_MAP.put(10000, "升级为ws出现错误"); + RESPONSE_ERROR_MAP.put(10001, "通过ws读取用户的消息出错"); + RESPONSE_ERROR_MAP.put(10002, "通过ws向用户发送消息错误"); + RESPONSE_ERROR_MAP.put(10003, "用户的消息格式有错误"); + RESPONSE_ERROR_MAP.put(10004, "用户数据的schema错误"); + RESPONSE_ERROR_MAP.put(10005, "用户参数值有错误"); + RESPONSE_ERROR_MAP.put(10006, "用户并发错误:当前用户已连接,同一用户不能多处同时连接"); + RESPONSE_ERROR_MAP.put(10007, "用户流量受限:服务正在处理用户当前的问题,需等待处理完成后再发送新的请求"); + RESPONSE_ERROR_MAP.put(10008, "服务容量不足,联系工作人员"); + RESPONSE_ERROR_MAP.put(10009, "和引擎建立连接失败"); + RESPONSE_ERROR_MAP.put(10010, "接收引擎数据的错误"); + RESPONSE_ERROR_MAP.put(10011, "发送数据给引擎的错误"); + RESPONSE_ERROR_MAP.put(10012, "引擎内部错误"); + RESPONSE_ERROR_MAP.put(10013, "输入内容审核不通过,涉嫌违规,请重新调整输入内容"); + RESPONSE_ERROR_MAP.put(10014, "输出内容涉及敏感信息,审核不通过,后续结果无法展示给用户"); + RESPONSE_ERROR_MAP.put(10015, "appid在黑名单中"); + RESPONSE_ERROR_MAP.put(10016, "appid授权类的错误。未开通此功能,未开通对应版本,token不足,并发超过授权等等"); + RESPONSE_ERROR_MAP.put(10017, "清除历史失败"); + RESPONSE_ERROR_MAP.put(10019, "本次会话内容有涉及违规信息的倾向"); + RESPONSE_ERROR_MAP.put(10110, "服务忙,请稍后再试"); + RESPONSE_ERROR_MAP.put(10163, "请求引擎的参数异常,引擎的schema检查不通过"); + RESPONSE_ERROR_MAP.put(10222, "引擎网络异常"); + RESPONSE_ERROR_MAP.put(10907, "token数量超过上限。对话历史+问题的字数太多,需要精简输入"); + RESPONSE_ERROR_MAP.put(11200, "授权错误:该appId没有相关功能的授权或者业务量超过限制"); + RESPONSE_ERROR_MAP.put(11201, "授权错误:日流控超限。超过当日最大访问量的限制"); + RESPONSE_ERROR_MAP.put(11202, "授权错误:秒级流控超限。秒级并发超过授权路数限制"); + RESPONSE_ERROR_MAP.put(11203, "授权错误:并发流控超限。并发路数超过授权路数限制"); + } +} diff --git a/src/main/java/com/book/backend/manager/constant/SparkMessageRole.java b/src/main/java/com/book/backend/manager/constant/SparkMessageRole.java new file mode 100644 index 0000000..f47e4af --- /dev/null +++ b/src/main/java/com/book/backend/manager/constant/SparkMessageRole.java @@ -0,0 +1,25 @@ +package com.book.backend.manager.constant; + +/** + * 消息角色常量 + * + * @author briqt + */ +public interface SparkMessageRole { + + /** + * 用户 + */ + String USER = "user"; + + /** + * 机器人助手 + */ + String ASSISTANT = "assistant"; + + /** + * 系统指令 + */ + String SYSTEM = "system"; + +} diff --git a/src/main/java/com/book/backend/manager/exception/SparkException.java b/src/main/java/com/book/backend/manager/exception/SparkException.java new file mode 100644 index 0000000..f3e3eea --- /dev/null +++ b/src/main/java/com/book/backend/manager/exception/SparkException.java @@ -0,0 +1,52 @@ +package com.book.backend.manager.exception; + + +import com.book.backend.manager.constant.SparkErrorCode; + +/** + * SparkException + * + * @author briqt + */ +public class SparkException extends RuntimeException { + + private static final long serialVersionUID = 3053312855506511893L; + + private Integer code; + + private String sid; + + public SparkException(Integer code, String message) { + super(message); + this.code = code; + } + + public SparkException(Integer code, String message, Throwable t) { + super(message, t); + this.code = code; + } + + public static SparkException bizFailed(Integer code) { + String errorMessage = SparkErrorCode.RESPONSE_ERROR_MAP.get(code); + if (null == errorMessage) { + errorMessage = "未知的错误码"; + } + return new SparkException(code, errorMessage); + } + + public Integer getCode() { + return code; + } + + public void setCode(Integer code) { + this.code = code; + } + + public String getSid() { + return sid; + } + + public void setSid(String sid) { + this.sid = sid; + } +} diff --git a/src/main/java/com/book/backend/manager/listener/SparkBaseListener.java b/src/main/java/com/book/backend/manager/listener/SparkBaseListener.java new file mode 100644 index 0000000..7061972 --- /dev/null +++ b/src/main/java/com/book/backend/manager/listener/SparkBaseListener.java @@ -0,0 +1,146 @@ +package com.book.backend.manager.listener; + +import com.book.backend.manager.exception.SparkException; +import com.book.backend.manager.model.SparkMessage; +import com.book.backend.manager.model.request.SparkRequest; +import com.book.backend.manager.model.response.*; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +/** + * SparkBaseListener + * + * @author briqt + */ +public class SparkBaseListener extends WebSocketListener { + private static final Logger logger = LoggerFactory.getLogger(SparkBaseListener.class); + + private SparkRequest sparkRequest; + + public ObjectMapper objectMapper; + + public SparkBaseListener() { + objectMapper = new ObjectMapper(); + // 排除值为null的字段 + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + // 设置全局忽略未知属性 + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + /** + * 收到回答时会调用此方法 + * + * @param content 回答内容 + * @param usage tokens消耗统计 + * @param status 会话状态,取值为[0,1,2];0代表首次结果;1代表中间结果;2代表最后一个结果,当status为2时,webSocket连接会自动关闭 + * @param sparkRequest 本次会话的请求参数 + * @param sparkResponse 本次回调的响应数据 + * @param webSocket 本次会话的webSocket连接 + */ + public void onMessage(String content, SparkResponseUsage usage, Integer status, SparkRequest sparkRequest, SparkResponse sparkResponse, WebSocket webSocket) { + // 重写此方法,实现业务逻辑 + } + + /** + * 收到functionCall调用此方法 + * + * @param functionCall functionCall + * @param usage tokens消耗统计 + * @param status 会话状态,取值为[0,1,2];0代表首次结果;1代表中间结果;2代表最后一个结果,当status为2时,webSocket连接会自动关闭 + * @param sparkRequest 本次会话的请求参数 + * @param sparkResponse 本次回调的响应数据 + * @param webSocket 本次会话的webSocket连接 + */ + public void onFunctionCall(SparkResponseFunctionCall functionCall, SparkResponseUsage usage, Integer status, SparkRequest sparkRequest, SparkResponse sparkResponse, WebSocket webSocket) { + // 重写此方法,实现业务逻辑 + } + + @Override + public final void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) { + // 发送消息 + String requestJson = null; + try { + requestJson = objectMapper.writeValueAsString(sparkRequest); + } catch (JsonProcessingException e) { + throw new SparkException(400, "请求数据 SparkRequest 序列化失败", e); + } + webSocket.send(requestJson); + } + + @Override + public final void onMessage(@NotNull WebSocket webSocket, @NotNull String text) { + SparkResponse sparkResponse; + + // 解析响应 + try { + sparkResponse = objectMapper.readValue(text, SparkResponse.class); + } catch (JsonProcessingException e) { + webSocket.close(1000, ""); + throw new SparkException(500, "响应数据 SparkResponse 解析失败:" + text, e); + } + SparkResponseHeader header = sparkResponse.getHeader(); + if (null == header) { + webSocket.close(1000, ""); + throw new SparkException(500, "响应数据不完整 SparkResponse.header为null,完整响应:" + text); + } + + // 业务状态判断 + Integer code = header.getCode(); + if (0 != code) { + webSocket.close(1000, ""); + throw SparkException.bizFailed(code); + } + + // 回答文本 + SparkResponseChoices choices = sparkResponse.getPayload().getChoices(); + List messages = choices.getText(); + StringBuilder stringBuilder = new StringBuilder(); + SparkResponseFunctionCall functionCall = null; + + SparkResponseUsage usage = sparkResponse.getPayload().getUsage(); + Integer status = header.getStatus(); + + for (SparkMessage message : messages) { + if (message.getFunction_call() != null) { + functionCall = message.getFunction_call(); + break; + } + stringBuilder.append(message.getContent()); + } + if (functionCall != null) { + this.onFunctionCall(functionCall, usage, status, sparkRequest, sparkResponse, webSocket); + } else { + String content = stringBuilder.toString(); + this.onMessage(content, usage, status, sparkRequest, sparkResponse, webSocket); + } + + // 最后一条结果,关闭连接 + if (2 == status) { + webSocket.close(1000, ""); + } + } + + @Override + public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, Response response) { + logger.error("讯飞星火api发生异常:", t); + } + + public SparkRequest getSparkRequest() { + return sparkRequest; + } + + public void setSparkRequest(SparkRequest sparkRequest) { + this.sparkRequest = sparkRequest; + } +} diff --git a/src/main/java/com/book/backend/manager/listener/SparkConsoleListener.java b/src/main/java/com/book/backend/manager/listener/SparkConsoleListener.java new file mode 100644 index 0000000..ae7c306 --- /dev/null +++ b/src/main/java/com/book/backend/manager/listener/SparkConsoleListener.java @@ -0,0 +1,98 @@ +package com.book.backend.manager.listener; + +import com.book.backend.manager.constant.SparkApiVersion; +import com.book.backend.manager.model.SparkMessage; +import com.book.backend.manager.model.request.SparkRequest; +import com.book.backend.manager.model.response.SparkResponse; +import com.book.backend.manager.model.response.SparkResponseFunctionCall; +import com.book.backend.manager.model.response.SparkResponseUsage; +import com.book.backend.manager.model.response.SparkTextUsage; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import okhttp3.Response; +import okhttp3.WebSocket; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Map; + +/** + * SparkConsoleListener + * + * @author briqt + */ +public class SparkConsoleListener extends SparkBaseListener { + private static final Logger logger = LoggerFactory.getLogger(SparkConsoleListener.class); + + private final StringBuilder stringBuilder = new StringBuilder(); + + public ObjectMapper objectMapper = new ObjectMapper(); + + { + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + } + + @Override + public void onMessage(String content, SparkResponseUsage usage, Integer status, SparkRequest sparkRequest, SparkResponse sparkResponse, WebSocket webSocket) { + stringBuilder.append(content); + + if (0 == status) { + List messages = sparkRequest.getPayload().getMessage().getText(); + try { + SparkApiVersion apiVersion = sparkRequest.getApiVersion(); + System.out.println("请求地址:" + apiVersion.getUrl()+" 版本:"+apiVersion.getVersion()); + System.out.println("\n提问:" + objectMapper.writeValueAsString(messages)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + System.out.println("\n收到回答:\n"); + } + + try { + System.out.println("--content:" + content + " --完整响应:" + objectMapper.writeValueAsString(sparkResponse)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + + if (2 == status) { + System.out.println("\n完整回答:" + stringBuilder); + SparkTextUsage textUsage = usage.getText(); + System.out.println("\n回答结束;提问tokens:" + textUsage.getPromptTokens() + + ",回答tokens:" + textUsage.getCompletionTokens() + + ",总消耗tokens:" + textUsage.getTotalTokens()); + } + } + + @Override + public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, Response response) { + logger.error("讯飞星火api发生异常:", t); + } + + /** + * 收到functionCall调用此方法 + * + * @param functionCall functionCall + * @param usage tokens消耗统计 + * @param status 会话状态,取值为[0,1,2];0代表首次结果;1代表中间结果;2代表最后一个结果,当status为2时,webSocket连接会自动关闭 + * @param sparkRequest 本次会话的请求参数 + * @param sparkResponse 本次回调的响应数据 + * @param webSocket 本次会话的webSocket连接 + */ + @Override + public void onFunctionCall(SparkResponseFunctionCall functionCall, SparkResponseUsage usage, Integer status, SparkRequest sparkRequest, SparkResponse sparkResponse, WebSocket webSocket) { + String functionCallName = functionCall.getName(); + Map arguments = functionCall.getMapArguments(); + + // 在这里根据方法名和参数自行调用方法实现 + + try { + System.out.println("\n收到functionCall:方法名称:" + functionCallName + ",参数:" + objectMapper.writeValueAsString(arguments)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/book/backend/manager/listener/SparkSyncChatListener.java b/src/main/java/com/book/backend/manager/listener/SparkSyncChatListener.java new file mode 100644 index 0000000..c62286c --- /dev/null +++ b/src/main/java/com/book/backend/manager/listener/SparkSyncChatListener.java @@ -0,0 +1,65 @@ +package com.book.backend.manager.listener; + + +import com.book.backend.manager.model.SparkSyncChatResponse; +import com.book.backend.manager.model.request.SparkRequest; +import com.book.backend.manager.model.response.SparkResponse; +import com.book.backend.manager.model.response.SparkResponseFunctionCall; +import com.book.backend.manager.model.response.SparkResponseUsage; +import okhttp3.Response; +import okhttp3.WebSocket; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * SparkSyncChatListener + * + * @author briqt + */ +public class SparkSyncChatListener extends SparkBaseListener { + private static final Logger logger = LoggerFactory.getLogger(SparkSyncChatListener.class); + + private final StringBuilder stringBuilder = new StringBuilder(); + + private final SparkSyncChatResponse sparkSyncChatResponse; + + public SparkSyncChatListener(SparkSyncChatResponse sparkSyncChatResponse) { + this.sparkSyncChatResponse = sparkSyncChatResponse; + } + + @Override + public void onMessage(String content, SparkResponseUsage usage, Integer status, SparkRequest sparkRequest, SparkResponse sparkResponse, WebSocket webSocket) { + stringBuilder.append(content); + if (2 == status) { + sparkSyncChatResponse.setContent(stringBuilder.toString()); + sparkSyncChatResponse.setTextUsage(usage.getText()); + sparkSyncChatResponse.setOk(true); + } + } + + @Override + public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, Response response) { + logger.error("讯飞星火api发生异常", t); + sparkSyncChatResponse.setOk(true); + sparkSyncChatResponse.setException(t); + } + + /** + * 收到functionCall调用此方法 + * + * @param functionCall functionCall + * @param sparkRequest 本次会话的请求参数 + * @param sparkResponse 本次回调的响应数据 + * @param webSocket 本次会话的webSocket连接 + */ + @Override + public void onFunctionCall(SparkResponseFunctionCall functionCall, SparkResponseUsage usage, Integer status, SparkRequest sparkRequest, SparkResponse sparkResponse, WebSocket webSocket) { + if (2 == status) { + sparkSyncChatResponse.setContent(stringBuilder.toString()); + sparkSyncChatResponse.setTextUsage(usage.getText()); + sparkSyncChatResponse.setFunctionCall(functionCall); + sparkSyncChatResponse.setOk(true); + } + } +} diff --git a/src/main/java/com/book/backend/manager/model/SparkChatParameter.java b/src/main/java/com/book/backend/manager/model/SparkChatParameter.java new file mode 100644 index 0000000..d183a3b --- /dev/null +++ b/src/main/java/com/book/backend/manager/model/SparkChatParameter.java @@ -0,0 +1,94 @@ +package com.book.backend.manager.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; + +/** + * $.parameter.chat + * + * @author briqt + */ +public class SparkChatParameter implements Serializable { + private static final long serialVersionUID = -1815416415486571475L; + + /** + * 指定访问的领域
+ * 必传,默认取值为 generalv2 + */ + private String domain = "generalv2"; + + /** + * 核采样阈值。用于决定结果随机性,取值越高随机性越强即相同的问题得到的不同答案的可能性越高
+ * 非必传,取值为[0,1],默认为0.5 + */ + private Double temperature; + + /** + * 模型回答的tokens的最大长度
+ * + * V1.5取值为[1,4096],默认为2048 + * V2.0取值为[1,8192],默认为2048。 + * V3.0取值为[1,8192],默认为2048。 + */ + @JsonProperty("max_tokens") + private Integer maxTokens; + + /** + * 从k个候选中随机选择⼀个(⾮等概率)
+ * 非必传,取值为[1,6],默认为4 + */ + @JsonProperty("top_k") + private Integer topK; + + /** + * 用于关联用户会话
+ * 非必传,需要保障用户下的唯一性 + */ + @JsonProperty("chat_id") + private String chatId; + + public static SparkChatParameter defaultParameter() { + return new SparkChatParameter(); + } + + public String getDomain() { + return domain; + } + + public void setDomain(String domain) { + this.domain = domain; + } + + public Double getTemperature() { + return temperature; + } + + public void setTemperature(Double temperature) { + this.temperature = temperature; + } + + public Integer getMaxTokens() { + return maxTokens; + } + + public void setMaxTokens(Integer maxTokens) { + this.maxTokens = maxTokens; + } + + public Integer getTopK() { + return topK; + } + + public void setTopK(Integer topK) { + this.topK = topK; + } + + public String getChatId() { + return chatId; + } + + public void setChatId(String chatId) { + this.chatId = chatId; + } +} diff --git a/src/main/java/com/book/backend/manager/model/SparkMessage.java b/src/main/java/com/book/backend/manager/model/SparkMessage.java new file mode 100644 index 0000000..cfc42bd --- /dev/null +++ b/src/main/java/com/book/backend/manager/model/SparkMessage.java @@ -0,0 +1,112 @@ +package com.book.backend.manager.model; + + +import com.book.backend.manager.constant.SparkMessageRole; +import com.book.backend.manager.model.response.SparkResponseFunctionCall; + +/** + * 消息 + * + * @author briqt + */ +public class SparkMessage { + + /** + * 角色 + */ + private String role; + + /** + * 内容类型 + */ + private String content_type; + + /** + * 函数调用 + */ + private SparkResponseFunctionCall function_call; + + /** + * 内容 + */ + private String content; + + /** + * 响应时独有,请求入参请忽略 + */ + private String index; + + /** + * 创建用户消息 + * + * @param content 内容 + */ + public static SparkMessage userContent(String content) { + return new SparkMessage(SparkMessageRole.USER, content); + } + + /** + * 创建机器人消息 + * + * @param content 内容 + */ + public static SparkMessage assistantContent(String content) { + return new SparkMessage(SparkMessageRole.ASSISTANT, content); + } + + /** + * 创建system指令 + * @param content 内容 + */ + public static SparkMessage systemContent(String content) { + return new SparkMessage(SparkMessageRole.SYSTEM, content); + } + + public SparkMessage() { + } + + public SparkMessage(String role, String content) { + this.role = role; + this.content = content; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getIndex() { + return index; + } + + public String getContent_type() { + return content_type; + } + + public void setContent_type(String content_type) { + this.content_type = content_type; + } + + public SparkResponseFunctionCall getFunction_call() { + return function_call; + } + + public void setFunction_call(SparkResponseFunctionCall function_call) { + this.function_call = function_call; + } + + public void setIndex(String index) { + this.index = index; + } +} diff --git a/src/main/java/com/book/backend/manager/model/SparkRequestBuilder.java b/src/main/java/com/book/backend/manager/model/SparkRequestBuilder.java new file mode 100644 index 0000000..8af4437 --- /dev/null +++ b/src/main/java/com/book/backend/manager/model/SparkRequestBuilder.java @@ -0,0 +1,130 @@ +package com.book.backend.manager.model; + + +import com.book.backend.manager.constant.SparkApiVersion; +import com.book.backend.manager.model.request.*; +import com.book.backend.manager.model.request.function.SparkRequestFunctionMessage; +import com.book.backend.manager.model.request.function.SparkRequestFunctions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +/** + * SparkRequestBuilder + * + * @author briqt + */ +public class SparkRequestBuilder { + private static final Logger logger = LoggerFactory.getLogger(SparkRequestBuilder.class); + + private final SparkRequest sparkRequest; + + public SparkRequestBuilder() { + sparkRequest = new SparkRequest(); + + // header + sparkRequest.setHeader(new SparkRequestHeader()); + + // parameter + sparkRequest.setParameter(new SparkRequestParameter(new SparkChatParameter())); + + // payload + sparkRequest.setPayload(new SparkRequestPayload(new SparkRequestMessage(new ArrayList<>()))); + } + + public SparkRequest build() { + SparkApiVersion apiVersion = sparkRequest.getApiVersion(); + if (sparkRequest.getPayload().getFunctions() != null && (apiVersion == SparkApiVersion.V2_0 || apiVersion == SparkApiVersion.V1_5)) { + logger.warn("apiVersion is {}, this version does not support functions", apiVersion.getVersion()); + } + sparkRequest.getParameter().getChat().setDomain(apiVersion.getDomain()); + return sparkRequest; + } + + /** + * 消息列表,如果想获取结合上下文的回答,需要将历史问答信息放在一起
+ * 必传,消息列表总tokens不能超过8192 + */ + public SparkRequestBuilder messages(List messages) { + sparkRequest.getPayload().getMessage().setText(messages); + return this; + } + + /** + * 核采样阈值。用于决定结果随机性,取值越高随机性越强即相同的问题得到的不同答案的可能性越高
+ * 非必传,取值为[0,1],默认为0.5 + */ + public SparkRequestBuilder temperature(Double temperature) { + sparkRequest.getParameter().getChat().setTemperature(temperature); + return this; + } + + /** + * 模型回答的tokens的最大长度
+ * 非必传,取值为[1,4096],默认为2048 + */ + public SparkRequestBuilder maxTokens(Integer maxTokens) { + sparkRequest.getParameter().getChat().setMaxTokens(maxTokens); + return this; + } + + /** + * 从k个候选中随机选择⼀个(⾮等概率)
+ * 非必传,取值为[1,6],默认为4 + */ + public SparkRequestBuilder topK(Integer topK) { + sparkRequest.getParameter().getChat().setTopK(topK); + return this; + } + + /** + * 每个用户的id,用于区分不同用户
+ * 非必传,最大长度32 + */ + public SparkRequestBuilder uid(String uid) { + sparkRequest.getHeader().setUid(uid); + return this; + } + + /** + * 用于关联用户会话
+ * 非必传,需要保障用户下的唯一性 + */ + public SparkRequestBuilder chatId(String chatId) { + sparkRequest.getParameter().getChat().setChatId(chatId); + return this; + } + + /** + * 覆盖默认的对话参数 + */ + public SparkRequestBuilder chatParameter(SparkChatParameter chatParameter) { + sparkRequest.getParameter().setChat(chatParameter); + return this; + } + + /** + * 指定apiVersion
+ * 非必传,默认使用2.0版本 + */ + public SparkRequestBuilder apiVersion(SparkApiVersion apiVersion) { + sparkRequest.setApiVersion(apiVersion); + sparkRequest.getParameter().getChat().setDomain(apiVersion.getDomain()); + return this; + } + + /** + * 新增function + */ + public SparkRequestBuilder addFunction(SparkRequestFunctionMessage function) { + SparkRequestFunctions functions = sparkRequest.getPayload().getFunctions(); + if (null == functions) { + functions = new SparkRequestFunctions(new ArrayList<>()); + sparkRequest.getPayload().setFunctions(functions); + } + functions.getText().add(function); + return this; + } +} diff --git a/src/main/java/com/book/backend/manager/model/SparkSyncChatResponse.java b/src/main/java/com/book/backend/manager/model/SparkSyncChatResponse.java new file mode 100644 index 0000000..df02e47 --- /dev/null +++ b/src/main/java/com/book/backend/manager/model/SparkSyncChatResponse.java @@ -0,0 +1,76 @@ +package com.book.backend.manager.model; + + + +import com.book.backend.manager.model.response.SparkResponseFunctionCall; +import com.book.backend.manager.model.response.SparkTextUsage; + +import java.io.Serializable; + +/** + * SparkTextChatResponse + * + * @author briqt + */ +public class SparkSyncChatResponse implements Serializable { + private static final long serialVersionUID = -6785055441385392782L; + + /** + * 回答内容 + */ + private String content; + + private SparkResponseFunctionCall functionCall; + + /** + * tokens统计 + */ + private SparkTextUsage textUsage; + + /** + * 内部自用字段 + */ + private boolean ok = false; + + private Throwable exception; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public SparkResponseFunctionCall getFunctionCall() { + return functionCall; + } + + public void setFunctionCall(SparkResponseFunctionCall functionCall) { + this.functionCall = functionCall; + } + + public SparkTextUsage getTextUsage() { + return textUsage; + } + + public void setTextUsage(SparkTextUsage textUsage) { + this.textUsage = textUsage; + } + + public boolean isOk() { + return ok; + } + + public void setOk(boolean ok) { + this.ok = ok; + } + + public Throwable getException() { + return exception; + } + + public void setException(Throwable exception) { + this.exception = exception; + } +} diff --git a/src/main/java/com/book/backend/manager/model/request/SparkRequest.java b/src/main/java/com/book/backend/manager/model/request/SparkRequest.java new file mode 100644 index 0000000..8e35c05 --- /dev/null +++ b/src/main/java/com/book/backend/manager/model/request/SparkRequest.java @@ -0,0 +1,63 @@ +package com.book.backend.manager.model.request; + +import com.book.backend.manager.constant.SparkApiVersion; +import com.book.backend.manager.model.SparkRequestBuilder; +import com.fasterxml.jackson.annotation.JsonIgnore; + + +import java.io.Serializable; + +/** + * SparkRequest + * + * @author briqt + */ +public class SparkRequest implements Serializable { + private static final long serialVersionUID = 8142547165395379456L; + + private SparkRequestHeader header; + + private SparkRequestParameter parameter; + + private SparkRequestPayload payload; + + private transient SparkApiVersion apiVersion = SparkApiVersion.V3_0; + + public static SparkRequestBuilder builder() { + return new SparkRequestBuilder(); + } + + public SparkRequestHeader getHeader() { + return header; + } + + public void setHeader(SparkRequestHeader header) { + this.header = header; + } + + public SparkRequestParameter getParameter() { + return parameter; + } + + public void setParameter(SparkRequestParameter parameter) { + this.parameter = parameter; + } + + public SparkRequestPayload getPayload() { + return payload; + } + + public void setPayload(SparkRequestPayload payload) { + this.payload = payload; + } + + @JsonIgnore + public SparkApiVersion getApiVersion() { + return apiVersion; + } + + @JsonIgnore + public void setApiVersion(SparkApiVersion apiVersion) { + this.apiVersion = apiVersion; + } +} diff --git a/src/main/java/com/book/backend/manager/model/request/SparkRequestHeader.java b/src/main/java/com/book/backend/manager/model/request/SparkRequestHeader.java new file mode 100644 index 0000000..3b8c84a --- /dev/null +++ b/src/main/java/com/book/backend/manager/model/request/SparkRequestHeader.java @@ -0,0 +1,43 @@ +package com.book.backend.manager.model.request; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; + +/** + * $.header + * + * @author briqt + */ +public class SparkRequestHeader implements Serializable { + private static final long serialVersionUID = -1426143090218924505L; + + /** + * 应用appid,从开放平台控制台创建的应用中获取
+ * 必传 + */ + @JsonProperty("app_id") + private String appId; + + /** + * 每个用户的id,用于区分不同用户
+ * 非必传,最大长度32 + */ + private String uid; + + public String getAppId() { + return appId; + } + + public void setAppId(String appId) { + this.appId = appId; + } + + public String getUid() { + return uid; + } + + public void setUid(String uid) { + this.uid = uid; + } +} diff --git a/src/main/java/com/book/backend/manager/model/request/SparkRequestMessage.java b/src/main/java/com/book/backend/manager/model/request/SparkRequestMessage.java new file mode 100644 index 0000000..5b33ada --- /dev/null +++ b/src/main/java/com/book/backend/manager/model/request/SparkRequestMessage.java @@ -0,0 +1,33 @@ +package com.book.backend.manager.model.request; + + +import com.book.backend.manager.model.SparkMessage; + +import java.io.Serializable; +import java.util.List; + +/** + * $.payload.message + * + * @author briqt + */ +public class SparkRequestMessage implements Serializable { + private static final long serialVersionUID = 6725091574720504980L; + + private List text; + + public SparkRequestMessage() { + } + + public SparkRequestMessage(List text) { + this.text = text; + } + + public List getText() { + return text; + } + + public void setText(List text) { + this.text = text; + } +} diff --git a/src/main/java/com/book/backend/manager/model/request/SparkRequestParameter.java b/src/main/java/com/book/backend/manager/model/request/SparkRequestParameter.java new file mode 100644 index 0000000..3402f8f --- /dev/null +++ b/src/main/java/com/book/backend/manager/model/request/SparkRequestParameter.java @@ -0,0 +1,32 @@ +package com.book.backend.manager.model.request; + + +import com.book.backend.manager.model.SparkChatParameter; + +import java.io.Serializable; + +/** + * $.parameter + * + * @author briqt + */ +public class SparkRequestParameter implements Serializable { + private static final long serialVersionUID = 4502096141480336425L; + + private SparkChatParameter chat; + + public SparkRequestParameter() { + } + + public SparkRequestParameter(SparkChatParameter chat) { + this.chat = chat; + } + + public SparkChatParameter getChat() { + return chat; + } + + public void setChat(SparkChatParameter chat) { + this.chat = chat; + } +} diff --git a/src/main/java/com/book/backend/manager/model/request/SparkRequestPayload.java b/src/main/java/com/book/backend/manager/model/request/SparkRequestPayload.java new file mode 100644 index 0000000..358426f --- /dev/null +++ b/src/main/java/com/book/backend/manager/model/request/SparkRequestPayload.java @@ -0,0 +1,47 @@ +package com.book.backend.manager.model.request; + + +import com.book.backend.manager.model.request.function.SparkRequestFunctions; + +import java.io.Serializable; + +/** + * $.payload + * + * @author briqt + */ +public class SparkRequestPayload implements Serializable { + private static final long serialVersionUID = 2084163918219863102L; + + private SparkRequestMessage message; + + private SparkRequestFunctions functions; + + public SparkRequestPayload() { + } + + public SparkRequestPayload(SparkRequestMessage message) { + this.message = message; + } + + public SparkRequestPayload(SparkRequestMessage message, SparkRequestFunctions functions) { + this.message = message; + this.functions = functions; + } + + public SparkRequestMessage getMessage() { + return message; + } + + public void setMessage(SparkRequestMessage message) { + this.message = message; + } + + public SparkRequestFunctions getFunctions() { + return functions; + } + + public void setFunctions(SparkRequestFunctions functions) { + this.functions = functions; + } +} diff --git a/src/main/java/com/book/backend/manager/model/request/function/SparkFunctionBuilder.java b/src/main/java/com/book/backend/manager/model/request/function/SparkFunctionBuilder.java new file mode 100644 index 0000000..08edb5e --- /dev/null +++ b/src/main/java/com/book/backend/manager/model/request/function/SparkFunctionBuilder.java @@ -0,0 +1,81 @@ +package com.book.backend.manager.model.request.function; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * SparkFunctionBuilder + * + * @author briqt + * @date 2023/11/25 + */ +public class SparkFunctionBuilder { + + private final SparkRequestFunctionMessage sparkRequestFunctionMessage; + + public SparkFunctionBuilder() { + sparkRequestFunctionMessage = new SparkRequestFunctionMessage(); + sparkRequestFunctionMessage.setParameters(new SparkRequestFunctionParameters("object", new LinkedHashMap<>(), new ArrayList<>())); + } + + public static SparkFunctionBuilder functionName(String name) { + return new SparkFunctionBuilder().name(name); + } + + /** + * 必传;function名称;用户输入命中后,会返回该名称 + */ + public SparkFunctionBuilder name(String name) { + sparkRequestFunctionMessage.setName(name); + return this; + } + + /** + * 必传;function功能描述;描述function功能即可,越详细越有助于大模型理解该function + */ + public SparkFunctionBuilder description(String description) { + sparkRequestFunctionMessage.setDescription(description); + return this; + } + + /** + * 必传;参数类型;默认值:object + */ + public SparkFunctionBuilder parameterType(String type) { + sparkRequestFunctionMessage.getParameters().setType(type); + return this; + } + + /** + * 必传;参数信息描述;该内容由用户定义,命中该方法时需要返回哪些参数 + * + * @param name 参数名称 + * @param type 参数类型 + * @param description 参数信息描述 + */ + public SparkFunctionBuilder addParameterProperty(String name, String type, String description) { + sparkRequestFunctionMessage.getParameters().getProperties().put(name, new SparkRequestFunctionProperty(type, description)); + return this; + } + + /** + * 必须返回的参数列表 + */ + public SparkFunctionBuilder addParameterRequired(String... name) { + for (String s : name) { + sparkRequestFunctionMessage.getParameters().getRequired().add(s); + } + return this; + } + + public SparkFunctionBuilder parameters(String type, Map properties, List required) { + sparkRequestFunctionMessage.setParameters(new SparkRequestFunctionParameters(type, properties, required)); + return this; + } + + public SparkRequestFunctionMessage build() { + return sparkRequestFunctionMessage; + } +} diff --git a/src/main/java/com/book/backend/manager/model/request/function/SparkRequestFunctionMessage.java b/src/main/java/com/book/backend/manager/model/request/function/SparkRequestFunctionMessage.java new file mode 100644 index 0000000..cf56438 --- /dev/null +++ b/src/main/java/com/book/backend/manager/model/request/function/SparkRequestFunctionMessage.java @@ -0,0 +1,61 @@ +package com.book.backend.manager.model.request.function; + +import java.io.Serializable; + +/** + * $.payload.functions.text + * + * @author briqt + */ +public class SparkRequestFunctionMessage implements Serializable { + + private static final long serialVersionUID = 6587302404547694700L; + + /** + * 必传;function名称;用户输入命中后,会返回该名称 + */ + private String name; + + /** + * 必传;function功能描述;描述function功能即可,越详细越有助于大模型理解该function + */ + private String description; + + /** + * 必传;function参数列表 + */ + private SparkRequestFunctionParameters parameters; + + public SparkRequestFunctionMessage() { + } + + public SparkRequestFunctionMessage(String name, String description, SparkRequestFunctionParameters parameters) { + this.name = name; + this.description = description; + this.parameters = parameters; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public SparkRequestFunctionParameters getParameters() { + return parameters; + } + + public void setParameters(SparkRequestFunctionParameters parameters) { + this.parameters = parameters; + } +} diff --git a/src/main/java/com/book/backend/manager/model/request/function/SparkRequestFunctionParameters.java b/src/main/java/com/book/backend/manager/model/request/function/SparkRequestFunctionParameters.java new file mode 100644 index 0000000..e75c5bb --- /dev/null +++ b/src/main/java/com/book/backend/manager/model/request/function/SparkRequestFunctionParameters.java @@ -0,0 +1,65 @@ +package com.book.backend.manager.model.request.function; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * $.payload.functions.text + * + * @author briqt + */ +public class SparkRequestFunctionParameters implements Serializable { + + private static final long serialVersionUID = 1079801431462837232L; + + /** + * 必传;参数类型 + */ + private String type; + + /** + * 必传;参数信息描述;该内容由用户定义,命中该方法时需要返回哪些参数
+ * key:参数名称
+ * value:参数信息描述 + */ + private Map properties; + + /** + * 必传;必须返回的参数列表;该内容由用户定义,命中方法时必须返回的字段;properties中的key + */ + private List required; + + public SparkRequestFunctionParameters() { + } + + public SparkRequestFunctionParameters(String type, Map properties, List required) { + this.type = type; + this.properties = properties; + this.required = required; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Map getProperties() { + return properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + + public List getRequired() { + return required; + } + + public void setRequired(List required) { + this.required = required; + } +} diff --git a/src/main/java/com/book/backend/manager/model/request/function/SparkRequestFunctionProperty.java b/src/main/java/com/book/backend/manager/model/request/function/SparkRequestFunctionProperty.java new file mode 100644 index 0000000..d921d99 --- /dev/null +++ b/src/main/java/com/book/backend/manager/model/request/function/SparkRequestFunctionProperty.java @@ -0,0 +1,47 @@ +package com.book.backend.manager.model.request.function; + +import java.io.Serializable; + +/** + * $.payload.functions.text.parameters.properties.* + * + * @author briqt + */ +public class SparkRequestFunctionProperty implements Serializable { + + private static final long serialVersionUID = -343415637582994606L; + + /** + * 必传;参数信息描述;该内容由用户定义,需要返回的参数是什么类型 + */ + private String type; + + /** + * 必传;参数详细描述;该内容由用户定义,需要返回的参数的具体描述 + */ + private String description; + + public SparkRequestFunctionProperty() { + } + + public SparkRequestFunctionProperty(String type, String description) { + this.type = type; + this.description = description; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/src/main/java/com/book/backend/manager/model/request/function/SparkRequestFunctions.java b/src/main/java/com/book/backend/manager/model/request/function/SparkRequestFunctions.java new file mode 100644 index 0000000..6cfa4d3 --- /dev/null +++ b/src/main/java/com/book/backend/manager/model/request/function/SparkRequestFunctions.java @@ -0,0 +1,31 @@ +package com.book.backend.manager.model.request.function; + +import java.io.Serializable; +import java.util.List; + +/** + * $.payload.functions + * + * @author briqt + */ +public class SparkRequestFunctions implements Serializable { + + private static final long serialVersionUID = -7696196392354475586L; + + private List text; + + public SparkRequestFunctions() { + } + + public SparkRequestFunctions(List text) { + this.text = text; + } + + public List getText() { + return text; + } + + public void setText(List text) { + this.text = text; + } +} diff --git a/src/main/java/com/book/backend/manager/model/response/SparkResponse.java b/src/main/java/com/book/backend/manager/model/response/SparkResponse.java new file mode 100644 index 0000000..d617cf2 --- /dev/null +++ b/src/main/java/com/book/backend/manager/model/response/SparkResponse.java @@ -0,0 +1,32 @@ +package com.book.backend.manager.model.response; + +import java.io.Serializable; + +/** + * SparkResponse + * + * @author briqt + */ +public class SparkResponse implements Serializable { + private static final long serialVersionUID = 886720558849587945L; + + private SparkResponseHeader header; + + private SparkResponsePayload payload; + + public SparkResponseHeader getHeader() { + return header; + } + + public void setHeader(SparkResponseHeader header) { + this.header = header; + } + + public SparkResponsePayload getPayload() { + return payload; + } + + public void setPayload(SparkResponsePayload payload) { + this.payload = payload; + } +} diff --git a/src/main/java/com/book/backend/manager/model/response/SparkResponseChoices.java b/src/main/java/com/book/backend/manager/model/response/SparkResponseChoices.java new file mode 100644 index 0000000..2e38c9e --- /dev/null +++ b/src/main/java/com/book/backend/manager/model/response/SparkResponseChoices.java @@ -0,0 +1,54 @@ +package com.book.backend.manager.model.response; + +import com.book.backend.manager.model.SparkMessage; + +import java.io.Serializable; +import java.util.List; + +/** + * $.payload.choices + * + * @author briqt + */ +public class SparkResponseChoices implements Serializable { + private static final long serialVersionUID = 3908073548592366629L; + + /** + * 文本响应状态,取值为[0,1,2]; 0代表首个文本结果;1代表中间文本结果;2代表最后一个文本结果 + */ + private Integer status; + + /** + * 返回的数据序号,取值为[0,9999999] + */ + private Integer seq; + + /** + * 消息列表 + */ + private List text; + + public Integer getStatus() { + return status; + } + + public void setStatus(Integer status) { + this.status = status; + } + + public Integer getSeq() { + return seq; + } + + public void setSeq(Integer seq) { + this.seq = seq; + } + + public List getText() { + return text; + } + + public void setText(List text) { + this.text = text; + } +} diff --git a/src/main/java/com/book/backend/manager/model/response/SparkResponseFunctionCall.java b/src/main/java/com/book/backend/manager/model/response/SparkResponseFunctionCall.java new file mode 100644 index 0000000..2c1f6e8 --- /dev/null +++ b/src/main/java/com/book/backend/manager/model/response/SparkResponseFunctionCall.java @@ -0,0 +1,53 @@ +package com.book.backend.manager.model.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.Serializable; +import java.util.Map; + +/** + * SparkResponseFunctionCall + * + * @author briqt + * @date 2023/11/25 + */ +public class SparkResponseFunctionCall implements Serializable { + private static final long serialVersionUID = -1586729944571910329L; + private final ObjectMapper objectMapper = new ObjectMapper(); + + { + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + } + + private String arguments; + + private String name; + + public String getArguments() { + return arguments; + } + + public void setArguments(String arguments) { + this.arguments = arguments; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getMapArguments() { + try { + return objectMapper.readValue(arguments, new TypeReference>() { + }); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/book/backend/manager/model/response/SparkResponseHeader.java b/src/main/java/com/book/backend/manager/model/response/SparkResponseHeader.java new file mode 100644 index 0000000..87017bc --- /dev/null +++ b/src/main/java/com/book/backend/manager/model/response/SparkResponseHeader.java @@ -0,0 +1,64 @@ +package com.book.backend.manager.model.response; + +import java.io.Serializable; + +/** + * $.header + * + * @author briqt + */ +public class SparkResponseHeader implements Serializable { + private static final long serialVersionUID = -2828057068263022569L; + + /** + * 错误码,0表示正常,非0表示出错;详细释义可在接口说明文档最后的错误码说明了解 + */ + private Integer code; + + /** + * 会话状态,取值为[0,1,2];0代表首次结果;1代表中间结果;2代表最后一个结果 + */ + private Integer status; + + /** + * 会话是否成功的描述信息 + */ + private String message; + + /** + * 会话的唯一id,用于讯飞技术人员查询服务端会话日志使用,出现调用错误时建议留存该字段 + */ + private String sid; + + public Integer getCode() { + return code; + } + + public void setCode(Integer code) { + this.code = code; + } + + public Integer getStatus() { + return status; + } + + public void setStatus(Integer status) { + this.status = status; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getSid() { + return sid; + } + + public void setSid(String sid) { + this.sid = sid; + } +} diff --git a/src/main/java/com/book/backend/manager/model/response/SparkResponsePayload.java b/src/main/java/com/book/backend/manager/model/response/SparkResponsePayload.java new file mode 100644 index 0000000..7194497 --- /dev/null +++ b/src/main/java/com/book/backend/manager/model/response/SparkResponsePayload.java @@ -0,0 +1,32 @@ +package com.book.backend.manager.model.response; + +import java.io.Serializable; + +/** + * $.payload + * + * @author briqt + */ +public class SparkResponsePayload implements Serializable { + private static final long serialVersionUID = 8090192271782303700L; + + private SparkResponseChoices choices; + + private SparkResponseUsage usage; + + public SparkResponseChoices getChoices() { + return choices; + } + + public void setChoices(SparkResponseChoices choices) { + this.choices = choices; + } + + public SparkResponseUsage getUsage() { + return usage; + } + + public void setUsage(SparkResponseUsage usage) { + this.usage = usage; + } +} diff --git a/src/main/java/com/book/backend/manager/model/response/SparkResponseUsage.java b/src/main/java/com/book/backend/manager/model/response/SparkResponseUsage.java new file mode 100644 index 0000000..2cddef5 --- /dev/null +++ b/src/main/java/com/book/backend/manager/model/response/SparkResponseUsage.java @@ -0,0 +1,22 @@ +package com.book.backend.manager.model.response; + +import java.io.Serializable; + +/** + * $.payload.usage + * + * @author briqt + */ +public class SparkResponseUsage implements Serializable { + private static final long serialVersionUID = 2181817132625461079L; + + private SparkTextUsage text; + + public SparkTextUsage getText() { + return text; + } + + public void setText(SparkTextUsage text) { + this.text = text; + } +} diff --git a/src/main/java/com/book/backend/manager/model/response/SparkTextUsage.java b/src/main/java/com/book/backend/manager/model/response/SparkTextUsage.java new file mode 100644 index 0000000..3d267aa --- /dev/null +++ b/src/main/java/com/book/backend/manager/model/response/SparkTextUsage.java @@ -0,0 +1,71 @@ +package com.book.backend.manager.model.response; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; + + +/** + * $.payload.usage.text + * + * @author briqt + */ +public class SparkTextUsage implements Serializable { + private static final long serialVersionUID = 8295933047877077971L; + + /** + * 包含历史问题的总tokens大小(提问tokens大小) + */ + @JsonProperty("prompt_tokens") + private Integer promptTokens; + + /** + * 回答的tokens大小 + */ + @JsonProperty("completion_tokens") + private Integer completionTokens; + + /** + * prompt_tokens和completion_tokens的和,也是本次交互计费的tokens大小 + */ + @JsonProperty("total_tokens") + private Integer totalTokens; + + /** + * 保留字段,可忽略 + */ + @JsonProperty("question_tokens") + private Integer questionTokens; + + public Integer getPromptTokens() { + return promptTokens; + } + + public void setPromptTokens(Integer promptTokens) { + this.promptTokens = promptTokens; + } + + public Integer getCompletionTokens() { + return completionTokens; + } + + public void setCompletionTokens(Integer completionTokens) { + this.completionTokens = completionTokens; + } + + public Integer getTotalTokens() { + return totalTokens; + } + + public void setTotalTokens(Integer totalTokens) { + this.totalTokens = totalTokens; + } + + public Integer getQuestionTokens() { + return questionTokens; + } + + public void setQuestionTokens(Integer questionTokens) { + this.questionTokens = questionTokens; + } +} diff --git a/src/main/java/com/book/backend/mapper/AdminsMapper.java b/src/main/java/com/book/backend/mapper/AdminsMapper.java new file mode 100644 index 0000000..13859a8 --- /dev/null +++ b/src/main/java/com/book/backend/mapper/AdminsMapper.java @@ -0,0 +1,18 @@ +package com.book.backend.mapper; + +import com.book.backend.pojo.Admins; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author 程序员小白条 +* @description 针对表【t_admins】的数据库操作Mapper +* @createDate 2023-02-03 20:01:01 +* @Entity com.book.backend.pojo.Admins +*/ +public interface AdminsMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/book/backend/mapper/AiIntelligentMapper.java b/src/main/java/com/book/backend/mapper/AiIntelligentMapper.java new file mode 100644 index 0000000..8399e2d --- /dev/null +++ b/src/main/java/com/book/backend/mapper/AiIntelligentMapper.java @@ -0,0 +1,18 @@ +package com.book.backend.mapper; + +import com.book.backend.pojo.AiIntelligent; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author xiaobaitiao +* @description 针对表【t_ai_intelligent】的数据库操作Mapper +* @createDate 2023-08-27 18:44:26 +* @Entity com.book.backend.pojo.AiIntelligent +*/ +public interface AiIntelligentMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/book/backend/mapper/BookAdminsMapper.java b/src/main/java/com/book/backend/mapper/BookAdminsMapper.java new file mode 100644 index 0000000..0adf8d2 --- /dev/null +++ b/src/main/java/com/book/backend/mapper/BookAdminsMapper.java @@ -0,0 +1,18 @@ +package com.book.backend.mapper; + +import com.book.backend.pojo.BookAdmins; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author 程序员小白条 +* @description 针对表【t_book_admins】的数据库操作Mapper +* @createDate 2023-02-04 16:55:39 +* @Entity com.book.backend.pojo.BookAdmins +*/ +public interface BookAdminsMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/book/backend/mapper/BookRuleMapper.java b/src/main/java/com/book/backend/mapper/BookRuleMapper.java new file mode 100644 index 0000000..6e590f7 --- /dev/null +++ b/src/main/java/com/book/backend/mapper/BookRuleMapper.java @@ -0,0 +1,18 @@ +package com.book.backend.mapper; + +import com.book.backend.pojo.BookRule; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author 程序员小白条 +* @description 针对表【t_book_rule】的数据库操作Mapper +* @createDate 2023-02-05 15:11:20 +* @Entity com.book.backend.pojo.BookRule +*/ +public interface BookRuleMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/book/backend/mapper/BookTypeMapper.java b/src/main/java/com/book/backend/mapper/BookTypeMapper.java new file mode 100644 index 0000000..2a26930 --- /dev/null +++ b/src/main/java/com/book/backend/mapper/BookTypeMapper.java @@ -0,0 +1,18 @@ +package com.book.backend.mapper; + +import com.book.backend.pojo.BookType; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author 程序员小白条 +* @description 针对表【t_book_type】的数据库操作Mapper +* @createDate 2023-02-04 18:51:24 +* @Entity com.book.backend.pojo.BookType +*/ +public interface BookTypeMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/book/backend/mapper/BooksBorrowMapper.java b/src/main/java/com/book/backend/mapper/BooksBorrowMapper.java new file mode 100644 index 0000000..af2efcf --- /dev/null +++ b/src/main/java/com/book/backend/mapper/BooksBorrowMapper.java @@ -0,0 +1,18 @@ +package com.book.backend.mapper; + +import com.book.backend.pojo.BooksBorrow; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author 程序员小白条 +* @description 针对表【t_books_borrow】的数据库操作Mapper +* @createDate 2023-02-05 18:53:07 +* @Entity com.book.backend.pojo.BooksBorrow +*/ +public interface BooksBorrowMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/book/backend/mapper/BooksMapper.java b/src/main/java/com/book/backend/mapper/BooksMapper.java new file mode 100644 index 0000000..a55c371 --- /dev/null +++ b/src/main/java/com/book/backend/mapper/BooksMapper.java @@ -0,0 +1,18 @@ +package com.book.backend.mapper; + +import com.book.backend.pojo.Books; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author 程序员小白条 +* @description 针对表【t_books】的数据库操作Mapper +* @createDate 2023-02-04 18:07:43 +* @Entity com.book.backend.pojo.Books +*/ +public interface BooksMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/book/backend/mapper/ChartMapper.java b/src/main/java/com/book/backend/mapper/ChartMapper.java new file mode 100644 index 0000000..ef38405 --- /dev/null +++ b/src/main/java/com/book/backend/mapper/ChartMapper.java @@ -0,0 +1,18 @@ +package com.book.backend.mapper; + +import com.book.backend.pojo.Chart; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author xiaobaitiao +* @description 针对表【t_chart(图表信息表)】的数据库操作Mapper +* @createDate 2023-08-30 11:05:22 +* @Entity com.book.backend.pojo.Chart +*/ +public interface ChartMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/book/backend/mapper/ChatMapper.java b/src/main/java/com/book/backend/mapper/ChatMapper.java new file mode 100644 index 0000000..39ed798 --- /dev/null +++ b/src/main/java/com/book/backend/mapper/ChatMapper.java @@ -0,0 +1,18 @@ +package com.book.backend.mapper; + +import com.book.backend.pojo.Chat; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author xiaobaitiao +* @description 针对表【t_chat】的数据库操作Mapper +* @createDate 2023-11-27 19:29:21 +* @Entity com.book.backend.pojo.Chat +*/ +public interface ChatMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/book/backend/mapper/CommentMapper.java b/src/main/java/com/book/backend/mapper/CommentMapper.java new file mode 100644 index 0000000..775fa3b --- /dev/null +++ b/src/main/java/com/book/backend/mapper/CommentMapper.java @@ -0,0 +1,18 @@ +package com.book.backend.mapper; + +import com.book.backend.pojo.Comment; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author 程序员小白条 +* @description 针对表【t_comment】的数据库操作Mapper +* @createDate 2023-02-06 19:19:20 +* @Entity com.book.backend.pojo.Comment +*/ +public interface CommentMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/book/backend/mapper/NoticeMapper.java b/src/main/java/com/book/backend/mapper/NoticeMapper.java new file mode 100644 index 0000000..9f42a75 --- /dev/null +++ b/src/main/java/com/book/backend/mapper/NoticeMapper.java @@ -0,0 +1,18 @@ +package com.book.backend.mapper; + +import com.book.backend.pojo.Notice; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author 程序员小白条 +* @description 针对表【t_notice】的数据库操作Mapper +* @createDate 2023-02-05 16:14:03 +* @Entity com.book.backend.pojo.Notice +*/ +public interface NoticeMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/book/backend/mapper/UserInterfaceInfoMapper.java b/src/main/java/com/book/backend/mapper/UserInterfaceInfoMapper.java new file mode 100644 index 0000000..aae8180 --- /dev/null +++ b/src/main/java/com/book/backend/mapper/UserInterfaceInfoMapper.java @@ -0,0 +1,18 @@ +package com.book.backend.mapper; + +import com.book.backend.pojo.UserInterfaceInfo; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author xiaobaitiao +* @description 针对表【t_user_interface_info】的数据库操作Mapper +* @createDate 2023-09-03 19:42:54 +* @Entity com.book.backend.pojo.UserInterfaceInfo +*/ +public interface UserInterfaceInfoMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/book/backend/mapper/UsersMapper.java b/src/main/java/com/book/backend/mapper/UsersMapper.java new file mode 100644 index 0000000..4a49319 --- /dev/null +++ b/src/main/java/com/book/backend/mapper/UsersMapper.java @@ -0,0 +1,20 @@ +package com.book.backend.mapper; + +import com.book.backend.pojo.Users; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** +* @author 程序员小白条 +* @description 针对表【t_users】的数据库操作Mapper +* @createDate 2023-02-02 16:20:02 +* @Entity com.book.backend.pojo.Users +*/ +@Mapper +public interface UsersMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/book/backend/mapper/ViolationMapper.java b/src/main/java/com/book/backend/mapper/ViolationMapper.java new file mode 100644 index 0000000..67eeba0 --- /dev/null +++ b/src/main/java/com/book/backend/mapper/ViolationMapper.java @@ -0,0 +1,18 @@ +package com.book.backend.mapper; + +import com.book.backend.pojo.Violation; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author 程序员小白条 +* @description 针对表【t_violation】的数据库操作Mapper +* @createDate 2023-02-06 16:31:20 +* @Entity com.book.backend.pojo.Violation +*/ +public interface ViolationMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/book/backend/pojo/Admins.java b/src/main/java/com/book/backend/pojo/Admins.java new file mode 100644 index 0000000..362b91b --- /dev/null +++ b/src/main/java/com/book/backend/pojo/Admins.java @@ -0,0 +1,56 @@ +package com.book.backend.pojo; + +import com.baomidou.mybatisplus.annotation.*; + +import java.io.Serializable; +import java.util.Date; +import lombok.Data; + +/** + * + * @TableName t_admins + */ +@TableName(value ="t_admins") +@Data +public class Admins implements Serializable { + /** + * 管理员表的唯一标识 + */ + @TableId(type = IdType.AUTO) + private Long adminId; + + /** + * 用户名 + */ + private String username; + + /** + * 密码(MD5加密) + */ + private String password; + + /** + * 管理员真实姓名 + */ + private String adminName; + + /** + * 1表示可用 0表示禁用 + */ + private Integer status; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private String createTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateTime; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} diff --git a/src/main/java/com/book/backend/pojo/AiIntelligent.java b/src/main/java/com/book/backend/pojo/AiIntelligent.java new file mode 100644 index 0000000..e711aaf --- /dev/null +++ b/src/main/java/com/book/backend/pojo/AiIntelligent.java @@ -0,0 +1,50 @@ +package com.book.backend.pojo; + +import com.baomidou.mybatisplus.annotation.*; + +import java.io.Serializable; +import java.util.Date; +import lombok.Data; + +/** + * + * @TableName t_ai_intelligent + */ +@TableName(value ="t_ai_intelligent") +@Data +public class AiIntelligent implements Serializable { + /** + * + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 用户输入信息 + */ + private String inputMessage; + /** + * AI生成的信息 + */ + private String aiResult; + + /** + * 用户id,标识是哪个用户的信息 可以为Null + */ + private Long userId; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private String createTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateTime; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} diff --git a/src/main/java/com/book/backend/pojo/BookAdmins.java b/src/main/java/com/book/backend/pojo/BookAdmins.java new file mode 100644 index 0000000..2badd7f --- /dev/null +++ b/src/main/java/com/book/backend/pojo/BookAdmins.java @@ -0,0 +1,61 @@ +package com.book.backend.pojo; + +import com.baomidou.mybatisplus.annotation.*; + +import java.io.Serializable; +import java.util.Date; +import lombok.Data; + +/** + * + * @TableName t_book_admins + */ +@TableName(value ="t_book_admins") +@Data +public class BookAdmins implements Serializable { + /** + * 图书管理员表的唯一标识 + */ + @TableId(type = IdType.AUTO) + private Long bookAdminId; + + /** + * 用户名 + */ + private String username; + + /** + * 密码md5加密 + */ + private String password; + + /** + * 图书管理员真实姓名 + */ + private String bookAdminName; + + /** + * 1表示可用 0表示禁用 + */ + private Integer status; + + /** + * 电子邮箱 + */ + private String email; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private String createTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateTime; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} diff --git a/src/main/java/com/book/backend/pojo/BookRule.java b/src/main/java/com/book/backend/pojo/BookRule.java new file mode 100644 index 0000000..9851bca --- /dev/null +++ b/src/main/java/com/book/backend/pojo/BookRule.java @@ -0,0 +1,61 @@ +package com.book.backend.pojo; + +import com.baomidou.mybatisplus.annotation.*; + +import java.io.Serializable; +import java.util.Date; +import lombok.Data; + +/** + * + * @TableName t_book_rule + */ +@TableName(value ="t_book_rule") +@Data +public class BookRule implements Serializable { + /** + * 借阅规则记录的唯一标识 + */ + @TableId(type = IdType.AUTO) + private Integer ruleId; + + /** + * 借阅规则编号 + */ + private Integer bookRuleId; + + /** + * 借阅天数 + */ + private Integer bookDays; + + /** + * 限制借阅的本数 + */ + private Integer bookLimitNumber; + + /** + * 限制的图书馆 + */ + private String bookLimitLibrary; + + /** + * 图书借阅后每天逾期费用 + */ + private Double bookOverdueFee; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private String createTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateTime; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} diff --git a/src/main/java/com/book/backend/pojo/BookType.java b/src/main/java/com/book/backend/pojo/BookType.java new file mode 100644 index 0000000..1676d82 --- /dev/null +++ b/src/main/java/com/book/backend/pojo/BookType.java @@ -0,0 +1,46 @@ +package com.book.backend.pojo; + +import com.baomidou.mybatisplus.annotation.*; + +import java.io.Serializable; +import java.util.Date; +import lombok.Data; + +/** + * + * @TableName t_book_type + */ +@TableName(value ="t_book_type") +@Data +public class BookType implements Serializable { + /** + * 图书类别唯一标识 + */ + @TableId(type = IdType.AUTO) + private Integer typeId; + + /** + * 借阅类别的昵称 + */ + private String typeName; + + /** + * 借阅类别的描述 + */ + private String typeContent; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private String createTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateTime; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} diff --git a/src/main/java/com/book/backend/pojo/Books.java b/src/main/java/com/book/backend/pojo/Books.java new file mode 100644 index 0000000..a58be25 --- /dev/null +++ b/src/main/java/com/book/backend/pojo/Books.java @@ -0,0 +1,76 @@ +package com.book.backend.pojo; + +import com.baomidou.mybatisplus.annotation.*; + +import java.io.Serializable; +import java.util.Date; +import lombok.Data; + +/** + * + * @TableName t_books + */ +@TableName(value ="t_books") +@Data +public class Books implements Serializable { + /** + * 图书表唯一标识 + */ + @TableId(type = IdType.AUTO) + private Integer bookId; + + /** + * 图书编号 图书的唯一标识 + */ + private Long bookNumber; + + /** + * 图书名称 + */ + private String bookName; + + /** + * 图书作者 + */ + private String bookAuthor; + + /** + * 图书所在图书馆名称 + */ + private String bookLibrary; + + /** + * 图书类别 + */ + private String bookType; + + /** + * 图书位置 + */ + private String bookLocation; + + /** + * 图书状态 + */ + private String bookStatus; + + /** + * 图书描述 + */ + private String bookDescription; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private String createTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateTime; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} diff --git a/src/main/java/com/book/backend/pojo/BooksBorrow.java b/src/main/java/com/book/backend/pojo/BooksBorrow.java new file mode 100644 index 0000000..26e0307 --- /dev/null +++ b/src/main/java/com/book/backend/pojo/BooksBorrow.java @@ -0,0 +1,85 @@ +package com.book.backend.pojo; + +import com.baomidou.mybatisplus.annotation.*; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.Date; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +/** + * + * @TableName t_books_borrow + */ +@TableName(value ="t_books_borrow") +@Data +public class BooksBorrow implements Serializable { + /** + * 借阅表唯一标识 + */ + @TableId(type = IdType.AUTO) + private Integer borrowId; + + /** + * 借阅证编号 固定11位随机生成 用户和图书关联的唯一标识 + */ + private Long cardNumber; + + /** + * 图书编号 图书唯一标识 + */ + private Long bookNumber; + + /** + * 借阅日期 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") + private LocalDateTime borrowDate; + + /** + * 截止日期 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") + private LocalDateTime closeDate; + + /** + * 归还日期 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") + private LocalDateTime returnDate; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private String createTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateTime; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + public BooksBorrow(Integer borrowId, Long cardNumber, Long bookNumber, LocalDateTime borrowDate, LocalDateTime closeDate, LocalDateTime returnDate, String createTime, String updateTime) { + this.borrowId = borrowId; + this.cardNumber = cardNumber; + this.bookNumber = bookNumber; + this.borrowDate = borrowDate; + this.closeDate = closeDate; + this.returnDate = returnDate; + this.createTime = createTime; + this.updateTime = updateTime; + } + + public BooksBorrow() { + } +} diff --git a/src/main/java/com/book/backend/pojo/Chart.java b/src/main/java/com/book/backend/pojo/Chart.java new file mode 100644 index 0000000..2550d82 --- /dev/null +++ b/src/main/java/com/book/backend/pojo/Chart.java @@ -0,0 +1,87 @@ +package com.book.backend.pojo; + +import com.baomidou.mybatisplus.annotation.*; + +import java.io.Serializable; +import java.util.Date; +import lombok.Data; + +/** + * 图表信息表 + * @TableName t_chart + */ +@TableName(value ="t_chart") +@Data +public class Chart implements Serializable { + /** + * id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 图标名称 + */ + private String name; + + /** + * 分析目标 + */ + private String goal; + + /** + * 图标数据 + */ + private String chartData; + + /** + * 图标类型 + */ + private String chartType; + + /** + * 生成的图标数据 + */ + private String genChart; + + /** + * 生成的分析结论 + */ + private String genResult; + + /** + * wait,running,succeed,failed + */ + private String status; + + /** + * 执行信息 + */ + private String execMessage; + + /** + * 创建管理员 id + */ + private Long adminId; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private String createTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateTime; + + /** + * 是否删除 + */ + @TableLogic + private Integer isDelete; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} diff --git a/src/main/java/com/book/backend/pojo/Chat.java b/src/main/java/com/book/backend/pojo/Chat.java new file mode 100644 index 0000000..46e2efc --- /dev/null +++ b/src/main/java/com/book/backend/pojo/Chat.java @@ -0,0 +1,58 @@ +package com.book.backend.pojo; + +import com.baomidou.mybatisplus.annotation.*; + +import java.io.Serializable; +import java.util.Date; +import lombok.Data; + +/** + * + * @TableName t_chat + */ +@TableName(value ="t_chat") +@Data +public class Chat implements Serializable { + /** + * 聊天记录id + + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 发送消息者id + + */ + private Long fromId; + + /** + * 接受消息者id,可以为空 + */ + private Long toId; + + /** + * 消息内容 + */ + private String text; + + /** + * 聊天类型 1-私聊 2-群聊 + */ + private Integer chatType; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private String createTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateTime; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} diff --git a/src/main/java/com/book/backend/pojo/Comment.java b/src/main/java/com/book/backend/pojo/Comment.java new file mode 100644 index 0000000..bdd76cd --- /dev/null +++ b/src/main/java/com/book/backend/pojo/Comment.java @@ -0,0 +1,56 @@ +package com.book.backend.pojo; + +import com.baomidou.mybatisplus.annotation.*; + +import java.io.Serializable; +import java.util.Date; +import lombok.Data; + +/** + * + * @TableName t_comment + */ +@TableName(value ="t_comment") +@Data +public class Comment implements Serializable { + /** + * 留言表唯一标识 + */ + @TableId(type = IdType.AUTO) + private Integer commentId; + + /** + * 留言的头像 链接 + */ + private String commentAvatar; + + /** + * 弹幕的高度(样式) + */ + private String commentBarrageStyle; + + /** + * 弹幕的内容 + */ + private String commentMessage; + + /** + * 留言的时间(控制速度) + */ + private Integer commentTime; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private String createTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateTime; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} diff --git a/src/main/java/com/book/backend/pojo/Notice.java b/src/main/java/com/book/backend/pojo/Notice.java new file mode 100644 index 0000000..df182c4 --- /dev/null +++ b/src/main/java/com/book/backend/pojo/Notice.java @@ -0,0 +1,51 @@ +package com.book.backend.pojo; + +import com.baomidou.mybatisplus.annotation.*; + +import java.io.Serializable; +import java.util.Date; +import lombok.Data; + +/** + * + * @TableName t_notice + */ +@TableName(value ="t_notice") +@Data +public class Notice implements Serializable { + /** + * 公告表唯一标识 + */ + @TableId(type = IdType.AUTO) + private Integer noticeId; + + /** + * 公告题目 + */ + private String noticeTitle; + + /** + * 公告内容 + */ + private String noticeContent; + + /** + * 发布公告的管理员id + */ + private Integer noticeAdminId; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private String createTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateTime; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} diff --git a/src/main/java/com/book/backend/pojo/UserInterfaceInfo.java b/src/main/java/com/book/backend/pojo/UserInterfaceInfo.java new file mode 100644 index 0000000..cad3d2b --- /dev/null +++ b/src/main/java/com/book/backend/pojo/UserInterfaceInfo.java @@ -0,0 +1,57 @@ +package com.book.backend.pojo; + +import com.baomidou.mybatisplus.annotation.*; + +import java.io.Serializable; +import java.util.Date; +import lombok.Data; + +/** + * + * @TableName t_user_interface_info + */ +@TableName(value ="t_user_interface_info") +@Data +public class UserInterfaceInfo implements Serializable { + /** + * + */ + @TableId(type=IdType.AUTO) + private Long id; + + /** + * 用户id + */ + private Long userId; + + /** + * 1 表示AI聊天接口 2表示智能分析接口 + */ + private Long interfaceId; + + /** + * 总共调用接口次数 + + */ + private Integer totalNum; + + /** + * 剩余接口可用次数 + */ + private Integer leftNum; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private String createTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateTime; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} diff --git a/src/main/java/com/book/backend/pojo/Users.java b/src/main/java/com/book/backend/pojo/Users.java new file mode 100644 index 0000000..0604bb5 --- /dev/null +++ b/src/main/java/com/book/backend/pojo/Users.java @@ -0,0 +1,67 @@ +package com.book.backend.pojo; + +import com.baomidou.mybatisplus.annotation.*; + +import java.io.Serializable; +import java.util.Date; +import lombok.Data; + +/** + * + * @author 程序员小白条 + * @TableName t_users + */ +@TableName(value ="t_users") +@Data +public class Users implements Serializable { + /** + * 用户表的唯一标识 + */ + @TableId(type = IdType.AUTO) + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 MD5加密 + */ + private String password; + + /** + * 真实姓名 + */ + private String cardName; + + /** + * 借阅证编号 固定11位随机生成 非空 + */ + private Long cardNumber; + + /** + * 规则编号 可以自定义也就是权限功能 + */ + private Integer ruleNumber; + + /** + * 1表示可用 0表示禁用 + */ + private Integer status; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private String createTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateTime; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} diff --git a/src/main/java/com/book/backend/pojo/Violation.java b/src/main/java/com/book/backend/pojo/Violation.java new file mode 100644 index 0000000..92b3c01 --- /dev/null +++ b/src/main/java/com/book/backend/pojo/Violation.java @@ -0,0 +1,81 @@ +package com.book.backend.pojo; + +import com.baomidou.mybatisplus.annotation.*; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.Date; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +/** + * + * @TableName t_violation + */ +@TableName(value ="t_violation") +@Data +public class Violation implements Serializable { + /** + * 违章表唯一标识 + */ + @TableId(type = IdType.AUTO) + private Integer violationId; + + /** + * 借阅证编号 11位 随机生成 + */ + private Long cardNumber; + + /** + * 图书编号 图书唯一标识 + */ + private Long bookNumber; + + /** + * 借阅日期 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") + private LocalDateTime borrowDate; + + /** + * 截止日期 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") + private LocalDateTime closeDate; + + /** + * 归还日期 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") + private LocalDateTime returnDate; + + /** + * 违章信息 + */ + private String violationMessage; + + /** + * 违章信息管理员的id + */ + private Integer violationAdminId; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private String createTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateTime; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} diff --git a/src/main/java/com/book/backend/pojo/dto/BookDTO.java b/src/main/java/com/book/backend/pojo/dto/BookDTO.java new file mode 100644 index 0000000..7514425 --- /dev/null +++ b/src/main/java/com/book/backend/pojo/dto/BookDTO.java @@ -0,0 +1,16 @@ +package com.book.backend.pojo.dto; + +import com.book.backend.pojo.Books; +import lombok.Data; + +/** + * @author 程序员小白条 + */ +@Data +public class BookDTO extends Books { + /** + * 书籍类型 + */ + public Integer bookTypeNumber; + +} diff --git a/src/main/java/com/book/backend/pojo/dto/BookData.java b/src/main/java/com/book/backend/pojo/dto/BookData.java new file mode 100644 index 0000000..c5285dc --- /dev/null +++ b/src/main/java/com/book/backend/pojo/dto/BookData.java @@ -0,0 +1,63 @@ +package com.book.backend.pojo.dto; + +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.io.Serializable; + +/** + * + * @TableName t_books + */ +@Data +public class BookData implements Serializable { + + + + + /** + * 图书名称 + */ + @ExcelProperty(value="图书名称",index = 0) + private String bookName; + + /** + * 图书作者 + */ + @ExcelProperty(value="图书作者",index = 1) + private String bookAuthor; + + /** + * 图书所在图书馆名称 + */ + @ExcelProperty(value="图书馆",index = 2) + private String bookLibrary; + + /** + * 图书类别 + */ + @ExcelProperty(value="图书类别",index = 3) + private String bookType; + + /** + * 图书位置 + * 例如A12 B06 + */ + @ExcelProperty(value="图书位置",index = 4) + private String bookLocation; + + /** + * 图书状态(已借出/未借出) + */ + @ExcelProperty(value="图书状态",index = 5) + private String bookStatus; + + /** + * 图书描述 + */ + @ExcelProperty(value="图书描述",index = 6) + private String bookDescription; + + + +} diff --git a/src/main/java/com/book/backend/pojo/dto/BookRuleDTO.java b/src/main/java/com/book/backend/pojo/dto/BookRuleDTO.java new file mode 100644 index 0000000..8b9faf8 --- /dev/null +++ b/src/main/java/com/book/backend/pojo/dto/BookRuleDTO.java @@ -0,0 +1,14 @@ +package com.book.backend.pojo.dto; + +import com.book.backend.pojo.BookRule; +import lombok.Data; + +import java.io.Serializable; + +/** + * @author 程序员小白条 + */ +@Data +public class BookRuleDTO extends BookRule implements Serializable { + public String[] checkList; +} diff --git a/src/main/java/com/book/backend/pojo/dto/BooksBorrowDTO.java b/src/main/java/com/book/backend/pojo/dto/BooksBorrowDTO.java new file mode 100644 index 0000000..c8198e8 --- /dev/null +++ b/src/main/java/com/book/backend/pojo/dto/BooksBorrowDTO.java @@ -0,0 +1,18 @@ +package com.book.backend.pojo.dto; + +import com.book.backend.pojo.BooksBorrow; +import lombok.Data; + +import java.io.Serializable; + +/** + * @author 程序员小白条 + */ +@Data +public class BooksBorrowDTO extends BooksBorrow implements Serializable { + /** + * 接受图书管理员的id + */ + public Integer bookAdminId; + +} diff --git a/src/main/java/com/book/backend/pojo/dto/BorrowData.java b/src/main/java/com/book/backend/pojo/dto/BorrowData.java new file mode 100644 index 0000000..a5794db --- /dev/null +++ b/src/main/java/com/book/backend/pojo/dto/BorrowData.java @@ -0,0 +1,26 @@ +package com.book.backend.pojo.dto; + +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * @author 程序员小白条 + */ +@Data +public class BorrowData implements Serializable { + /** + * 借阅时期 一周为一个间隔 + */ + public String [] borrowDates; + /** + * 借阅量 每个数值代表一周的借阅量 + */ + public Integer [] borrowNumber; + + public BorrowData(String[] borrowDates, Integer[] borrowNumber) { + this.borrowDates = borrowDates; + this.borrowNumber = borrowNumber; + } +} diff --git a/src/main/java/com/book/backend/pojo/dto/BorrowTypeDTO.java b/src/main/java/com/book/backend/pojo/dto/BorrowTypeDTO.java new file mode 100644 index 0000000..eb66f2a --- /dev/null +++ b/src/main/java/com/book/backend/pojo/dto/BorrowTypeDTO.java @@ -0,0 +1,19 @@ +package com.book.backend.pojo.dto; + +import lombok.Data; + +/** + * @author
小白条 + * @from 个人博客 + */ +@Data +public class BorrowTypeDTO { + /** + * 图书分类 + */ + public String bookTypes; + /** + * 借阅量 + */ + public Integer borrowNumbers; +} diff --git a/src/main/java/com/book/backend/pojo/dto/CommentDTO.java b/src/main/java/com/book/backend/pojo/dto/CommentDTO.java new file mode 100644 index 0000000..a900598 --- /dev/null +++ b/src/main/java/com/book/backend/pojo/dto/CommentDTO.java @@ -0,0 +1,17 @@ +package com.book.backend.pojo.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * @author 程序员小白条 + */ +@Data +public class CommentDTO implements Serializable { + public Integer id; + public String avatar; + public String msg; + public Integer time; + public String barrageStyle; +} diff --git a/src/main/java/com/book/backend/pojo/dto/UsersDTO.java b/src/main/java/com/book/backend/pojo/dto/UsersDTO.java new file mode 100644 index 0000000..fa47238 --- /dev/null +++ b/src/main/java/com/book/backend/pojo/dto/UsersDTO.java @@ -0,0 +1,14 @@ +package com.book.backend.pojo.dto; + +import com.book.backend.pojo.Users; +import lombok.Data; + +import java.io.Serializable; + +/** + * @author 程序员小白条 + */ +@Data +public class UsersDTO extends Users implements Serializable { + public String userStatus; +} diff --git a/src/main/java/com/book/backend/pojo/dto/ViolationDTO.java b/src/main/java/com/book/backend/pojo/dto/ViolationDTO.java new file mode 100644 index 0000000..c19df5a --- /dev/null +++ b/src/main/java/com/book/backend/pojo/dto/ViolationDTO.java @@ -0,0 +1,26 @@ +package com.book.backend.pojo.dto; + +import com.book.backend.pojo.Violation; +import lombok.Data; + +import java.io.Serializable; + +/** + * @author 程序员小白条 + * DTO用于将管理员昵称传输 + */ +@Data +public class ViolationDTO extends Violation implements Serializable{ +// /** +// * 违章列表 +// */ +// public Violation violation; + /** + * 违章信息处理人的姓名 + */ + public String violationAdmin; + /** + * 还剩多少天逾期 + */ + public long expireDays; +} diff --git a/src/main/java/com/book/backend/pojo/dto/chart/ChartAddRequest.java b/src/main/java/com/book/backend/pojo/dto/chart/ChartAddRequest.java new file mode 100644 index 0000000..9a26083 --- /dev/null +++ b/src/main/java/com/book/backend/pojo/dto/chart/ChartAddRequest.java @@ -0,0 +1,37 @@ +package com.book.backend.pojo.dto.chart; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 创建请求 + * + * @author 程序员小白条 + * + */ +@Data +public class ChartAddRequest implements Serializable { + + /** + * 名称 + */ + private String name; + + /** + * 分析目标 + */ + private String goal; + + /** + * 图表数据 + */ + private String chartData; + + /** + * 图表类型 + */ + private String chartType; + + private static final long serialVersionUID = 1L; +} diff --git a/src/main/java/com/book/backend/pojo/dto/chart/ChartEditRequest.java b/src/main/java/com/book/backend/pojo/dto/chart/ChartEditRequest.java new file mode 100644 index 0000000..8075668 --- /dev/null +++ b/src/main/java/com/book/backend/pojo/dto/chart/ChartEditRequest.java @@ -0,0 +1,42 @@ +package com.book.backend.pojo.dto.chart; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 编辑请求 + * + * @author 程序员小白条 + * + */ +@Data +public class ChartEditRequest implements Serializable { + + /** + * 名称 + */ + private String name; + + /** + * id + */ + private Long id; + + /** + * 分析目标 + */ + private String goal; + + /** + * 图表数据 + */ + private String chartData; + + /** + * 图表类型 + */ + private String chartType; + + private static final long serialVersionUID = 1L; +} diff --git a/src/main/java/com/book/backend/pojo/dto/chart/ChartUpdateRequest.java b/src/main/java/com/book/backend/pojo/dto/chart/ChartUpdateRequest.java new file mode 100644 index 0000000..ff78aed --- /dev/null +++ b/src/main/java/com/book/backend/pojo/dto/chart/ChartUpdateRequest.java @@ -0,0 +1,73 @@ +package com.book.backend.pojo.dto.chart; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * 更新请求 + * + * @author 程序员小白条 + * + */ +@Data +public class ChartUpdateRequest implements Serializable { + + /** + * id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 名称 + */ + private String name; + + /** + * 分析目标 + */ + private String goal; + + /** + * 图表数据 + */ + private String chartData; + + /** + * 图表类型 + */ + private String chartType; + + /** + * 生成的图表数据 + */ + private String genChart; + + /** + * 生成的分析结论 + */ + private String genResult; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 更新时间 + */ + private Date updateTime; + + /** + * 是否删除 + */ + @TableLogic + private Integer isDelete; + + private static final long serialVersionUID = 1L; +} diff --git a/src/main/java/com/book/backend/pojo/dto/chart/GenChartByAiRequest.java b/src/main/java/com/book/backend/pojo/dto/chart/GenChartByAiRequest.java new file mode 100644 index 0000000..cf4f216 --- /dev/null +++ b/src/main/java/com/book/backend/pojo/dto/chart/GenChartByAiRequest.java @@ -0,0 +1,35 @@ +package com.book.backend.pojo.dto.chart; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 文件上传请求 + * + * @author 程序员小白条 + * + */ +@Data +public class GenChartByAiRequest implements Serializable { + + /** + * 名称 + */ + private String name; + + /** + * 分析目标 + */ + private String goal; + + /** + * 图表类型 + */ + private String chartType; + /** + * 用户id + */ + private Long adminId; + private static final long serialVersionUID = 1L; +} diff --git a/src/main/java/com/book/backend/pojo/dto/chat/ChatRequest.java b/src/main/java/com/book/backend/pojo/dto/chat/ChatRequest.java new file mode 100644 index 0000000..acff495 --- /dev/null +++ b/src/main/java/com/book/backend/pojo/dto/chat/ChatRequest.java @@ -0,0 +1,27 @@ +package com.book.backend.pojo.dto.chat; + +import lombok.Data; + +import java.io.Serializable; + +/** + * @Author: QiMu + * @Date: 2023年04月11日 11:55 + * @Version: 1.0 + * @Description: + */ +@Data +public class ChatRequest implements Serializable { + private static final long serialVersionUID = 1445805872513828206L; + + /** + * 队伍聊天室id + */ + private Long teamId; + + /** + * 接收消息id + */ + private Long toId; + +} diff --git a/src/main/java/com/book/backend/pojo/dto/chat/MessageRequest.java b/src/main/java/com/book/backend/pojo/dto/chat/MessageRequest.java new file mode 100644 index 0000000..a4f98bb --- /dev/null +++ b/src/main/java/com/book/backend/pojo/dto/chat/MessageRequest.java @@ -0,0 +1,21 @@ +package com.book.backend.pojo.dto.chat; + +import lombok.Data; + +import java.io.Serializable; + +/** + * @Author: QiMu + * @Date: 2023年04月10日 14:21 + * @Version: 1.0 + * @Description: + */ +@Data +public class MessageRequest implements Serializable { + private static final long serialVersionUID = 1324635911327892058L; + private Long toId; + private Long teamId; + private String text; + private Integer chatType; + private boolean isAdmin; +} diff --git a/src/main/java/com/book/backend/pojo/vo/BiResponse.java b/src/main/java/com/book/backend/pojo/vo/BiResponse.java new file mode 100644 index 0000000..8e3b8b5 --- /dev/null +++ b/src/main/java/com/book/backend/pojo/vo/BiResponse.java @@ -0,0 +1,16 @@ +package com.book.backend.pojo.vo; + +import lombok.Data; + +/** + * Bi 的返回结果 + */ +@Data +public class BiResponse { + + private String genChart; + + private String genResult; + + private Long chartId; +} diff --git a/src/main/java/com/book/backend/pojo/vo/ChatVo.java b/src/main/java/com/book/backend/pojo/vo/ChatVo.java new file mode 100644 index 0000000..bdddead --- /dev/null +++ b/src/main/java/com/book/backend/pojo/vo/ChatVo.java @@ -0,0 +1,38 @@ +package com.book.backend.pojo.vo; + +import com.baomidou.mybatisplus.annotation.*; + +import java.io.Serializable; +import lombok.Data; + +/** + * + * @TableName t_chat + */ +@Data +public class ChatVo implements Serializable { + + private static final long serialVersionUID = 1324635911327892059L; + /** + * 发送消息者id + + */ + private Long fromId; + + /** + * 接受消息者id,可以为空 + */ + private Long toId; + + /** + * 消息内容 + */ + private String text; + + /** + * 聊天类型 1-私聊 2-群聊 + */ + private Integer chatType; + + +} diff --git a/src/main/java/com/book/backend/service/AdminsService.java b/src/main/java/com/book/backend/service/AdminsService.java new file mode 100644 index 0000000..445c228 --- /dev/null +++ b/src/main/java/com/book/backend/service/AdminsService.java @@ -0,0 +1,44 @@ +package com.book.backend.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.book.backend.common.R; +import com.book.backend.pojo.Admins; +import com.book.backend.pojo.dto.UsersDTO; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +/** +* @author 程序员小白条 +* @description 针对表【t_admins】的数据库操作Service +* @createDate 2023-02-03 20:01:01 +*/ +public interface AdminsService extends IService { + + /** + * 添加借阅证 + * @param usersDTO 用户DTO + * @return R + */ + R addRule(UsersDTO usersDTO); + /** + * 系统管理员登录 + * @param users 系统管理员 + * @return 返回R通用数据 + */ + R login( Admins users); + /** + * 返回给前端系统管理员的数据 + * @param admin 系统管理员 + * @return R + */ + R getUserData( Admins admin); + + + /** + * Excel批量导入图书 + * @param file Excel文件 + * @return R + */ + R upload( MultipartFile file) throws IOException; +} diff --git a/src/main/java/com/book/backend/service/AiIntelligentService.java b/src/main/java/com/book/backend/service/AiIntelligentService.java new file mode 100644 index 0000000..dd46744 --- /dev/null +++ b/src/main/java/com/book/backend/service/AiIntelligentService.java @@ -0,0 +1,28 @@ +package com.book.backend.service; + +import com.book.backend.common.R; +import com.book.backend.pojo.AiIntelligent; +import com.baomidou.mybatisplus.extension.service.IService; + +import java.util.List; + +/** +* @author xiaobaitiao +* @description 针对表【t_ai_intelligent】的数据库操作Service +* @createDate 2023-08-27 18:44:26 +*/ +public interface AiIntelligentService extends IService { + /** + * 调用AI接口,获取推荐的图书信息字符串 + * @param aiIntelligent + * @return + */ + R getGenResult(AiIntelligent aiIntelligent); + + /** + * 根据用户ID 获取该用户和AI聊天的最近的五条消息 + * @param userId + * @return + */ + R> getAiInformationByUserId(Long userId); +} diff --git a/src/main/java/com/book/backend/service/BookAdminsService.java b/src/main/java/com/book/backend/service/BookAdminsService.java new file mode 100644 index 0000000..4e5b656 --- /dev/null +++ b/src/main/java/com/book/backend/service/BookAdminsService.java @@ -0,0 +1,74 @@ +package com.book.backend.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.book.backend.common.BasePage; +import com.book.backend.common.R; +import com.book.backend.pojo.BookAdmins; +import com.baomidou.mybatisplus.extension.service.IService; +import com.book.backend.pojo.dto.ViolationDTO; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; + +/** +* @author 程序员小白条 +* @description 针对表【t_book_admins】的数据库操作Service +* @createDate 2023-02-04 16:55:39 +*/ +public interface BookAdminsService extends IService { + + /** + * 添加图书管理员 + * @param bookAdmins 图书管理员 + * @return R + */ + R addBookAdmin(BookAdmins bookAdmins); + /** + * 图书管理员登录 + * + * @param users 图书管理员 + * @return 返回R通用数据 + */ + R login( BookAdmins users); + /** + * 返回给前端图书管理员的数据 + * + * @param bookAdmins 图书管理员 + * @return R + */ + R getUserData( BookAdmins bookAdmins); + /** + * 获取借书报表 + * + * @param basePage 接受分页构造器和模糊查询的传参 + * @return R> + */ + R> getBorrowStatement( BasePage basePage); + /** + * 获取图书管理员的列表 + * + * @param basePage 分页构造器传参 + * @return R> + */ + R> getBookAdminListByPage( BasePage basePage); + /** + * 获取图书管理员信息 通过图书管理员id + * + * @param bookAdminId 图书管理员id + * @return R + */ + R getBookAdminById( Integer bookAdminId); + /** + * 删除图书管理员 根据图书管理员id + * + * @param bookAdminId 图书管理员id + * @return R + */ + R deleteBookAdminById( Integer bookAdminId); + /** + * 修改图书管理员 + * + * @param bookAdmins 图书管理员 + * @return R + */ + R updateBookAdmin( BookAdmins bookAdmins); +} diff --git a/src/main/java/com/book/backend/service/BookRuleService.java b/src/main/java/com/book/backend/service/BookRuleService.java new file mode 100644 index 0000000..6e9a485 --- /dev/null +++ b/src/main/java/com/book/backend/service/BookRuleService.java @@ -0,0 +1,61 @@ +package com.book.backend.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.book.backend.common.BasePage; +import com.book.backend.common.R; +import com.book.backend.pojo.BookRule; +import com.baomidou.mybatisplus.extension.service.IService; +import com.book.backend.pojo.dto.BookRuleDTO; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; + +import java.util.List; + +/** +* @author 程序员小白条 +* @description 针对表【t_book_rule】的数据库操作Service +* @createDate 2023-02-05 15:11:20 +*/ +public interface BookRuleService extends IService { + /** + * 读者规则查询 + * + * @return R> + */ + R> getRuleList(); + /** + * 获取规则列表(分页) + * + * @param basePage 分页构造器用于接受页数和页码 + * @return R> + */ + R> getRuleListByPage( BasePage basePage); + /** + * 添加规则 + * + * @param bookRule 图书规则 + * @return R + */ + R addRule( BookRule bookRule); + /** + * 根据规则编号 查询规则 + * + * @param ruleId 规则编号 + * @return R + */ + R getRuleByRuleId( Integer ruleId); + /** + * 修改规则 + * + * @param bookRuleDTO 图书规则 + * @return R + */ + R updateRule( BookRuleDTO bookRuleDTO); + /** + * 删除规则 + * + * @param ruleId 规则编号 + * @return R + */ + R deleteRule( Integer ruleId); +} diff --git a/src/main/java/com/book/backend/service/BookTypeService.java b/src/main/java/com/book/backend/service/BookTypeService.java new file mode 100644 index 0000000..56c5afa --- /dev/null +++ b/src/main/java/com/book/backend/service/BookTypeService.java @@ -0,0 +1,61 @@ +package com.book.backend.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.book.backend.common.BasePage; +import com.book.backend.common.R; +import com.book.backend.pojo.BookType; +import com.baomidou.mybatisplus.extension.service.IService; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; + +import java.util.List; + +/** + * @author 程序员小白条 + * @description 针对表【t_book_type】的数据库操作Service + * @createDate 2023-02-04 18:51:24 + */ +public interface BookTypeService extends IService { + /** + * 查询书籍类型的列表 用于添加图书中回显分类 + * + * @return R + */ + R> getBookTypeList(); + + /** + * 书籍类别 获取书籍类别的列表 + * + * @return R> + */ + R> getBookTypeListByPage(BasePage basePage); + + /** + * 添加书籍类别 + * + * @param bookType 书籍类别 + * @return R + */ + R addBookType(BookType bookType); + /** + * 根据书籍类别id 获取书籍类别信息 + * + * @param typeId 书籍类别id + * @return R + */ + R getBookTypeByTypeId(Integer typeId); + /** + * 更新书籍类别 + * + * @param bookType 书籍类别 + * @return R + */ + R updateBookType( BookType bookType); + /** + * 删除书籍类别 根据书籍类别的ID + * + * @param typeId 书籍类别的id + * @return R + */ + R deleteBookTypeByTypeId( Integer typeId); +} diff --git a/src/main/java/com/book/backend/service/BooksBorrowService.java b/src/main/java/com/book/backend/service/BooksBorrowService.java new file mode 100644 index 0000000..7da5ae2 --- /dev/null +++ b/src/main/java/com/book/backend/service/BooksBorrowService.java @@ -0,0 +1,47 @@ +package com.book.backend.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.book.backend.common.BasePage; +import com.book.backend.common.R; +import com.book.backend.pojo.BooksBorrow; +import com.book.backend.pojo.Violation; +import com.book.backend.pojo.dto.ViolationDTO; + +/** + * @author 程序员小白条 + * @description 针对表【t_books_borrow】的数据库操作Service + * @createDate 2023-02-05 18:53:07 + */ +public interface BooksBorrowService extends IService { + /** + * 借阅信息查询 根据用户id,条件及其内容 + * + * @param basePage 用于接受分页传参和用户id + * @return R> + */ + R> getBookBorrowPage(BasePage basePage); + + /** + * 获取图书逾期信息 + * + * @param bookNumber 图书编号 + * @return R + */ + R queryExpireInformationByBookNumber(Long bookNumber); + + /** + * 归还图书 + * + * @param violation 违章表 + * @return R + */ + R returnBook(Violation violation); + /** + * 获取还书报表 + * + * @param basePage 接受分页构造器和模糊查询的传参 + * @return R> + */ + R> getReturnStatement(BasePage basePage); +} diff --git a/src/main/java/com/book/backend/service/BooksService.java b/src/main/java/com/book/backend/service/BooksService.java new file mode 100644 index 0000000..a79bb51 --- /dev/null +++ b/src/main/java/com/book/backend/service/BooksService.java @@ -0,0 +1,91 @@ +package com.book.backend.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.book.backend.common.BasePage; +import com.book.backend.common.R; +import com.book.backend.pojo.Books; +import com.book.backend.pojo.dto.BookDTO; +import com.book.backend.pojo.dto.BooksBorrowDTO; +import com.book.backend.pojo.dto.BorrowTypeDTO; + +import java.util.List; + +/** + * @author 程序员小白条 + * @description 针对表【t_books】的数据库操作Service + * @createDate 2023-02-04 18:07:43 + */ +public interface BooksService extends IService { + /** + * 图书查询 分页和条件查询 (模糊查询) + * + * @param basePage 用于接受分页传参 + * @return R> + */ + R> searchBookPage(BasePage basePage); + + /** + * 借阅图书根据借阅证号和图书编号 + * + * @return R + */ + R borrowBookByCardNumberAndBookNumber(BooksBorrowDTO booksBorrowDTO); + + /** + * 查看图书是否有逾期(查看是否借出) + * + * @param bookNumber 图书编号 + * @return R + */ + R queryBookExpireByBookNumber(Long bookNumber); + + /** + * 获取图书列表 + * + * @param basePage 页码,页数,条件和内容 + * @return R> + */ + R> getBookList(BasePage basePage); + /** + * 添加图书 利用DTO去接受 书籍类别的id 然后再通过id查询分类表获取分类名称 封装给图书 + * + * @return R + */ + R addBook( BookDTO bookDTO); + /** + * 根据图书id删除对应的图书 + * + * @param bookId 图书id + * @return R + */ + R deleteBookByBookId( Integer bookId); + /** + * 根据图书id获得相对应的图书信息 + * + * @param bookId 图书id + * @return R + */ + R getBookInformationByBookId( Integer bookId); + /** + * 根据前端传输的图书信息更新图书 + * + * @param books 图书 + * @return R + */ + R updateBookByEditForm( Books books); + /** + * 获取借书分类统计情况 + * + * @return R> + */ + R> getBorrowTypeStatistic(); + /** + * 批量删除图书 + * + * @param booksList 图书列表 + * @return R + */ + R deleteBookByBatch( List booksList); + +} diff --git a/src/main/java/com/book/backend/service/ChartService.java b/src/main/java/com/book/backend/service/ChartService.java new file mode 100644 index 0000000..d7011fe --- /dev/null +++ b/src/main/java/com/book/backend/service/ChartService.java @@ -0,0 +1,20 @@ +package com.book.backend.service; + +import com.book.backend.common.R; +import com.book.backend.pojo.Chart; +import com.baomidou.mybatisplus.extension.service.IService; +import com.book.backend.pojo.dto.chart.GenChartByAiRequest; +import com.book.backend.pojo.vo.BiResponse; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletRequest; + +/** +* @author xiaobaitiao +* @description 针对表【t_chart(图表信息表)】的数据库操作Service +* @createDate 2023-08-30 11:05:22 +*/ +public interface ChartService extends IService { + R genChartByAi(MultipartFile multipartFile, + GenChartByAiRequest genChartByAiRequest); +} diff --git a/src/main/java/com/book/backend/service/ChatService.java b/src/main/java/com/book/backend/service/ChatService.java new file mode 100644 index 0000000..7e7d1ca --- /dev/null +++ b/src/main/java/com/book/backend/service/ChatService.java @@ -0,0 +1,13 @@ +package com.book.backend.service; + +import com.book.backend.pojo.Chat; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author xiaobaitiao +* @description 针对表【t_chat】的数据库操作Service +* @createDate 2023-11-27 19:29:21 +*/ +public interface ChatService extends IService { + +} diff --git a/src/main/java/com/book/backend/service/CommentService.java b/src/main/java/com/book/backend/service/CommentService.java new file mode 100644 index 0000000..0dac6de --- /dev/null +++ b/src/main/java/com/book/backend/service/CommentService.java @@ -0,0 +1,30 @@ +package com.book.backend.service; + +import com.book.backend.common.R; +import com.book.backend.pojo.Comment; +import com.baomidou.mybatisplus.extension.service.IService; +import com.book.backend.pojo.dto.CommentDTO; +import org.springframework.web.bind.annotation.RequestBody; + +import java.util.List; + +/** + * @author 程序员小白条 + * @description 针对表【t_comment】的数据库操作Service + * @createDate 2023-02-06 19:19:20 + */ +public interface CommentService extends IService { + /** + * 获取弹幕列表 + * + * @return R + */ + R> getCommentList(); + + /** + * 添加弹幕 + * + * @return R + */ + R addComment(CommentDTO commentDTO); +} diff --git a/src/main/java/com/book/backend/service/NoticeService.java b/src/main/java/com/book/backend/service/NoticeService.java new file mode 100644 index 0000000..a64895b --- /dev/null +++ b/src/main/java/com/book/backend/service/NoticeService.java @@ -0,0 +1,65 @@ +package com.book.backend.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.book.backend.common.BasePage; +import com.book.backend.common.R; +import com.book.backend.pojo.Notice; +import com.baomidou.mybatisplus.extension.service.IService; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; + +import java.util.List; + +/** + * @author 程序员小白条 + * @description 针对表【t_notice】的数据库操作Service + * @createDate 2023-02-05 16:14:03 + */ +public interface NoticeService extends IService { + + /** + * 查询公告信息 + * + * @return R> + */ + R> getNoticeList(); + + /** + * 获取公告列表 + * + * @return R + */ + R> getNoticeList(BasePage basePage); + + /** + * 添加公告 + * + * @param notice 公告 + * @return R + */ + R addNotice(Notice notice); + + /** + * 删除公告根据指定的id + * + * @param noticeId 公告id + * @return R + */ + R deleteNoticeById(Integer noticeId); + + /** + * 根据指定id获取公告 + * + * @param noticeId 公告id + * @return R + */ + R getNoticeByNoticeId(Integer noticeId); + /** + * 更新公告根据公告id + * + * @param noticeId 公告id + * @param notice 公告 + * @return R + */ + R updateNoticeByNoticeId(Integer noticeId, Notice notice); +} diff --git a/src/main/java/com/book/backend/service/UserInterfaceInfoService.java b/src/main/java/com/book/backend/service/UserInterfaceInfoService.java new file mode 100644 index 0000000..df5611e --- /dev/null +++ b/src/main/java/com/book/backend/service/UserInterfaceInfoService.java @@ -0,0 +1,13 @@ +package com.book.backend.service; + +import com.book.backend.pojo.UserInterfaceInfo; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author xiaobaitiao +* @description 针对表【t_user_interface_info】的数据库操作Service +* @createDate 2023-09-03 19:42:54 +*/ +public interface UserInterfaceInfoService extends IService { + +} diff --git a/src/main/java/com/book/backend/service/UsersService.java b/src/main/java/com/book/backend/service/UsersService.java new file mode 100644 index 0000000..46463d8 --- /dev/null +++ b/src/main/java/com/book/backend/service/UsersService.java @@ -0,0 +1,75 @@ +package com.book.backend.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.book.backend.common.BasePage; +import com.book.backend.common.R; +import com.book.backend.pojo.Users; +import com.baomidou.mybatisplus.extension.service.IService; +import com.book.backend.pojo.dto.UsersDTO; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; + +/** + * @author 程序员小白条 + * @description 针对表【t_users】的数据库操作Service + * @createDate 2023-02-02 16:20:02 + */ +public interface UsersService extends IService { + /** + * Rest接受参数 查询个人用户userId + * + * @param userId 用户id + * @return R + */ + R getUserByUserId(Integer userId); + + /** + * 修改密码 + * + * @return R + */ + R updatePassword(Users users); + + /** + * 借阅用户登录 + * + * @param users 借阅者用户 + * @return 返回R通用数据 + */ + R login(Users users); + + /** + * 根据用户id传给用户所需的信息 + * @param users 用户 + * @return R + */ + R getUserData( Users users); + /** + * 获取借阅证列表(用户列表) + * + * @param basePage 用于接受模糊查询和分页构造的参数 + * @return R> + */ + R> getStatementList( BasePage basePage); + /** + * 获取用户信息 根据用户id 用于回显借阅证 + * + * @param userId 用户id + * @return R + */ + R getStatementByUserId( Integer userId); + /** + * 修改借阅证信息(用户信息) + * + * @param usersDTO 用户DTO + * @return R + */ + R updateStatement( UsersDTO usersDTO); + /** + * 删除借阅证信息 根据用户id + * + * @param userId 用户id + * @return R + */ + R deleteStatementByUserId( Integer userId); +} diff --git a/src/main/java/com/book/backend/service/ViolationService.java b/src/main/java/com/book/backend/service/ViolationService.java new file mode 100644 index 0000000..d57e47a --- /dev/null +++ b/src/main/java/com/book/backend/service/ViolationService.java @@ -0,0 +1,31 @@ +package com.book.backend.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.book.backend.common.BasePage; +import com.book.backend.common.R; +import com.book.backend.pojo.Violation; +import com.baomidou.mybatisplus.extension.service.IService; +import com.book.backend.pojo.dto.BorrowData; +import com.book.backend.pojo.dto.ViolationDTO; +import org.springframework.web.bind.annotation.RequestBody; + +/** +* @author 程序员小白条 +* @description 针对表【t_violation】的数据库操作Service +* @createDate 2023-02-06 16:31:20 +*/ +public interface ViolationService extends IService { + /** + * 查询违章信息(借阅证) + * + * @param basePage 获取前端的分页参数,条件和内容,借阅证 + * @return R> + */ + R> getViolationListByPage( BasePage basePage); + /** + * 获取借阅量 + * + * @return R + */ + R getBorrowDate(); +} diff --git a/src/main/java/com/book/backend/service/impl/AdminsServiceImpl.java b/src/main/java/com/book/backend/service/impl/AdminsServiceImpl.java new file mode 100644 index 0000000..7c7832d --- /dev/null +++ b/src/main/java/com/book/backend/service/impl/AdminsServiceImpl.java @@ -0,0 +1,196 @@ +package com.book.backend.service.impl; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.book.backend.constant.Constant; +import com.book.backend.common.R; +import com.book.backend.mapper.AdminsMapper; +import com.book.backend.pojo.Admins; +import com.book.backend.pojo.Books; +import com.book.backend.pojo.Users; +import com.book.backend.pojo.dto.BookData; +import com.book.backend.pojo.dto.UsersDTO; +import com.book.backend.service.AdminsService; +import com.book.backend.service.BooksService; +import com.book.backend.service.UsersService; +import com.book.backend.utils.JwtKit; +import com.book.backend.utils.NumberUtil; +import com.book.backend.utils.RandomNameUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.util.DigestUtils; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.io.IOException; +import java.util.ArrayList; + +/** + * @author 程序员小白条 + * @description 针对表【t_admins】的数据库操作Service实现 + * @createDate 2023-02-03 20:01:01 + */ +@Service +public class AdminsServiceImpl extends ServiceImpl + implements AdminsService { + + /** + * 盐值,混淆密码 + */ + private static final String SALT = "xiaobaitiao"; + @Resource + private UsersService usersService; + @Resource + private BooksService booksService; + @Resource + private JwtKit jwtKit; + /** + * 1.接受请求发送的用户名,密码,规则编号,用户状态 + * 2.根据用户状态可用/禁用 去设置1和0 + * 3.用户id自增设为null,密码需要md5加密,随机生成card_name姓名 + * 4.工具类随机生成11位借阅证编号 + * 5.调用服务插入用户,判断是否成功 + */ + @Override + public R addRule(UsersDTO usersDTO) { + + // 密码 + String password = SALT + usersDTO.getPassword(); + // 用户状态 + String userStatus = usersDTO.getUserStatus(); + int status = 0; + if (Constant.USERAVAILABLE.equals(userStatus)) { + status = 1; + } + Users users = new Users(); + BeanUtils.copyProperties(usersDTO, users, "userStatus"); + String md5Password = DigestUtils.md5DigestAsHex(password.getBytes()); + users.setStatus(status); + users.setCardName(RandomNameUtils.fullName()); + // 密码加密 + users.setPassword(md5Password); + long cardNumber = Long.parseLong(new String(NumberUtil.getNumber(11))); + users.setCardNumber(cardNumber); + boolean save = usersService.save(users); + if (!save) { + return R.error("添加借阅证失败"); + } + return R.success(null, "添加借阅证成功"); + } + /** + * 1.将axios请求携带的json字符串反序列成实体类 + * 2.从实体类中获取用户名(判断空的情况),从数据库中查询,如果不存在,直接返回响应状态码404和错误信息 + * 3.用户存在,判断是否为禁用状态,如果是直接返回 + * 4.直接和数据库比对 + * 5.密码校验成功,使用工具类生成Token(传入User) + * 6.返回给前端,响应状态码 200(请求成功) 并在map动态数据中放入token,传输给前端 + */ + @Override + public R login(Admins users) { + + R result = new R<>(); + // 检查用户名是否为空或null等情况 + if (StringUtils.isBlank(users.getUsername())) { + result.setStatus(404); + return R.error("用户名不存在"); + } + // 判断系统管理员是否存在 + LambdaUpdateWrapper adminWrapper = new LambdaUpdateWrapper<>(); + adminWrapper.eq(Admins::getUsername, users.getUsername()); + Admins adminOne = this.getOne(adminWrapper); + if (adminOne == null) { + result.setStatus(404); + return R.error("用户名不存在"); + } + // 系统管理员存在 判断禁用情况 + if (Constant.DISABLE.equals(adminOne.getStatus())) { + return R.error("该系统管理员已被禁用"); + } + String password = DigestUtils.md5DigestAsHex((SALT+users.getPassword()).getBytes()); + if (!password.equals(adminOne.getPassword())) { + result.setStatus(404); + return R.error("用户名或密码错误"); + } + // 密码校验成功 生成Token + String token = jwtKit.generateToken(users); + // 返回成功信息,并将token加入到动态数据map中 + result.setStatus(200); + result.add("token", token); + result.setMsg("登录成功"); + result.add("id", adminOne.getAdminId()); + return result; + } + /** + * 1.先获取请求中的id + * 2.根据id到数据库中查询id是否存活 + * 3.如果存在,查询出数据, + * 4.用户数据需要脱敏 将密码设为空 + * 5.然后封装到R,设置响应状态码和请求信息,返回前端 + */ + @Override + public R getUserData(Admins admin) { + R r = new R<>(); + // 条件构造器 + LambdaQueryWrapper adminsLambdaQueryWrapper = new LambdaQueryWrapper<>(); + adminsLambdaQueryWrapper.eq(Admins::getAdminId,admin.getAdminId()); + Admins adminOne = this.getOne(adminsLambdaQueryWrapper); + if (adminOne == null) { + return R.error("系统管理员不存在"); + } + adminOne.setPassword(""); + r.setData(adminOne); + r.setStatus(200); + r.setMsg("获取系统管理员数据成功"); + return r; + } + + @Override + public R upload(MultipartFile file) throws IOException { + + // 读取excel + EasyExcel.read(file.getInputStream(), BookData.class, new AnalysisEventListener() { + ArrayList booksList = new ArrayList<>(); + // 每解析一行数据,该方法会被调用一次 + @Override + public void invoke(BookData bookData, AnalysisContext analysisContext) { + Books books = new Books(); + // 生成11位数字的图书编号 + StringBuilder stringBuilder = NumberUtil.getNumber(11); + long bookNumber = Long.parseLong(new String(stringBuilder)); + BeanUtils.copyProperties(bookData,books); + books.setBookNumber(bookNumber); + booksList.add(books); +// System.out.println("解析数据为:" + bookData.toString()); + } + // 全部解析完成被调用 + @Override + public void doAfterAllAnalysed(AnalysisContext analysisContext) { + // 全部加入到容器list中后,一次性批量导入,先判断容器是否为空 + if(!booksList.isEmpty()){ + // 可以将解析的数据保存到数据库 + boolean flag = booksService.saveBatch(booksList); + // 如果数据添加成功 + if(flag){ + System.out.println("Excel批量添加图书成功"); + }else{ + System.out.println("Excel批量添加图书失败"); + } + }else{ + System.out.println("空表无法进行数据导入"); + } + System.out.println("解析完成..."); + + } + }).sheet().doRead(); + return R.success(null,"Excel批量添加图书成功"); + } +} + + + + diff --git a/src/main/java/com/book/backend/service/impl/AiIntelligentServiceImpl.java b/src/main/java/com/book/backend/service/impl/AiIntelligentServiceImpl.java new file mode 100644 index 0000000..57afbf9 --- /dev/null +++ b/src/main/java/com/book/backend/service/impl/AiIntelligentServiceImpl.java @@ -0,0 +1,304 @@ +package com.book.backend.service.impl; + +import cn.hutool.core.date.StopWatch; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.book.backend.common.R; +import com.book.backend.common.exception.VueBookException; +import com.book.backend.manager.GuavaRateLimiterManager; +import com.book.backend.mapper.AiIntelligentMapper; +import com.book.backend.pojo.AiIntelligent; +import com.book.backend.pojo.Books; +import com.book.backend.pojo.UserInterfaceInfo; +import com.book.backend.service.AiIntelligentService; +import com.book.backend.service.BooksService; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +/** + * @author xiaobaitiao + * @description 针对表【t_ai_intelligent】的数据库操作Service实现 + * @createDate 2023-08-27 18:44:26 + */ +@Service +public class AiIntelligentServiceImpl extends ServiceImpl + implements AiIntelligentService { + + @Resource + private BooksService booksService; + @Resource + private GuavaRateLimiterManager guavaRateLimiterManager; + @Resource + private UserInterfaceInfoServiceImpl userInterfaceInfoService; + @Resource + @Lazy + private AiIntelligentService aiIntelligentService; + + // 创建RestTemplate对象,用于发送HTTP请求 + private final RestTemplate restTemplate; + + // 在构造方法中初始化RestTemplate并设置超时 + public AiIntelligentServiceImpl() { + org.springframework.http.client.SimpleClientHttpRequestFactory factory = new org.springframework.http.client.SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(600000); + factory.setReadTimeout(600000); + this.restTemplate = new RestTemplate(factory); + } + + // 设置NewAPI的OpenAI格式接口信息 + private static final String NEW_API_URL = "https://openai.933999.xyz/v1/chat/completions"; + private static final String API_KEY = "sk-1PBIyxIdJ42yyC11XRNqbEXYDt2eZRNVNbd8XxmKjnPXGh5S"; + private static final String MODEL = "gpt-4o-mini"; + + + @Override + public R getGenResult(AiIntelligent aiIntelligent) { + System.out.println("==========开始AI推荐请求处理=========="); + // 判断用户输入文本是否过长,超过128字,直接返回,防止资源耗尽 + String message = aiIntelligent.getInputMessage(); + if(StringUtils.isBlank(message)){ + return R.error("文本不能为空"); + } + if (message.length() > 128) { + return R.error("文本字数过长"); + } + Long user_id = aiIntelligent.getUserId(); + // 查看用户接口次数是否足够,如果不够直接返回接口次数不够 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(UserInterfaceInfo::getUserId, user_id); + queryWrapper.eq(UserInterfaceInfo::getInterfaceId, 1); + UserInterfaceInfo userInterfaceInfo = userInterfaceInfoService.getOne(queryWrapper); + if (userInterfaceInfo == null) { + return R.error("该接口已废弃"); + } + Integer leftNum = userInterfaceInfo.getLeftNum(); + Integer totalNum = userInterfaceInfo.getTotalNum(); +// if (leftNum <= 0) { +// return R.error("AI接口次数不足,请明天再来"); +// } + // 限流 + boolean rateLimit = guavaRateLimiterManager.doRateLimit(user_id); +// if (!rateLimit) { +// return R.error("请求次数过多,请稍后重试"); +// } + + // 准备发送给AI的信息 + List list = booksService.list(); + StringBuilder stringBuilder = new StringBuilder(); + HashSet hashSet = new HashSet<>(); + String presetInformation = "请根据数据库内容和游客信息作出推荐,书籍优先选择数据库里面有的,如果游客喜欢的书籍,数据库没有,你可能根据自身的知识去推荐,可以是一本也可以是多本,但不可以超过三本书,根据游客喜欢的信息作出推荐,输出的文字格式都为普通格式,不要出现markdown格式。如果用户问的问题与图书推荐无关,请拒绝回答!"; + + stringBuilder.append(presetInformation).append("\n").append("数据库内容: "); + for (Books books : list) { + if (!hashSet.contains(books.getBookName())) { + hashSet.add(books.getBookName()); + stringBuilder.append(books.getBookName()).append(","); + } + } + stringBuilder.append("\n"); + + stringBuilder.append("游客信息: ").append(message).append("\n"); + + // 调用之前先获取该用户最近的五条历史记录 + R> history = aiIntelligentService.getAiInformationByUserId(user_id); + List historyData = history.getData(); + + String response; + ExecutorService executor = Executors.newSingleThreadExecutor(); + + // 转换为NewAPI OpenAI格式的消息 + List> messages = new ArrayList<>(); + + // 添加系统角色消息 + Map systemMessage = new HashMap<>(); + systemMessage.put("role", "system"); + systemMessage.put("content", presetInformation); + messages.add(systemMessage); + + // 添加历史消息 + if (historyData != null) { + for (AiIntelligent item : historyData) { + Map userMessage = new HashMap<>(); + userMessage.put("role", "user"); + userMessage.put("content", item.getInputMessage()); + messages.add(userMessage); + + Map assistantMessage = new HashMap<>(); + assistantMessage.put("role", "assistant"); + assistantMessage.put("content", item.getAiResult()); + messages.add(assistantMessage); + } + } + + // 添加当前用户消息 + Map currentUserMessage = new HashMap<>(); + currentUserMessage.put("role", "user"); + currentUserMessage.put("content", stringBuilder.toString()); + messages.add(currentUserMessage); + + // 构造请求 + Map requestBody = new HashMap<>(); + requestBody.put("model", MODEL); + requestBody.put("messages", messages); + requestBody.put("max_tokens", 2048); + requestBody.put("temperature", 0.2); + + // 打印请求信息 + System.out.println("===== 请求信息 ====="); + System.out.println("API地址: " + NEW_API_URL); + System.out.println("模型: " + MODEL); + System.out.println("消息数量: " + messages.size()); + try { + // 使用Jackson打印请求体JSON内容 + com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); + System.out.println("请求体: " + mapper.writeValueAsString(requestBody)); + } catch (Exception e) { + System.out.println("无法打印请求体: " + e.getMessage()); + } + + // 设置HTTP头部 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Authorization", "Bearer " + API_KEY); + + HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); + + int timeout = 60; + Future future = executor.submit(() -> { + try { + // 同步调用 + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + + // 发送请求到NewAPI + System.out.println("开始发送请求到API..."); + Map responseBody = null; + try { + responseBody = restTemplate.postForObject(NEW_API_URL, requestEntity, Map.class); + System.out.println("API请求成功,开始解析响应"); + // 打印完整响应 + com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); + System.out.println("完整响应: " + mapper.writeValueAsString(responseBody)); + } catch (Exception e) { + e.printStackTrace(); + System.out.println("API请求失败: " + e.getMessage()); + System.out.println("异常类型: " + e.getClass().getName()); + if (e.getCause() != null) { + System.out.println("异常原因: " + e.getCause().getMessage()); + } + return "抱歉,服务暂时不可用,请稍后再试。错误: " + e.getMessage(); + } + + stopWatch.stop(); + long total = stopWatch.getTotal(TimeUnit.SECONDS); + System.out.println("本次接口调用耗时:" + total + "秒"); + + // 解析响应 + @SuppressWarnings("unchecked") + List> choices = (List>) responseBody.get("choices"); + if (choices != null && !choices.isEmpty()) { + Map choice = choices.get(0); + @SuppressWarnings("unchecked") + Map messageObj = (Map) choice.get("message"); + String content = messageObj.get("content"); + + // 打印token使用情况 + @SuppressWarnings("unchecked") + Map usage = (Map) responseBody.get("usage"); + if (usage != null) { + System.out.println("\n提问tokens:" + usage.get("prompt_tokens") + + ",回答tokens:" + usage.get("completion_tokens") + + ",总消耗tokens:" + usage.get("total_tokens")); + } + + return content; + } + return "未能获取到有效回答"; + } catch (Exception exception) { + exception.printStackTrace(); + throw new RuntimeException("遇到异常: " + exception.getMessage()); + } + }); + + try { + System.out.println("等待Future任务完成,超时设置: " + timeout + "秒"); + response = future.get(timeout, TimeUnit.SECONDS); + System.out.println("Future任务成功完成,返回结果: " + (response != null ? response.substring(0, Math.min(50, response.length())) + "..." : "null")); + } catch (Exception e) { + e.printStackTrace(); + System.out.println("Future任务执行异常: " + e.getMessage() + ", 类型: " + e.getClass().getName()); + future.cancel(true); // 取消任务 + return R.error("服务器内部错误,请求超时,请稍后重试 (" + e.getMessage() + ")"); + } finally { + // 关闭ExecutorService + System.out.println("关闭执行器"); + executor.shutdownNow(); // 使用shutdownNow强制关闭执行器 + } + + // 保存结果 + AiIntelligent saveResult = new AiIntelligent(); + saveResult.setInputMessage(aiIntelligent.getInputMessage()); + saveResult.setAiResult(response); + saveResult.setUserId(user_id); + boolean save = this.save(saveResult); + if (!save) { + throw new VueBookException("获取AI推荐信息失败"); + } + + // 更新调用接口的次数 剩余接口调用次数-1.总共调用次数+1 + userInterfaceInfo.setLeftNum(leftNum - 1); + userInterfaceInfo.setTotalNum(totalNum + 1); + boolean update = userInterfaceInfoService.updateById(userInterfaceInfo); + if (!update) { + return R.error("调用接口信息失败"); + } + return R.success(response, "获取AI推荐信息成功"); + } + + @Override + public R> getAiInformationByUserId(Long userId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq(userId != null && userId > 0, "user_id", userId); + queryWrapper.orderByDesc("create_time"); + queryWrapper.last("LIMIT 5"); + List list = this.list(queryWrapper); + if (list.size() == 0) { + return R.success(null, "用户暂时没有和AI的聊天记录"); + } + Collections.reverse(list); + return R.success(list, "获取和AI最近的5条聊天记录成功"); + } + + public static int getSleepTimeStrategy(String message){ + int length = message.length(); + if(length<20){ + return 10; + }else if(length<=30){ + return 12; + }else if(length<=50){ + return 15; + }else{ + return 20; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/book/backend/service/impl/BookAdminsServiceImpl.java b/src/main/java/com/book/backend/service/impl/BookAdminsServiceImpl.java new file mode 100644 index 0000000..2a162a9 --- /dev/null +++ b/src/main/java/com/book/backend/service/impl/BookAdminsServiceImpl.java @@ -0,0 +1,277 @@ +package com.book.backend.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +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.book.backend.common.BasePage; +import com.book.backend.constant.Constant; +import com.book.backend.common.R; +import com.book.backend.mapper.BookAdminsMapper; +import com.book.backend.pojo.BookAdmins; +import com.book.backend.pojo.Violation; +import com.book.backend.pojo.dto.ViolationDTO; +import com.book.backend.service.BookAdminsService; +import com.book.backend.service.ViolationService; +import com.book.backend.utils.JwtKit; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.util.DigestUtils; + +import javax.annotation.Resource; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author 程序员小白条 + * @description 针对表【t_book_admins】的数据库操作Service实现 + * @createDate 2023-02-04 16:55:39 + */ +@Service +public class BookAdminsServiceImpl extends ServiceImpl + implements BookAdminsService { + /** + * 盐值,混淆密码 + */ + private static final String SALT = "xiaobaitiao"; + @Resource + private JwtKit jwtKit; + + private ViolationService violationService; + @Autowired + public BookAdminsServiceImpl(@Lazy ViolationService violationService){ + this.violationService = violationService; + } + /** + * 1.接受图书管理员的参数(用户名,密码,姓名,邮箱) + * 2.对密码进行md5加密,设置状态为1可用 + * 3.调用服务插入图书管理员,判断是否成功 + * 4.成功->200,失败->返回错误信息 + */ + @Override + public R addBookAdmin(BookAdmins bookAdmins) { + + // 获取未加密的密码 + String password = SALT + bookAdmins.getPassword(); + String md5Password = DigestUtils.md5DigestAsHex(password.getBytes()); + bookAdmins.setPassword(md5Password); + bookAdmins.setStatus(Constant.AVAILABLE); + boolean save = this.save(bookAdmins); + if (!save) { + return R.error("添加图书管理员失败"); + } + return R.success(null, "添加图书管理员成功"); + } + /** + * 1.将axios请求携带的json字符串反序列成实体类 + * 2.从实体类中获取用户名(判断空的情况),从数据库中查询,如果不存在,直接返回响应状态码404和错误信息 + * 3.用户存在,判断是否为禁用状态,如果是直接返回 + * 4.直接和数据库比对 + * 5.密码校验成功,使用工具类生成Token(传入User) + * 6.返回给前端,响应状态码 200(请求成功) 并在map动态数据中放入token,传输给前端 + */ + @Override + public R login(BookAdmins users) { + + R result = new R<>(); + // 检查用户名是否为空或null等情况 + if (StringUtils.isBlank(users.getUsername())) { + result.setStatus(404); + return R.error("用户名不存在"); + } + // 判断图书管理员是否存在 + LambdaUpdateWrapper adminWrapper = new LambdaUpdateWrapper<>(); + adminWrapper.eq(BookAdmins::getUsername, users.getUsername()); + BookAdmins bookAdminOne = this.getOne(adminWrapper); + if (bookAdminOne == null) { + result.setStatus(404); + return R.error("用户名不存在"); + } + // 系统管理员存在 判断禁用情况 + if (Constant.DISABLE.equals(bookAdminOne.getStatus())) { + return R.error("该图书管理员已被禁用"); + } + String password = DigestUtils.md5DigestAsHex((SALT+users.getPassword()).getBytes()); + if (!password.equals(bookAdminOne.getPassword())) { + result.setStatus(404); + return R.error("用户名或密码错误"); + } + // 密码校验成功 生成Token + String token = jwtKit.generateToken(users); + // 返回成功信息,并将token加入到动态数据map中 + result.setStatus(200); + result.add("token", token); + result.setMsg("登录成功"); + result.add("id", bookAdminOne.getBookAdminId()); + return result; + } + /** + * 1.先获取请求中的id + * 2.根据id到数据库中查询id是否存活 + * 3.如果存在,查询出数据, + * 4.用户数据需要脱敏 将密码设为空 + * 5.然后封装到R,设置响应状态码和请求信息,返回前端 + */ + @Override + public R getUserData(BookAdmins bookAdmins) { + + R r = new R<>(); + // 条件构造器 + LambdaQueryWrapper adminsLambdaQueryWrapper = new LambdaQueryWrapper<>(); + adminsLambdaQueryWrapper.eq(BookAdmins::getBookAdminId, bookAdmins.getBookAdminId()); + BookAdmins bookAdminOne = this.getOne(adminsLambdaQueryWrapper); + if (bookAdminOne == null) { + return R.error("图书管理员不存在"); + } + bookAdminOne.setPassword(""); + r.setData(bookAdminOne); + r.setStatus(200); + r.setMsg("获取图书管理员数据成功"); + return r; + } + /** + * 1.获取页码,页数,条件和查询内容 + * 2.判断条件或者查询内容是否有空值情况 + * 3.如果有空值,查询出所有记录(归还日期不为null),封装DTO对象,调用Page方法,返回 + * 4.创建条件构造器,like,调用booksBorrow.page(pageInfo,构造器) + * 5.如果不为空则返回正确信息,为空返回错误信息 + */ + @Override + public R> getBorrowStatement(BasePage basePage) { + + // 页数 + int pageSize = basePage.getPageSize(); + // 页码 + int pageNum = basePage.getPageNum(); + // 条件 + String condition = basePage.getCondition(); + // 内容 + String query = basePage.getQuery(); + Page pageInfo = new Page<>(pageNum, pageSize); + Page dtoPage = new Page<>(pageNum, pageSize); + R> result = new R<>(); + // 有空值的情况 + if (StringUtils.isBlank(condition) || StringUtils.isBlank(query)) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.isNotNull(Violation::getReturnDate).orderByAsc(Violation::getBorrowDate); + Page page = violationService.page(pageInfo, queryWrapper); + if (page.getTotal() == 0) { + return R.error("借书报表为空"); + } + // 不为空,封装为DTO返回 + BeanUtils.copyProperties(pageInfo, dtoPage, "records"); + List records = page.getRecords(); + List dtoList = records.stream().map(item -> { + ViolationDTO violationDTO = new ViolationDTO(); + BeanUtils.copyProperties(item, violationDTO); + // 获取图书管理员id + Integer violationAdminId = item.getViolationAdminId(); + // 根据id查询用户名 + LambdaQueryWrapper queryWrapper1 = new LambdaQueryWrapper<>(); + queryWrapper1.eq(BookAdmins::getBookAdminId, violationAdminId); + BookAdmins bookAdmins = this.getOne(queryWrapper1); + if (bookAdmins != null) { + // 获取用户名 + String username = bookAdmins.getUsername(); + violationDTO.setViolationAdmin(username); + } + return violationDTO; + }).collect(Collectors.toList()); + dtoPage.setRecords(dtoList); + result.setData(dtoPage); + result.setStatus(200); + result.setMsg("获取借书报表信息成功"); + return result; + } + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.like(condition, query).isNotNull("return_date"); + Page page = violationService.page(pageInfo, queryWrapper); + if (page.getTotal() == 0) { + return R.error("借书报表为空"); + } + BeanUtils.copyProperties(pageInfo, dtoPage, "records"); + List records = pageInfo.getRecords(); + List dtoList = records.stream().map(item -> { + ViolationDTO violationDTO = new ViolationDTO(); + BeanUtils.copyProperties(item, violationDTO); + Integer violationAdminId = item.getViolationAdminId(); + LambdaQueryWrapper queryWrapper1 = new LambdaQueryWrapper<>(); + queryWrapper1.eq(BookAdmins::getBookAdminId, violationAdminId); + BookAdmins bookAdmins = this.getOne(queryWrapper1); + if (bookAdmins != null) { + String username = bookAdmins.getUsername(); + violationDTO.setViolationAdmin(username); + } + return violationDTO; + }).collect(Collectors.toList()); + dtoPage.setRecords(dtoList); + result.setData(dtoPage); + result.setStatus(200); + result.setMsg("获取借书报表信息成功"); + return result; + } + + @Override + public R> getBookAdminListByPage(BasePage basePage) { + // 页码 + int pageNum = basePage.getPageNum(); + // 页数 + int pageSize = basePage.getPageSize(); + Page pageInfo = new Page<>(pageNum, pageSize); + Page page = this.page(pageInfo); + if (page.getTotal() == 0) { + return R.error("图书管理员列表为空"); + } + return R.success(pageInfo, "获取图书管理员列表成功"); + } + /** + * 1.调用服务查询是否有该id对应的图书管理员 + * 2.如果存在,封装到数据实体类 + * 3.不存在,返回错误信息 + */ + @Override + public R getBookAdminById(Integer bookAdminId) { + BookAdmins bookAdmins = this.getById(bookAdminId); + if (bookAdmins == null) { + return R.error("获取图书管理员信息失败"); + } + return R.success(bookAdmins, "获取图书管理员信息成功"); + } + + @Override + public R deleteBookAdminById(Integer bookAdminId) { + BookAdmins bookAdmins = this.getById(bookAdminId); + if (bookAdmins == null) { + return R.error("删除图书管理员失败"); + } + boolean remove = this.removeById(bookAdminId); + if (!remove) { + return R.error("删除图书管理员失败"); + } + return R.success(null, "删除图书管理员成功"); + } + + @Override + public R updateBookAdmin(BookAdmins bookAdmins) { + String password = bookAdmins.getPassword(); + if (password.length() >= Constant.MD5PASSWORD) { + bookAdmins.setPassword(password); + } else { + String md5Password = DigestUtils.md5DigestAsHex(password.getBytes()); + bookAdmins.setPassword(md5Password); + } + boolean update = this.updateById(bookAdmins); + if (!update) { + return R.error("修改图书管理员失败"); + } + return R.success(null, "修改图书管理员成功"); + } +} + + + + diff --git a/src/main/java/com/book/backend/service/impl/BookRuleServiceImpl.java b/src/main/java/com/book/backend/service/impl/BookRuleServiceImpl.java new file mode 100644 index 0000000..56f46cc --- /dev/null +++ b/src/main/java/com/book/backend/service/impl/BookRuleServiceImpl.java @@ -0,0 +1,152 @@ +package com.book.backend.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.book.backend.common.BasePage; +import com.book.backend.common.R; +import com.book.backend.mapper.BookRuleMapper; +import com.book.backend.pojo.BookRule; +import com.book.backend.pojo.dto.BookRuleDTO; +import com.book.backend.service.BookRuleService; +import com.book.backend.utils.NumberUtil; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** +* @author 程序员小白条 +* @description 针对表【t_book_rule】的数据库操作Service实现 +* @createDate 2023-02-05 15:11:20 +*/ +@Service +public class BookRuleServiceImpl extends ServiceImpl + implements BookRuleService{ + /** + * 1.获取所有读者规则 + * 2.判断是否为空,如果为空,设置响应状态码和请求信息返回前端 + * 3.如果不为空,则设置200响应状态码和请求信息,封装到通用类,返回前端 + */ + @Override + public R> getRuleList() { + + List list = this.list(); + R> result = new R<>(); + if (list.isEmpty()) { + result.setStatus(404); + result.setMsg("获取读者规则失败"); + return result; + } + result.setData(list); + result.setStatus(200); + result.setMsg("获取读者规则成功"); + return result; + } + /** + * 1.接受页码和页数,创建分页构造器 + * 2.调用服务的page方法,条件构造器按创建时间升序 + * 3.判断是否page返回为空 + * 4.不为空,200->前端 + */ + @Override + public R> getRuleListByPage(BasePage basePage) { + // 页码 + int pageNum = basePage.getPageNum(); + // 页数 + int pageSize = basePage.getPageSize(); + Page pageInfo = new Page<>(pageNum, pageSize); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.orderByAsc(BookRule::getCreateTime); + Page page = this.page(pageInfo, queryWrapper); + if (page.getTotal() == 0) { + return R.error("规则列表为空"); + } + R> result = new R<>(); + result.setData(pageInfo); + result.setMsg("获取规则列表成功"); + result.setStatus(200); + return result; + } + /** + * 1.获取限制天数,限制本数,逾期费用和限制的图书馆 + * 2.随机生成三位数编号 + * 3.调用服务,插入图书编号判断是否成功 + * 4.成功->200,失败->错误信息 + */ + @Override + public R addRule(BookRule bookRule) { + // 三位数 + bookRule.setBookRuleId(Integer.parseInt(new String(NumberUtil.getNumber(3)))); + boolean save = this.save(bookRule); + if (!save) { + return R.error("添加图书规则失败"); + } + return R.success(null,"添加图书规则成功"); + } + /** + * 1.先根据规则编号查询是否有该规则 + * 2.没有查询到直接返回,查询到,将属性拷贝,并将限制图书馆封装给DTO + * 3.将DTO返回前端 + */ + @Override + public R getRuleByRuleId(Integer ruleId) { + BookRule bookRule = this.getById(ruleId); + if (bookRule == null) { + return R.error("获取规则信息失败"); + } + BookRuleDTO bookRuleDTO = new BookRuleDTO(); + BeanUtils.copyProperties(bookRule, bookRuleDTO, "bookLimitLibrary"); + // + String bookLimitLibrary = bookRule.getBookLimitLibrary(); + String[] split = bookLimitLibrary.split(","); + bookRuleDTO.setCheckList(split); + R result = new R<>(); + result.setData(bookRuleDTO); + result.setStatus(200); + result.setMsg("获取规则信息成功"); + return result; + } + /** + * 1.接受限制的图书馆数组,将数组变为字符串 + * 2.拷贝属性忽略限制图书馆,单独设置字符串的限制图书馆 + * 3.调用服务更新规则,判断是否成功 + * 4.成功->200,失败->错误信息 + */ + @Transactional + @Override + public R updateRule(BookRuleDTO bookRuleDTO) { + String[] checkList = bookRuleDTO.getCheckList(); + String bookLimitLibrary = String.join(",", checkList); + BookRule bookRule = new BookRule(); + BeanUtils.copyProperties(bookRuleDTO, bookRule, "bookLimitLibrary"); + bookRule.setBookLimitLibrary(bookLimitLibrary); + boolean update = this.updateById(bookRule); + if (!update) { + return R.error("更新规则失败"); + } + return R.success(null,"更新规则成功"); + } + /** + * 1.根据规则id查询是否有该规则 + * 2.如果有调用删除的方法,判断是否成功 + */ + @Transactional + @Override + public R deleteRule(Integer ruleId) { + BookRule bookRule = this.getById(ruleId); + if (bookRule == null) { + return R.error("删除规则失败"); + } + boolean remove = this.removeById(ruleId); + if (!remove) { + return R.error("删除规则失败"); + } + return R.success(null, "删除规则成功"); + } +} + + + + diff --git a/src/main/java/com/book/backend/service/impl/BookTypeServiceImpl.java b/src/main/java/com/book/backend/service/impl/BookTypeServiceImpl.java new file mode 100644 index 0000000..7580819 --- /dev/null +++ b/src/main/java/com/book/backend/service/impl/BookTypeServiceImpl.java @@ -0,0 +1,130 @@ +package com.book.backend.service.impl; + +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.book.backend.common.BasePage; +import com.book.backend.common.R; +import com.book.backend.pojo.BookType; +import com.book.backend.service.BookTypeService; +import com.book.backend.mapper.BookTypeMapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** +* @author 程序员小白条 +* @description 针对表【t_book_type】的数据库操作Service实现 +* @createDate 2023-02-04 18:51:24 +*/ +@Service +public class BookTypeServiceImpl extends ServiceImpl + implements BookTypeService{ + + @Override + public R> getBookTypeList() { + List list = this.list(); + R> result = new R<>(); + if (list.isEmpty()) { + return R.error("获取书籍分类失败"); + } + result.setData(list); + result.setMsg("获取书籍分类成功"); + result.setStatus(200); + return result; + } + /** + * 1.获取页码和页数 + * 2.调用服务的page分页 判断是否为空 + * 3.如果不为空,存入数据,200响应状态吗,请求成功信息 + */ + @Override + public R> getBookTypeListByPage(BasePage basePage) { + + // 页码 + int pageNum = basePage.getPageNum(); + // 页数 + int pageSize = basePage.getPageSize(); + Page pageInfo = new Page<>(pageNum, pageSize); + Page page = this.page(pageInfo); + if (page.getTotal() == 0) { + return R.error("书籍分类列表为空"); + } + R> result = new R<>(); + result.setData(pageInfo); + result.setMsg("获取书籍分类列表成功"); + result.setStatus(200); + return result; + } + /** + * 1.调用服务插入书籍类别 + * 2.判断是否成功 + * 3.成功返回响应状态码和请求信息 + */ + @Override + public R addBookType(BookType bookType) { + boolean save = this.save(bookType); + if (!save) { + return R.error("添加书籍类别失败"); + } + return R.success(null,"添加书籍类型成功"); + } + /** + * 1.根据typeId查询 + * 2.判断是否为空 不为空返回前端 + */ + @Override + public R getBookTypeByTypeId(Integer typeId) { + BookType type = this.getById(typeId); + if (type == null) { + return R.error("获取书籍类别失败"); + } + R result = new R<>(); + result.setData(type); + result.setStatus(200); + result.setMsg("获取书籍类别成功"); + return result; + } + /** + * 1.判断空参数 + * 2.更新书籍 判断是否成功 + * 3.成功->200 失败->错误信息 + */ + @Override + public R updateBookType(BookType bookType) { + String typeContent = bookType.getTypeContent(); + String typeName = bookType.getTypeName(); + if (StringUtils.isBlank(typeContent) || StringUtils.isBlank(typeName)) { + return R.error("更新书籍类别失败"); + } + boolean update = this.updateById(bookType); + if (!update) { + return R.error("更新书籍类别失败"); + } + + return R.success(null,"更新书籍类别成功"); + } + /** + * 1.先根据typeId查询是否有此书籍类别 + * 2.调用服务,删除书籍类别,判断是否成功 + * 3.成功->200 失败->错误信息 + */ + @Transactional + @Override + public R deleteBookTypeByTypeId(Integer typeId) { + BookType bookType = this.getById(typeId); + if (bookType == null) { + return R.error("删除书籍类别失败"); + } + boolean remove = this.removeById(typeId); + if (!remove) { + return R.error("删除书籍类别失败"); + } + return R.success(null,"删除书籍类别成功"); + } +} + + + + diff --git a/src/main/java/com/book/backend/service/impl/BooksBorrowServiceImpl.java b/src/main/java/com/book/backend/service/impl/BooksBorrowServiceImpl.java new file mode 100644 index 0000000..60fa36a --- /dev/null +++ b/src/main/java/com/book/backend/service/impl/BooksBorrowServiceImpl.java @@ -0,0 +1,231 @@ +package com.book.backend.service.impl; + +import cn.hutool.core.date.LocalDateTimeUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +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.book.backend.common.BasePage; +import com.book.backend.constant.Constant; +import com.book.backend.common.R; +import com.book.backend.mapper.BooksBorrowMapper; +import com.book.backend.pojo.Books; +import com.book.backend.pojo.BooksBorrow; +import com.book.backend.pojo.Violation; +import com.book.backend.pojo.dto.ViolationDTO; +import com.book.backend.service.BooksBorrowService; +import com.book.backend.service.BooksService; +import com.book.backend.service.ViolationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +/** + * @author 程序员小白条 + * @description 针对表【t_books_borrow】的数据库操作Service实现 + * @createDate 2023-02-05 18:53:07 + */ +@Service +public class BooksBorrowServiceImpl extends ServiceImpl + implements BooksBorrowService { + @Resource + private ViolationService violationService; + + private BooksService booksService; + + @Autowired + public BooksBorrowServiceImpl(@Lazy BooksService booksService) { + this.booksService = booksService; + } + + /** + * 1.先根据借阅证查询是否该用户在借阅表中,如果没有直接返回 + * 2.先判断BasePage中传入的condition和query是否有空值 + * 3.如果有空值,根据借阅证查询所有的借阅信息,放入分页构造器,设置响应状态码和请求信息,返回给前端 + * 5.如果没有空值,创建条件构造器,并根据用户id、条件、内容查询 + * 6.获取借阅数据,判断是否为空,如果为空,设置响应状态码404,并提示前端查询不到数据 + * 7.如果不为空,放入分页构造器,设置响应状态码和请求信息,返回给前端 + */ + @Override + public R> getBookBorrowPage(BasePage basePage) { + + String cardNumberString = basePage.getCardNumber(); + long cardNumber = Long.parseLong(cardNumberString); + R> result = new R<>(); + QueryWrapper queryWrapper = new QueryWrapper<>(); + // 页码 + int pageNum = basePage.getPageNum(); + // 页数 + int pageSize = basePage.getPageSize(); + // 创建分页构造器 + Page pageInfo = new Page<>(pageNum, pageSize); + queryWrapper.eq("card_number", cardNumber); + List list = this.list(queryWrapper); + // 判断用户id 是否有借阅记录 + if (list.isEmpty()) { + return R.error("获取不到该用户借阅信息"); + } + // 有借阅记录 + String condition = basePage.getCondition(); + String query = basePage.getQuery(); + if (StringUtils.isBlank(condition) || StringUtils.isBlank(query)) { + LambdaQueryWrapper queryWrapper1 = new LambdaQueryWrapper<>(); + queryWrapper1.eq(BooksBorrow::getCardNumber, cardNumber).orderByAsc(BooksBorrow::getCreateTime); + this.page(pageInfo, queryWrapper1); + result.setData(pageInfo); + result.setStatus(200); + result.setMsg("获取借阅信息成功"); + return result; + } + queryWrapper.like(condition, query); + Page page = this.page(pageInfo, queryWrapper); + if (page.getTotal() == 0) { + return R.error("查询不到该借阅信息"); + } + result.setData(pageInfo); + result.setStatus(200); + result.setMsg("获取借阅信息成功"); + return result; + } + + /** + * 1.根据图书编号和归还日期(null)去借阅表中查询唯一的一条记录 + * 2.如果记录不存在,返回错误信息 + * 3.根据获取记录的,现在日期和借阅日期算出 逾期日期 expireDays + * 4.将截止日期、图书编号、逾期日期、封装到DTO,设置响应状态码和请求信息,返回前端 + */ + @Override + public R queryExpireInformationByBookNumber(Long bookNumber) { + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(BooksBorrow::getBookNumber, bookNumber).isNull(BooksBorrow::getReturnDate); + BooksBorrow bookBorrowRecord = this.getOne(queryWrapper); + if (bookBorrowRecord == null) { + return R.error("获取逾期信息失败"); + } + LocalDateTime now = LocalDateTime.now(); + LocalDateTime closeDate = bookBorrowRecord.getCloseDate(); + // 格式化 + String nowFormat = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + String closeFormat = closeDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + String[] s = nowFormat.split(" "); + String[] s1 = closeFormat.split(" "); + LocalDateTime borrow = LocalDateTimeUtil.parse(s[0] + "T" + s[1]); + LocalDateTime close = LocalDateTimeUtil.parse(s1[0] + "T" + s1[1]); + Duration between = LocalDateTimeUtil.between(borrow, close); + // 获取逾期的天数 + long expireDay = between.toDays(); + ViolationDTO violationDTO = new ViolationDTO(); + violationDTO.setExpireDays(expireDay); + violationDTO.setBookNumber(bookNumber); + violationDTO.setCloseDate(closeDate); + R result = new R<>(); + result.setData(violationDTO); + result.setStatus(200); + result.setMsg("获取逾期信息成功"); + return result; + } + + /** + * 1.获取归还日期和违章信息和图书编号,判断参数是否有异常 + * 2.根据图书编号,查询归还日期为空的记录,更新图书表 + * 3.更新违章表 + * 4.更新图书表,图书编号的借出状态 + * 5.三个表都更新则返回成功的响应状态码和请求信息,否则返回失败信息 + */ + @Transactional + @Override + public R returnBook(Violation violation) { + + Long bookNumber = violation.getBookNumber(); + LocalDateTime returnDate = violation.getReturnDate(); + if (returnDate == null) { + return R.error("归还日期不能为空"); + } + String violationMessage = violation.getViolationMessage(); + Integer violationAdminId = violation.getViolationAdminId(); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + LambdaQueryWrapper queryWrapper1 = new LambdaQueryWrapper<>(); + LambdaQueryWrapper queryWrapper2 = new LambdaQueryWrapper<>(); + + queryWrapper.eq(Violation::getBookNumber, bookNumber).isNull(Violation::getReturnDate); + queryWrapper1.eq(BooksBorrow::getBookNumber, bookNumber).isNull(BooksBorrow::getReturnDate); + queryWrapper2.eq(Books::getBookNumber, bookNumber); + Violation violation1 = violationService.getOne(queryWrapper); + BooksBorrow booksBorrow = this.getOne(queryWrapper1); + Books book = booksService.getOne(queryWrapper2); + if (violation1 == null || booksBorrow == null || book == null) { + return R.error("归还图书失败"); + } + + violation1.setViolationMessage(violationMessage); + violation1.setReturnDate(returnDate); + violation1.setViolationAdminId(violationAdminId); + booksBorrow.setReturnDate(returnDate); + book.setBookStatus(Constant.BOOKAVAILABLE); + boolean update1 = violationService.update(violation1, queryWrapper); + boolean update2 = this.update(booksBorrow, queryWrapper1); + boolean update3 = booksService.update(book, queryWrapper2); + if (!update1 || !update2 || !update3) { + return R.error("归还图书失败"); + } + + return R.success(null, "归还图书成功"); + } + + /** + * 1.获取页码,页数,条件和查询内容 + * 2.判断条件或者查询内容是否有空值情况 + * 3.如果有空值,查询出所有记录(归还日期为null) + * 4.创建条件构造器,like,调用booksBorrow.page(pageInfo,构造器) + * 5.如果不为空则返回正确信息,为空返回错误信息 + */ + @Override + public R> getReturnStatement(BasePage basePage) { + + // 页码 + int pageNum = basePage.getPageNum(); + // 页数 + int pageSize = basePage.getPageSize(); + // 内容 + String query = basePage.getQuery(); + // 条件 + String condition = basePage.getCondition(); + Page pageInfo = new Page<>(pageNum, pageSize); + R> result = new R<>(); + if (StringUtils.isBlank(condition) || StringUtils.isBlank(query)) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.isNull(BooksBorrow::getReturnDate); + Page page = this.page(pageInfo, queryWrapper); + if (page.getTotal() == 0) { + return R.error("还书报表信息为空"); + } + result.setData(pageInfo); + result.setStatus(200); + result.setMsg("获取还书报表信息成功"); + return result; + } + QueryWrapper queryWrapper1 = new QueryWrapper<>(); + queryWrapper1.like(condition, query).isNull("return_date").orderByAsc("borrow_date"); + Page page = this.page(pageInfo, queryWrapper1); + if (page.getTotal() == 0) { + return R.error("查询不到该还书报表信息"); + } + result.setData(pageInfo); + result.setMsg("获取还书报表信息成功"); + result.setStatus(200); + return result; + } +} + + + + diff --git a/src/main/java/com/book/backend/service/impl/BooksServiceImpl.java b/src/main/java/com/book/backend/service/impl/BooksServiceImpl.java new file mode 100644 index 0000000..b64c63b --- /dev/null +++ b/src/main/java/com/book/backend/service/impl/BooksServiceImpl.java @@ -0,0 +1,390 @@ +package com.book.backend.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +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.book.backend.common.BasePage; +import com.book.backend.constant.Constant; +import com.book.backend.common.R; +import com.book.backend.common.exception.CommonError; +import com.book.backend.common.exception.VueBookException; +import com.book.backend.mapper.BooksMapper; +import com.book.backend.pojo.*; +import com.book.backend.pojo.dto.BookDTO; +import com.book.backend.pojo.dto.BooksBorrowDTO; +import com.book.backend.pojo.dto.BorrowTypeDTO; +import com.book.backend.service.*; +import com.book.backend.utils.NumberUtil; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.*; + +/** + * @author 程序员小白条 + * @description 针对表【t_books】的数据库操作Service实现 + * @createDate 2023-02-04 18:07:43 + */ +@Service +public class BooksServiceImpl extends ServiceImpl + implements BooksService { + @Resource + private UsersService usersService; + @Resource + private BookRuleService bookRuleService; + @Resource + private ViolationService violationService; + @Resource + private BookTypeService bookTypeService; + private BooksBorrowService booksBorrowService; + + @Autowired + public BooksServiceImpl(@Lazy BooksBorrowService booksBorrowService) { + this.booksBorrowService = booksBorrowService; + } + + /** + * 1.先判断BasePage中传入的condition和query是否有空值 + * 2.如果有空值,查询所有书本,放入分页构造器,设置响应状态码和请求信息,返回给前端 + * 3.如果没有空值,创建条件构造器,并根据条件和内容查询 + * 4.获取书本数据,判断是否为空,如果为空,设置响应状态码404,并提示前端查询不到数据 + * 5.如果不为空,放入分页构造器,设置响应状态码和请求信息,返回给前端 + */ + @Override + public R> searchBookPage(BasePage basePage) { + + String condition = basePage.getCondition(); + String query = basePage.getQuery(); + // 页数 + int pageSize = basePage.getPageSize(); + // 当前页码 + int pageNum = basePage.getPageNum(); + // 创建分页构造器 + Page pageInfo = new Page<>(pageNum, pageSize); + R> result = new R<>(); + // 检验空值情况 + if (StringUtils.isBlank(condition) || StringUtils.isBlank(query)) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.orderByAsc(Books::getBookId); + // 分页操作(按主键升序) + this.page(pageInfo, queryWrapper); + result.setData(pageInfo); + result.setStatus(200); + result.setMsg("获取图书信息成功"); + return result; + } + // 创建条件构造器 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.like(condition, query); + // 根据条件模糊查询后分页 + Page page = this.page(pageInfo, queryWrapper); + // 如果查询不到数据 + if (page.getTotal() == 0) { + result.setMsg("查询不到该图书"); + result.setStatus(404); + return result; + } + result.setData(page); + result.setStatus(200); + result.setMsg("获取图书信息成功"); + return result; + } + + /** + * 1.接受前端请求中的参数(借阅证号、图书编号、借阅时间(不能为空)) + * 2.先根据借阅证号查询是否有此用户存在,不存在直接返回错误信息 + * 3.用户存在,根据图书编号查询图书表,查询是否有图书存在并且该图书的状态是未借出 + * 4.用户存在,图书存在,且未借出 说明可以借出该图书 + * 5.#获取用户的规则编号#,根据编号查询出规则(判断空),获取规则的可借天数 + * 6.设置期限天数为当前时间+规则的可借天数 设置归还日期为空 + * 7.调用bookBorrow,进行插入记录 + * 8.如果插入成功,修改在图书表中对应图书编号的状态为已借出 + * 9.判断是否更新成功 + * 10.插入成功+更新成功,则返回请求状态码200和请求信息 + */ + @Transactional + @Override + public R borrowBookByCardNumberAndBookNumber(BooksBorrowDTO booksBorrowDTO) { + + // 图书编号 + Long bookNumber = booksBorrowDTO.getBookNumber(); + // 借阅证号 + Long cardNumber = booksBorrowDTO.getCardNumber(); + // 借阅时间 + LocalDateTime borrowDate = booksBorrowDTO.getBorrowDate(); + if (borrowDate == null) { + return R.error("借阅时间不能为空"); + } + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(Users::getCardNumber, cardNumber); + Users users = usersService.getOne(queryWrapper); + if (users == null) { + return R.error("借阅图书失败"); + } + LambdaQueryWrapper queryWrapper1 = new LambdaQueryWrapper<>(); + queryWrapper1.eq(Books::getBookNumber, bookNumber); + Books book = this.getOne(queryWrapper1); + if ((book == null) || (book.getBookStatus().equals(Constant.BOOKDISABLE))) { + VueBookException.cast(CommonError.QUERY_NULL); + } + + // 规则编号 + Integer ruleNumber = users.getRuleNumber(); + LambdaQueryWrapper queryWrapper2 = new LambdaQueryWrapper<>(); + queryWrapper2.eq(BookRule::getBookRuleId, ruleNumber); + BookRule bookRule = bookRuleService.getOne(queryWrapper2); + if (bookRule == null) { + return R.error("借阅图书失败"); + } + // 可借天数 + Integer bookDays = bookRule.getBookDays(); + LocalDateTime closeDate = borrowDate.plusDays(bookDays); + BooksBorrow booksBorrow1 = new BooksBorrow(); + booksBorrow1.setBorrowId(null); + booksBorrow1.setBookNumber(bookNumber); + booksBorrow1.setCardNumber(cardNumber); + booksBorrow1.setBorrowDate(borrowDate); + booksBorrow1.setCloseDate(closeDate); + booksBorrow1.setReturnDate(null); + boolean flag = booksBorrowService.save(booksBorrow1); + if (!flag) { + return R.error("借阅图书失败"); + } + book.setBookStatus(Constant.BOOKDISABLE); + boolean update = this.update(book, queryWrapper1); + if (!update) { + return R.error("借阅图书失败"); + } + Violation violation = new Violation(); + BeanUtils.copyProperties(booksBorrow1, violation, "borrowId"); + violation.setViolationId(null); + violation.setViolationMessage(""); + violation.setViolationAdminId(booksBorrowDTO.getBookAdminId()); + boolean save = violationService.save(violation); + if (!save) { + return R.error("借阅图书失败"); + } + return R.success(null, "借阅图书成功"); + } + + /** + * 1.获取图书编号 + * 2.根据图书编号查询图书表中是否存在该书 + * 3.如果存在并且状态为已借出,则返回成功信息 + * 4.状态未借出:返回错误信息 + */ + @Override + public R queryBookExpireByBookNumber(Long bookNumber) { + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(Books::getBookNumber, bookNumber); + Books book = this.getOne(queryWrapper); + if (book == null || book.getBookStatus().equals(Constant.BOOKAVAILABLE)) { + return R.error("该图书未借出或图书编号不存在"); + } + return R.success(null, "图书已借出"); + } + + /** + * 1.获取页码,页数,条件和查询内容 + * 2.判断条件或者查询内容是否有空值情况 + * 3.如果有空值,查询出所有记录(根据图书编号升序) + * 4.创建条件构造器,like,调用booksBorrow.page(pageInfo,构造器) + * 5.如果不为空则返回正确信息,为空返回错误信息 + */ + @Override + public R> getBookList(BasePage basePage) { + + // 页码 + int pageNum = basePage.getPageNum(); + // 页数 + int pageSize = basePage.getPageSize(); + // 内容 + String query = basePage.getQuery(); + // 条件 + String condition = basePage.getCondition(); + Page pageInfo = new Page<>(pageNum, pageSize); + R> result = new R<>(); + if (StringUtils.isBlank(condition) || StringUtils.isBlank(query)) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.orderByAsc(Books::getBookNumber); + Page page = this.page(pageInfo, queryWrapper); + if (page.getTotal() == 0) { + return R.error("图书列表为空"); + } + result.setData(pageInfo); + result.setStatus(200); + result.setMsg("获取图书列表成功"); + return result; + + } + QueryWrapper queryWrapper1 = new QueryWrapper<>(); + queryWrapper1.like(condition, query); + Page page = this.page(pageInfo, queryWrapper1); + if (page.getTotal() == 0) { + return R.error("图书列表为空"); + } + result.setData(pageInfo); + result.setStatus(200); + result.setMsg("获取图书列表成功"); + return result; + } + + /** + * 1.获取图书名称,图书作者,图书馆名称,书籍类别的id,书籍位置,书籍状态,书籍介绍 + * 2.随机生成11位数字的图书编号 + * 3.根据书籍类别的id查询分类表中书籍类别的名称 + * 4.封装名称,保存图书 + * 5.判断是否成功,赋值相应的响应状态吗和请求信息,返回前端 + */ + @Transactional + @Override + public R addBook(BookDTO bookDTO) { + + Integer bookTypeNumber = bookDTO.getBookTypeNumber(); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(BookType::getTypeId, bookTypeNumber); + BookType bookType = bookTypeService.getOne(queryWrapper); + if (bookType == null) { + return R.error("添加图书失败"); + } + // 获取类别名称 + String typeName = bookType.getTypeName(); + // 生成11位数字的图书编号 + StringBuilder stringNumber = NumberUtil.getNumber(11); + long bookNumber = Long.parseLong(new String(stringNumber)); + Books books = new Books(); + BeanUtils.copyProperties(bookDTO, books, "book_type"); + books.setBookType(typeName); + books.setBookNumber(bookNumber); + boolean save = this.save(books); + if (!save) { + return R.error("添加图书失败"); + } + + return R.success(null, "添加图书信息成功"); + } + + /** + * 1.先根据图书id查询是否有这本图书,如果图书不存在直接返回 + * 2.图书存在,执行删除操作 + * 3.删除是否成功,返回响应的响应状态码和请求信息,返回前端 + */ + @Transactional + @Override + public R deleteBookByBookId(Integer bookId) { + + Books books = this.getById(bookId); + if (books == null) { + return R.error("删除图书失败"); + } + boolean remove = this.removeById(bookId); + if (!remove) { + return R.error("删除图书失败"); + } + return R.success(null, "删除图书成功"); + } + + /** + * 1.根据图书id获取相对应的图书 + * 2.判断图书是否为null,如果为空,返回错误信息 + * 3.如果不为空,返回图书数据,响应状态吗和请求信息。 + */ + @Override + public R getBookInformationByBookId(Integer bookId) { + Books books = this.getById(bookId); + if (books == null) { + return R.error("获取图书信息错误"); + } + R result = new R<>(); + result.setStatus(200); + result.setData(books); + result.setMsg("获取图书信息成功"); + return result; + } + + /** + * 1.判断books是否为空 + * 2.为空返回错误信息 + * 3.不为空,返回响应状态吗和正确信息 + */ + @Override + public R updateBookByEditForm(Books books) { + if (books == null) { + return R.error("修改图书失败"); + } + boolean update = this.updateById(books); + if (!update) { + return R.error("修改图书失败"); + } + return R.success(null, "修改图书成功"); + } + + /** + * 1.先获取所有的借书记录 + * 2.然后根据每条记录的图书编号去查询对应的分类 + * 3.如果hashMap中没有该分类,那么就初始化,添加String分类,然后Integer为0 + * 4.如果hashMap中有该分类,那么就获取该分类的值+1 + * 5.封装到通用格式中,返回前端 + */ + @Override + public R> getBorrowTypeStatistic() { + + HashMap hashMap = new HashMap<>(); + List list = new ArrayList<>(); + + BooksBorrowService borrowService = booksBorrowService; + List booksBorrowList = borrowService.list(); + for (BooksBorrow booksBorrow : booksBorrowList) { + Long bookNumber = booksBorrow.getBookNumber(); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(Books::getBookNumber, bookNumber); + Books book = this.getOne(queryWrapper); + if (book == null) { + VueBookException.cast(CommonError.OBJECT_NULL); + } + String bookType = book.getBookType(); + hashMap.put(bookType, hashMap.getOrDefault(bookType, 0) + 1); + } + Set> entries = hashMap.entrySet(); + for (Map.Entry entry : entries) { + BorrowTypeDTO borrowTypeDTO = new BorrowTypeDTO(); + borrowTypeDTO.setBookTypes(entry.getKey()); + borrowTypeDTO.setBorrowNumbers(entry.getValue()); + list.add(borrowTypeDTO); + } + return R.success(list, "获取借书分类统计情况成功"); + } + + /** + * 1.先获取所有的图书列表 + * 2.遍历图书列表,把所有需要删除的id都加入到一个集合中去 + * 3.调用booksService的批量删除图书(根据图书id) + * 4.判断是否成功,如何成功返回相对应的提示信息,失败则提示失败信息 + */ + @Override + public R deleteBookByBatch(List booksList) { + ArrayList list = new ArrayList<>(); + for (Books books : booksList) { + list.add(books.getBookId()); + } + boolean delete = this.removeBatchByIds(list); + if (delete) { + return R.success(null, "批量删除图书成功"); + } + return R.error("批量删除图书失败"); + } + + +} + + + + diff --git a/src/main/java/com/book/backend/service/impl/ChartServiceImpl.java b/src/main/java/com/book/backend/service/impl/ChartServiceImpl.java new file mode 100644 index 0000000..b517705 --- /dev/null +++ b/src/main/java/com/book/backend/service/impl/ChartServiceImpl.java @@ -0,0 +1,172 @@ +package com.book.backend.service.impl; + +import cn.hutool.core.io.FileUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.book.backend.common.R; +import com.book.backend.common.exception.ErrorCode; +import com.book.backend.common.exception.ThrowUtils; +import com.book.backend.manager.AiPost; +import com.book.backend.pojo.Admins; +import com.book.backend.pojo.Chart; +import com.book.backend.pojo.UserInterfaceInfo; +import com.book.backend.pojo.dto.chart.GenChartByAiRequest; +import com.book.backend.pojo.vo.BiResponse; +import com.book.backend.service.AdminsService; +import com.book.backend.service.ChartService; +import com.book.backend.mapper.ChartMapper; +import com.book.backend.utils.ExcelUtils; +import com.google.gson.Gson; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Service +public class ChartServiceImpl extends ServiceImpl implements ChartService { + + @Resource + private AdminsService adminsService; + @Resource + private AiPost aiPost; + @Resource + private UserInterfaceInfoServiceImpl userInterfaceInfoService; + + @Override + public R genChartByAi(MultipartFile multipartFile, GenChartByAiRequest genChartByAiRequest) { + String name = genChartByAiRequest.getName(); + String goal = genChartByAiRequest.getGoal(); + String chartType = genChartByAiRequest.getChartType(); + + // 参数校验 + ThrowUtils.throwIf(StringUtils.isBlank(goal), ErrorCode.PARAMS_ERROR, "目标为空"); + ThrowUtils.throwIf(StringUtils.isNotBlank(name) && name.length() > 100, ErrorCode.PARAMS_ERROR, "名称过长"); + + // 文件校验 + long size = multipartFile.getSize(); + String originalFilename = multipartFile.getOriginalFilename(); + final long ONE_MB = 1024 * 1024L; + ThrowUtils.throwIf(size > ONE_MB, ErrorCode.PARAMS_ERROR, "文件超过 1M"); + + String suffix = FileUtil.getSuffix(originalFilename); + final List validFileSuffixList = Arrays.asList("xlsx", "xls"); + ThrowUtils.throwIf(!validFileSuffixList.contains(suffix), ErrorCode.PARAMS_ERROR, "文件后缀非法"); + + // 查询接口配额 + Admins admin = adminsService.getById(genChartByAiRequest.getAdminId()); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(UserInterfaceInfo::getUserId, admin.getAdminId()); + UserInterfaceInfo interfaceInfo = userInterfaceInfoService.getOne(queryWrapper); + + if (interfaceInfo == null) { + return R.error("该接口已废弃"); + } + + Integer leftNum = interfaceInfo.getLeftNum(); + Integer totalNum = interfaceInfo.getTotalNum(); + + // 构建 AI 输入 + final String prompt = "你是一个数据分析师和前端开发专家,按照以下固定格式分析数据:\n" + + "分析需求:\n{数据分析的需求或者目标}\n" + + "原始数据:\n{csv格式的原始数据,用,作为分隔符}\n" + + "请按以下格式生成内容(不要输出任何多余的开头、结尾、注释)\n" + + "【【【【【\n" + + "{前端 Echarts V5 的 option 配置对象js代码,合理地将数据进行可视化}\n" + + "【【【【【\n" + + "{明确的数据分析结论,越详细越好}\n"; + + StringBuilder userInput = new StringBuilder(); + userInput.append(prompt); + userInput.append("你是一个数据分析师,接下来我会给你分析目标和原始数据,请告诉我分析结论。").append("\n"); + + String userGoal = goal; + if (StringUtils.isNotBlank(chartType)) { + userGoal += ",请使用" + chartType; + } + + userInput.append("分析目标:").append(userGoal).append("\n"); + userInput.append("分析需求:").append(userGoal).append("\n"); + userInput.append("原始数据: ").append("\n"); + + String csvData = ExcelUtils.excelToCsv(multipartFile); + userInput.append(csvData).append("\n"); + + String result; + try { + result = aiPost.sendMessageAndGetResponse(userInput.toString(), 1024); + System.out.println("AI返回内容:\n" + result); + } catch (Exception e) { + throw new RuntimeException("AI分析失败", e); + } + + // 解析AI返回内容 + String[] splits = result.split("【【【【【"); + if (splits.length < 3) { + return R.error("AI生成格式错误,请确认返回内容完整"); + } + + // 提取 option JSON + String optionBlock = splits[1].trim(); + Pattern jsonPattern = Pattern.compile("\\{.*\\}", Pattern.DOTALL); + Matcher jsonMatcher = jsonPattern.matcher(optionBlock); + String genChart; + + if (jsonMatcher.find()) { + genChart = jsonMatcher.group(0).trim(); + } else { + return R.error("AI返回的图表配置无法识别"); + } + + // 提取分析结论 + String genResult = splits[2].trim(); + + // 解析 Echarts 配置 + Gson gson = new Gson(); + Object objectChart; + try { + objectChart = gson.fromJson(genChart, Object.class); + } catch (Exception e) { + return R.error("图表配置JSON格式错误:" + e.getMessage()); + } + + // 保存图表数据 + Chart chart = new Chart(); + chart.setName(name); + chart.setGoal(goal); + chart.setChartData(csvData); + chart.setChartType(chartType); + chart.setGenChart(genChart); + chart.setGenResult(genResult); + chart.setAdminId(genChartByAiRequest.getAdminId()); + + boolean saveResult = this.save(chart); + ThrowUtils.throwIf(!saveResult, ErrorCode.SYSTEM_ERROR, "图表保存失败"); + + // 更新配额 + interfaceInfo.setLeftNum(leftNum - 1); + interfaceInfo.setTotalNum(totalNum + 1); + boolean update = userInterfaceInfoService.updateById(interfaceInfo); + if (!update) { + return R.error("调用接口失败"); + } + + // 构造响应 + BiResponse biResponse = new BiResponse(); + biResponse.setGenChart(genChart); + biResponse.setGenResult(genResult); + biResponse.setChartId(chart.getId()); + + R resultData = new R<>(); + resultData.add("genChart", objectChart); + resultData.setData(biResponse); + resultData.setMsg("图表生成成功"); + resultData.setStatus(200); + + return resultData; + } +} diff --git a/src/main/java/com/book/backend/service/impl/ChatServiceImpl.java b/src/main/java/com/book/backend/service/impl/ChatServiceImpl.java new file mode 100644 index 0000000..5d58a56 --- /dev/null +++ b/src/main/java/com/book/backend/service/impl/ChatServiceImpl.java @@ -0,0 +1,22 @@ +package com.book.backend.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.book.backend.pojo.Chat; +import com.book.backend.service.ChatService; +import com.book.backend.mapper.ChatMapper; +import org.springframework.stereotype.Service; + +/** +* @author xiaobaitiao +* @description 针对表【t_chat】的数据库操作Service实现 +* @createDate 2023-11-27 19:29:21 +*/ +@Service +public class ChatServiceImpl extends ServiceImpl + implements ChatService{ + +} + + + + diff --git a/src/main/java/com/book/backend/service/impl/CommentServiceImpl.java b/src/main/java/com/book/backend/service/impl/CommentServiceImpl.java new file mode 100644 index 0000000..42233dd --- /dev/null +++ b/src/main/java/com/book/backend/service/impl/CommentServiceImpl.java @@ -0,0 +1,136 @@ +package com.book.backend.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.book.backend.common.R; +import com.book.backend.mapper.CommentMapper; +import com.book.backend.pojo.Comment; +import com.book.backend.pojo.dto.CommentDTO; +import com.book.backend.service.CommentService; +import com.book.backend.utils.NumberUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @author 程序员小白条 + * @description 针对表【t_comment】的数据库操作Service实现 + * @createDate 2023-02-06 19:19:20 + */ +@Service +public class CommentServiceImpl extends ServiceImpl + implements CommentService { + /** + * 1.先查看 Redis 是否有数据,如果有的话,直接返回 Redis 中的数据 + * 2.如果 Redis 没有数据,先查询数据库,再将数据写入 Redis 缓存。 + * 2.如果弹幕列表为空,设置响应状态码404,和请求信息,返回前端 + * 3.不为空,封装为DTO,设置响应状态码200和请求信息,返回前端 + */ + // todo 如果需要 Redis 版本注释另外一个同名方法,开启此方法即可 +// @Autowired +// private RedisTemplate redisTemplate; +// +// @Override +// public R> getCommentList() { +// LocalDate localDate = LocalDate.now(); +// String key = "comment:" + localDate; +// List redisList = (List) redisTemplate.opsForList().range(key, 0, -1); +// if(redisList!=null&&!redisList.isEmpty()){ +// return R.success(redisList,"获取弹幕列表成功"); +// } +// List list = this.list(); +// R> result = new R<>(); +// if (list.isEmpty()) { +// result.setStatus(404); +// result.setMsg("弹幕列表为空"); +// return result; +// } +// List commentDTOList = list.stream().map((item) -> { +// CommentDTO commentDTO = new CommentDTO(); +// commentDTO.setAvatar(item.getCommentAvatar()); +// commentDTO.setId(item.getCommentId()); +// commentDTO.setMsg(item.getCommentMessage()); +// commentDTO.setTime(item.getCommentTime()); +// commentDTO.setBarrageStyle(item.getCommentBarrageStyle()); +// return commentDTO; +// }).collect(Collectors.toList()); +// // 向缓存插入数据 +// redisTemplate.opsForList().rightPushAll(key,commentDTOList); +// // 留言数据保存一周 +// redisTemplate.expire(key, 7, TimeUnit.DAYS); +// result.setData(commentDTOList); +// result.setMsg("获取弹幕列表成功"); +// result.setStatus(200); +// return result; +// } + /** + * 1.查询当前的弹幕列表 + * 2.如果弹幕列表为空,设置响应状态码404,和请求信息,返回前端 + * 3.不为空,封装为DTO,设置响应状态码200和请求信息,返回前端 + */ + @Override + public R> getCommentList() { + + R> result = new R<>(); + List list = this.list(); + if (list.isEmpty()) { + result.setStatus(404); + result.setMsg("弹幕列表为空"); + return result; + } + List commentDTOList = list.stream().map((item) -> { + CommentDTO commentDTO = new CommentDTO(); + commentDTO.setAvatar(item.getCommentAvatar()); + commentDTO.setId(item.getCommentId()); + commentDTO.setMsg(item.getCommentMessage()); + commentDTO.setTime(item.getCommentTime()); + commentDTO.setBarrageStyle(item.getCommentBarrageStyle()); + return commentDTO; + }).collect(Collectors.toList()); + result.setData(commentDTOList); + result.setMsg("获取弹幕列表成功"); + result.setStatus(200); + return result; + } + /** + * 1.先获取请求中的参数(msg即可) + * 2.id为null,因为数据库设置了自增 + * 3.avatar可以设置为数组中随机获取,这里暂定同一个头像 + * 4.时间随机生成(5-9秒)调用工具类 + * 5.barrageStyle从数组中随机获取 + * 6.将生成参数,传给Comment,调用service进行插入 + * 7.如果插入失败,返回404,和错误信息 + * 8.插入成功,返回200和成功信息 + */ + @Override + public R addComment(CommentDTO commentDTO) { + + String[] barrageStyleArrays = {"yibai", "erbai", "sanbai", "sibai", "wubai", "liubai", "qibai", "babai", "jiubai", "yiqian"}; + Comment comment = new Comment(); + comment.setCommentId(null); + comment.setCommentAvatar("https://img0.baidu.com/it/u=825023390,3429989944&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500"); + comment.setCommentTime(NumberUtil.getBarrageTime()); + comment.setCommentMessage(commentDTO.getMsg()); + long index = Math.round(Math.random() * 10); + //如果四舍五入后,下标越界,直接-1 + if (index == 10) { + index = 9L; + } + String s = Long.toString(index); + int newIndex = Integer.parseInt(s); + comment.setCommentBarrageStyle(barrageStyleArrays[newIndex]); + boolean flag = this.save(comment); + if (!flag) { + return R.error("添加弹幕失败"); + } + return R.success(null, "添加弹幕成功"); + } +} + + + + diff --git a/src/main/java/com/book/backend/service/impl/NoticeServiceImpl.java b/src/main/java/com/book/backend/service/impl/NoticeServiceImpl.java new file mode 100644 index 0000000..a070361 --- /dev/null +++ b/src/main/java/com/book/backend/service/impl/NoticeServiceImpl.java @@ -0,0 +1,141 @@ +package com.book.backend.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.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.book.backend.common.BasePage; +import com.book.backend.common.R; +import com.book.backend.mapper.NoticeMapper; +import com.book.backend.pojo.Notice; +import com.book.backend.service.NoticeService; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @author 程序员小白条 + * @description 针对表【t_notice】的数据库操作Service实现 + * @createDate 2023-02-05 16:14:03 + */ +@Service +public class NoticeServiceImpl extends ServiceImpl + implements NoticeService { + /** + * 1.创建条件构造器,根据日期升序,查询 + * 2.判断是否为空,如果为空,设置响应状态码和请求信息返回前端 + * 3.如果不为空,则设置200响应状态码和请求信息,封装到通用类,返回前端 + */ + @Override + public R> getNoticeList() { + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.orderByAsc(Notice::getCreateTime); + List list = this.list(queryWrapper); + R> result = new R<>(); + if (list.isEmpty()) { + result.setStatus(404); + result.setMsg("获取公告信息失败"); + return result; + } + result.setData(list); + result.setStatus(200); + result.setMsg("获取公告信息成功"); + return result; + } + + /** + * 1.查询出公告列表(创建时间升序) + * 2.判断是否为空 + * 3.为空,返回错误信息,不为空返回成功新 + */ + @Override + public R> getNoticeList(BasePage basePage) { + + int pageNum = basePage.getPageNum(); + int pageSize = basePage.getPageSize(); + Page pageInfo = new Page<>(pageNum, pageSize); + R> result = new R<>(); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.orderByAsc(Notice::getCreateTime); + Page page = this.page(pageInfo, queryWrapper); + if (page.getTotal() == 0) { + return R.error("公告列表为空"); + } + result.setStatus(200); + result.setMsg("获取公告列表成功"); + result.setData(pageInfo); + return result; + } + + /** + * 1.接受公告标题和公告内容(检查是否为空的情况),图书管理员id + * 2.如果都不为空,调用服务新增公告,返回响应状态码和请求信息 + */ + @Override + public R addNotice(Notice notice) { + + String noticeTitle = notice.getNoticeTitle(); + String noticeContent = notice.getNoticeContent(); + if (StringUtils.isBlank(noticeTitle) || StringUtils.isBlank(noticeContent)) { + return R.error("新增公告失败,存在空值"); + } + Notice notice1 = new Notice(); + notice1.setNoticeId(null); + notice1.setNoticeTitle(noticeTitle); + notice1.setNoticeContent(noticeContent); + notice1.setNoticeAdminId(notice.getNoticeAdminId()); + boolean save = this.save(notice1); + if (!save) { + return R.error("新增公告失败"); + } + return R.success(null, "新增公告成功"); + } + + @Override + public R deleteNoticeById(Integer noticeId) { + boolean remove = this.removeById(noticeId); + if (!remove) { + return R.error("删除公告失败"); + } + return R.success(null, "删除公告成功"); + } + + @Override + public R getNoticeByNoticeId(Integer noticeId) { + Notice notice = this.getById(noticeId); + if (notice == null) { + return R.error("获取公告失败"); + } + R result = new R<>(); + result.setStatus(200); + result.setMsg("获取公告信息成功"); + result.setData(notice); + return result; + } + + @Override + public R updateNoticeByNoticeId(Integer noticeId, Notice notice) { + String noticeTitle = notice.getNoticeTitle(); + String noticeContent = notice.getNoticeContent(); + if (StringUtils.isBlank(noticeTitle) || StringUtils.isBlank(noticeContent)) { + return R.error("修改公告失败"); + } + Notice notice2 = this.getById(noticeId); + if (notice2 == null) { + return R.error("修改公告失败"); + } + notice2.setNoticeTitle(noticeTitle); + notice2.setNoticeContent(noticeContent); + boolean update = this.updateById(notice2); + if (!update) { + return R.error("修改公告失败"); + } + + return R.success(null, "修改公告成功"); + } +} + + + + diff --git a/src/main/java/com/book/backend/service/impl/UserInterfaceInfoServiceImpl.java b/src/main/java/com/book/backend/service/impl/UserInterfaceInfoServiceImpl.java new file mode 100644 index 0000000..b42ea84 --- /dev/null +++ b/src/main/java/com/book/backend/service/impl/UserInterfaceInfoServiceImpl.java @@ -0,0 +1,22 @@ +package com.book.backend.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.book.backend.pojo.UserInterfaceInfo; +import com.book.backend.service.UserInterfaceInfoService; +import com.book.backend.mapper.UserInterfaceInfoMapper; +import org.springframework.stereotype.Service; + +/** +* @author xiaobaitiao +* @description 针对表【t_user_interface_info】的数据库操作Service实现 +* @createDate 2023-09-03 19:42:54 +*/ +@Service +public class UserInterfaceInfoServiceImpl extends ServiceImpl + implements UserInterfaceInfoService{ + +} + + + + diff --git a/src/main/java/com/book/backend/service/impl/UsersServiceImpl.java b/src/main/java/com/book/backend/service/impl/UsersServiceImpl.java new file mode 100644 index 0000000..af1e807 --- /dev/null +++ b/src/main/java/com/book/backend/service/impl/UsersServiceImpl.java @@ -0,0 +1,296 @@ +package com.book.backend.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +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.book.backend.common.BasePage; +import com.book.backend.constant.Constant; +import com.book.backend.common.R; +import com.book.backend.common.exception.CommonError; +import com.book.backend.common.exception.VueBookException; +import com.book.backend.mapper.UsersMapper; +import com.book.backend.pojo.Users; +import com.book.backend.pojo.dto.UsersDTO; +import com.book.backend.service.UsersService; +import com.book.backend.utils.JwtKit; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.DigestUtils; + +import javax.annotation.Resource; + +/** + * @author 程序员小白条 + * @description 针对表【t_users】的数据库操作Service实现 + * @createDate 2023-02-02 16:20:02 + */ +@Service +public class UsersServiceImpl extends ServiceImpl + implements UsersService { + + @Resource + private JwtKit jwtKit; + /** + * 盐值,混淆密码 + */ + private static final String SALT = "xiaobaitiao"; + /** + * 1.获取userId,创建条件构造器 判断userId是否为null + * 2.调用userService的getOne查询是否等于该用户id的用户 + * 3.如果没有,设置响应状态码404和请求信息,封装后,返回前端 + * 4.如果有,将用户信息脱敏后,设置响应状态码200和请求信息,封装后,返回前端 + */ + @Override + public R getUserByUserId(Integer userId) { + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + R result = new R<>(); + // 判断userId是否为null + if (userId == null) { + VueBookException.cast(CommonError.USER_NULL); + } + queryWrapper.eq(Users::getUserId, userId); + Users users = this.getOne(queryWrapper); + // 判断是否有用户id等于userId的用户 + if (users == null) { + result.setStatus(404); + result.setMsg("获取用户信息失败"); + return result; + } + // 用户信息脱敏 + users.setPassword(""); + result.setData(users); + result.setStatus(200); + result.setMsg("获取用户信息成功"); + return result; + } + + /** + * 1.获取用户传输的密码和用户id + * 2.根据用户id查询数据库是否有该用户 + * 3.将密码进行md5加密 + * 4.更新该用户的密码 + * 5.设置响应状态码和请求信息,封装后,返回前端 + */ + @Override + public R updatePassword(Users users) { + + // 条件构造器 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + Long userId = users.getUserId(); + // 当userId!=null时,调用条件构造器 + queryWrapper.eq(userId != null, Users::getUserId, userId); + String password = users.getPassword(); + Users userOne = this.getOne(queryWrapper); + // 当用户不存在时,返回错误信息 + if (userOne == null) { + return R.error("更改密码失败"); + } + // 密码加密加上盐值 + String saltPassword = SALT+password; + String md5Password = DigestUtils.md5DigestAsHex(saltPassword.getBytes()); + userOne.setPassword(md5Password); + // todo 取消注释即可成功修改密码 我这边防止有人恶意修改默认用户导致其他游客访问不了,因此取消修改密码的逻辑 +// boolean update = this.update(userOne, queryWrapper); +// if (!update) { +// return R.error("更改密码失败"); +// } + return R.success(null, "更改密码成功"); + } + + /** + * 1.将axios请求携带的json字符串反序列成实体类 + * 2.从实体类中获取用户名(判断空的情况),从数据库中查询,如果不存在,直接返回响应状态码404和错误信息 + * 3.用户存在,判断状态是否为禁用状态,如果是直接返回 + * 4.直接和数据库比对 + * 5.密码校验成功,使用工具类生成Token(传入User) + * 6.返回给前端,响应状态码 200(请求成功) 并在map动态数据中放入token,传输给前端 + */ + @Override + public R login(Users users) { + + R result = new R<>(); + // 检查用户名是否为空或null等情况 + if (StringUtils.isBlank(users.getUsername())) { + result.setStatus(404); + return R.error("用户名不存在"); + } + // 判断用户是否存在 + LambdaUpdateWrapper userWrapper = new LambdaUpdateWrapper<>(); + userWrapper.eq(Users::getUsername, users.getUsername()); + Users user = this.getOne(userWrapper); + if (user == null) { + result.setStatus(404); + return R.error("用户名不存在"); + } + // 用户存在 判断是否为禁用状态 + if (Constant.DISABLE.equals(user.getStatus())) { + return R.error("账号已被禁止登录"); + } + + String password = DigestUtils.md5DigestAsHex((SALT+users.getPassword()).getBytes()); + if (!password.equals(user.getPassword())) { + result.setStatus(404); + return R.error("用户名或密码错误"); + } + // 密码校验成功 生成Token + String token = jwtKit.generateToken(user); + // 返回成功信息,并将token加入到动态数据map中 + result.setStatus(200); + result.add("token", token); + result.setMsg("登录成功"); + result.add("id", user.getUserId()); + return result; + } + + /** + * 1.先获取请求中的id + * 2.根据id到数据库中查询id是否存活 + * 3.如果存在,查询出数据, + * 4.用户数据需要脱敏 将密码设为空 + * 5.然后封装到R,设置响应状态码和请求信息,返回前端 + */ + @Override + public R getUserData(Users users) { + + R r = new R<>(); + // 条件构造器 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(Users::getUserId, users.getUserId()); + Users userOne = this.getOne(queryWrapper); + if (userOne == null) { + return R.error("用户不存在"); + } + userOne.setPassword(""); + r.setData(userOne); + r.setStatus(200); + r.setMsg("获取用户数据成功"); + return r; + } + + /** + * 1.接受页数、页码、模糊查询条件和内容 创建分页构造器 + * 2.判断条件和内容是否有一个为空,如果为空则查询所有记录(判空),放入分页构造器 + * 3.如果二者都不为空,构造条件构造器,通过QueryWrapper的like方法模糊查询 + * 4.放入分页构造器,判断getTotal是否为空 + * 5.不为空->正确信息,为空->错误信息 + */ + @Override + public R> getStatementList(BasePage basePage) { + // 页数 + int pageSize = basePage.getPageSize(); + // 页码 + int pageNum = basePage.getPageNum(); + // 条件 + String condition = basePage.getCondition(); + // 内容 + String query = basePage.getQuery(); + R> result = new R<>(); + Page pageInfo = new Page<>(pageNum, pageSize); + if (StringUtils.isBlank(condition) || StringUtils.isBlank(query)) { + Page page = this.page(pageInfo); + if (page.getTotal() == 0) { + return R.error("借阅证列表为空"); + } + result.setStatus(200); + result.setMsg("获取借阅证列表成功"); + result.setData(pageInfo); + return result; + } + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.like(condition, query); + Page page = this.page(pageInfo, queryWrapper); + if (page.getTotal() == 0) { + return R.error("借阅证列表为空"); + } + result.setStatus(200); + result.setData(pageInfo); + result.setMsg("获取借阅证列表成功"); + return result; + } + + /** + * 1.根据用户id查询是否存在该用户 + * 2.如果存在,封装DTO返回信息 + * 3.不存在,返回错误信息 + */ + @Override + public R getStatementByUserId(Integer userId) { + Users users = this.getById(userId); + if (users == null) { + return R.error("获取用户信息失败"); + } + R result = new R<>(); + Integer status = users.getStatus(); + UsersDTO usersDTO = new UsersDTO(); + BeanUtils.copyProperties(users, usersDTO); + if (status.equals(Constant.AVAILABLE)) { + usersDTO.setUserStatus("可用"); + } else { + usersDTO.setUserStatus("禁用"); + } + result.setData(usersDTO); + result.setStatus(200); + result.setMsg("获取用户信息成功"); + return result; + } + + /** + * 1.接受用户名,密码(需要md5加密),规则编号,状态 + * 2.将usersDTO拷贝到users,忽略状态 + * 3.根据可用/禁用,设置用户的状态 + * 4.调用服务更新用户信息 + * 5.判断是否成功,成功->返回前端,错误->错误信息返回 + */ + @Override + public R updateStatement(UsersDTO usersDTO) { + Users users = new Users(); + BeanUtils.copyProperties(usersDTO, users, "password", "userStatus"); + String userStatus = usersDTO.getUserStatus(); + if (Constant.USERAVAILABLE.equals(userStatus)) { + users.setStatus(1); + } else { + users.setStatus(0); + } + String password = usersDTO.getPassword(); + if (password.length() >= Constant.MD5PASSWORD) { + users.setPassword(password); + } else { + String saltPassword = SALT+password; + String md5Password = DigestUtils.md5DigestAsHex(saltPassword.getBytes()); + users.setPassword(md5Password); + } + + boolean update = this.updateById(users); + if (!update) { + return R.error("修改借阅证信息失败"); + } + return R.success(null, "修改借阅证信息成功"); + } + + /** + * 1.根据userId查询是否有该用户 + * 2.如果有,执行删除操作,判断是否成功 + */ + @Transactional + @Override + public R deleteStatementByUserId(Integer userId) { + Users users = this.getById(userId); + if (users == null) { + return R.error("删除借阅证失败"); + } + boolean remove = this.removeById(userId); + if (!remove) { + return R.error("删除借阅证失败"); + } + return R.success(null, "删除借阅证成功"); + } +} + + + + diff --git a/src/main/java/com/book/backend/service/impl/ViolationServiceImpl.java b/src/main/java/com/book/backend/service/impl/ViolationServiceImpl.java new file mode 100644 index 0000000..4732f57 --- /dev/null +++ b/src/main/java/com/book/backend/service/impl/ViolationServiceImpl.java @@ -0,0 +1,175 @@ +package com.book.backend.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +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.book.backend.common.BasePage; +import com.book.backend.common.R; +import com.book.backend.mapper.ViolationMapper; +import com.book.backend.pojo.BookAdmins; +import com.book.backend.pojo.Violation; +import com.book.backend.pojo.dto.BorrowData; +import com.book.backend.pojo.dto.ViolationDTO; +import com.book.backend.service.BookAdminsService; +import com.book.backend.service.ViolationService; +import com.book.backend.utils.BorrowDateUtil; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author 程序员小白条 + * @description 针对表【t_violation】的数据库操作Service实现 + * @createDate 2023-02-06 16:31:20 + */ +@Service +public class ViolationServiceImpl extends ServiceImpl + implements ViolationService { + private BookAdminsService bookAdminsService; + + @Autowired + public ViolationServiceImpl(@Lazy BookAdminsService bookAdminsService) { + this.bookAdminsService = bookAdminsService; + } + + /** + * 1.先根据借阅证查询是否该用户在违章表中,如果没有直接返回 + * 2.先判断BasePage中传入的condition和query是否有空值 + * 3.如果有空值,根据借阅证查询所有的违章信息,进行分页查询 + * 3.1利用对象拷贝BeanUtils将分页构造器拷贝到DTO的分页构造器(忽略records) + * 3.2根据图书管理员的id去获取用户名,然后封装到每一个DTO,返回DTOList + * 3.3利用DTO分页构造器的setRecords重新赋值DTOList,将DTO分页构造器放入通用类的数据中,设置响应状态码和请求信息,返回前端 + * 4.如果没有空值,创建条件构造器,并根据借阅证编号、条件、内容查询 + * 5.获取违章数据,判断是否为空,如果为空,设置响应状态码404,并提示前端查询不到数据 + * 6.如果不为空,先放入原来的分页构造器 + * 6.1利用对象拷贝BeanUtils将分页构造器拷贝到DTO的分页构造器(忽略records) + * 6.2根据图书管理员的id去获取用户名,然后封装到每一个DTO,返回DTOList + * 6.3利用DTO分页构造器的setRecords重新赋值DTOList,将DTO分页构造器放入通用类的数据中,设置响应状态码和请求信息,返回前端 + */ + @Override + public R> getViolationListByPage(BasePage basePage) { + + String cardNumber = basePage.getCardNumber(); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("card_number", cardNumber); + List list = this.list(queryWrapper); + if (list.isEmpty()) { + return R.error("没有该用户的违章信息"); + } + // 页码 + int pageNum = basePage.getPageNum(); + // 页数 + int pageSize = basePage.getPageSize(); + // 两个分页构造器 + Page pageInfo = new Page<>(pageNum, pageSize); + Page dtoPage = new Page<>(pageNum, pageSize); + // 创建返回结果类 + R> result = new R<>(); + // 有违章记录 + String condition = basePage.getCondition(); + String query = basePage.getQuery(); + if (StringUtils.isBlank(condition) || StringUtils.isBlank(query)) { + LambdaQueryWrapper queryWrapper1 = new LambdaQueryWrapper<>(); + queryWrapper1.eq(Violation::getCardNumber, cardNumber).orderByAsc(Violation::getCreateTime); + this.page(pageInfo, queryWrapper1); + BeanUtils.copyProperties(pageInfo, dtoPage, "records"); + List records = pageInfo.getRecords(); + List listDTO = records.stream().map((item) -> { + ViolationDTO violationDTO = new ViolationDTO(); + // 对象拷贝 + BeanUtils.copyProperties(item, violationDTO); + // 获取图书管理员的id + Integer violationAdminId = item.getViolationAdminId(); + // 根据id去查询用户名 + LambdaQueryWrapper queryWrapper2 = new LambdaQueryWrapper<>(); + queryWrapper2.eq(BookAdmins::getBookAdminId, violationAdminId); + BookAdmins bookAdmins = bookAdminsService.getOne(queryWrapper2); + if (bookAdmins != null) { + // 获取用户名 + String username = bookAdmins.getUsername(); + violationDTO.setViolationAdmin(username); + } + return violationDTO; + }).collect(Collectors.toList()); + dtoPage.setRecords(listDTO); + result.setData(dtoPage); + result.setMsg("获取违章记录成功"); + result.setStatus(200); + return result; + } + // 模糊查询 + queryWrapper.like(condition, query); + Page page = this.page(pageInfo, queryWrapper); + if (page.getTotal() == 0) { + return R.error("查询不到该用户的违章记录"); + } + BeanUtils.copyProperties(pageInfo, dtoPage, "records"); + List records2 = pageInfo.getRecords(); + List violationDTOList = records2.stream().map((item) -> { + ViolationDTO violationDTO2 = new ViolationDTO(); + BeanUtils.copyProperties(item, violationDTO2); + // 获取图书管理员的id + Integer violationAdminId = item.getViolationAdminId(); + // 根据id获取用户名 + LambdaQueryWrapper queryWrapper3 = new LambdaQueryWrapper<>(); + queryWrapper3.eq(BookAdmins::getBookAdminId, violationAdminId); + BookAdmins bookAdmins2 = bookAdminsService.getOne(queryWrapper3); + if (bookAdmins2 != null) { + // 获取用户名 + String username = bookAdmins2.getUsername(); + violationDTO2.setViolationAdmin(username); + } + return violationDTO2; + }).collect(Collectors.toList()); + dtoPage.setRecords(violationDTOList); + result.setData(dtoPage); + result.setMsg("获取违章信息成功"); + result.setStatus(200); + return result; + } + + /** + * 1.分别获取5个时间节点,计算四个间隔之间的借阅量,也就是一周的借阅量 + * 2.时间格式化 然后封装到BorrowDate的日期数组中 再分别封装借阅量 + * 3.返回前端 + */ + @Override + public R getBorrowDate() { + LocalDateTime now = LocalDateTime.now(); + String[] dateArray = BorrowDateUtil.getDateArray(now); + LocalDateTime time1 = now.minusWeeks(1); + LocalDateTime time2 = now.minusWeeks(2); + LocalDateTime time3 = now.minusWeeks(3); + LocalDateTime time4 = now.minusWeeks(4); + LambdaQueryWrapper queryWrapper1 = new LambdaQueryWrapper<>(); + queryWrapper1.between(Violation::getBorrowDate, time1, now); + LambdaQueryWrapper queryWrapper2 = new LambdaQueryWrapper<>(); + queryWrapper2.between(Violation::getBorrowDate, time2, time1); + LambdaQueryWrapper queryWrapper3 = new LambdaQueryWrapper<>(); + queryWrapper3.between(Violation::getBorrowDate, time3, time2); + LambdaQueryWrapper queryWrapper4 = new LambdaQueryWrapper<>(); + queryWrapper4.between(Violation::getBorrowDate, time4, time3); + List list1 = this.list(queryWrapper1); + List list2 = this.list(queryWrapper2); + List list3 = this.list(queryWrapper3); + List list4 = this.list(queryWrapper4); + Integer[] borrowNumbers = new Integer[4]; + borrowNumbers[3] = list1.size(); + borrowNumbers[1] = list2.size(); + borrowNumbers[2] = list3.size(); + borrowNumbers[0] = list4.size(); + BorrowData borrowData = new BorrowData(dateArray, borrowNumbers); + return R.success(borrowData, "获取借阅量成功"); + } +} + + + + diff --git a/src/main/java/com/book/backend/utils/BorrowDateUtil.java b/src/main/java/com/book/backend/utils/BorrowDateUtil.java new file mode 100644 index 0000000..070545a --- /dev/null +++ b/src/main/java/com/book/backend/utils/BorrowDateUtil.java @@ -0,0 +1,28 @@ +package com.book.backend.utils; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * @author 程序员小白条 + */ +public class BorrowDateUtil { + public static String [] getDateArray(LocalDateTime now){ + String nowFormat = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + LocalDateTime time1 = now.minusWeeks(1); + LocalDateTime time2 = now.minusWeeks(2); + LocalDateTime time3 = now.minusWeeks(3); + LocalDateTime time4 = now.minusWeeks(4); + String format1 = time1.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + String format2 = time2.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + String format3 = time3.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + String format4 = time4.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + String [] dateArrays = new String[4]; + dateArrays[3]= format1+"-"+nowFormat; + dateArrays[2]= format2+"-"+format1; + dateArrays[1] = format3+"-"+format2; + dateArrays[0] = format4+"-"+format3; + return dateArrays; + } + +} diff --git a/src/main/java/com/book/backend/utils/ExcelUtils.java b/src/main/java/com/book/backend/utils/ExcelUtils.java new file mode 100644 index 0000000..52658d5 --- /dev/null +++ b/src/main/java/com/book/backend/utils/ExcelUtils.java @@ -0,0 +1,68 @@ +package com.book.backend.utils; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.support.ExcelTypeEnum; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Excel 相关工具类 + */ +@Slf4j +public class ExcelUtils { + + /** + * excel 转 csv + * + * @param multipartFile + * @return + */ + public static String excelToCsv(MultipartFile multipartFile) { +// File file = null; +// try { +// file = ResourceUtils.getFile("classpath:网站数据.xlsx"); +// } catch (FileNotFoundException e) { +// e.printStackTrace(); +// } + // 读取数据 + List> list = null; + try { + list = EasyExcel.read(multipartFile.getInputStream()) + .excelType(ExcelTypeEnum.XLSX) + .sheet() + .headRowNumber(0) + .doReadSync(); + } catch (IOException e) { + log.error("表格处理错误", e); + } + if (CollUtil.isEmpty(list)) { + return ""; + } + // 转换为 csv + StringBuilder stringBuilder = new StringBuilder(); + // 读取表头 + LinkedHashMap headerMap = (LinkedHashMap) list.get(0); + List headerList = headerMap.values().stream().filter(ObjectUtils::isNotEmpty).collect(Collectors.toList()); + stringBuilder.append(StringUtils.join(headerList, ",")).append("\n"); + // 读取数据 + for (int i = 1; i < list.size(); i++) { + LinkedHashMap dataMap = (LinkedHashMap) list.get(i); + List dataList = dataMap.values().stream().filter(ObjectUtils::isNotEmpty).collect(Collectors.toList()); + stringBuilder.append(StringUtils.join(dataList, ",")).append("\n"); + } + return stringBuilder.toString(); + } + + public static void main(String[] args) { + excelToCsv(null); + } +} diff --git a/src/main/java/com/book/backend/utils/JwtKit.java b/src/main/java/com/book/backend/utils/JwtKit.java new file mode 100644 index 0000000..d9ae717 --- /dev/null +++ b/src/main/java/com/book/backend/utils/JwtKit.java @@ -0,0 +1,71 @@ +package com.book.backend.utils; + + +import com.book.backend.common.JwtProperties; +import com.book.backend.pojo.Users; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * + * JWT工具类 + * + * @author xiaobaitiao + * + * + */ +@Component +public class JwtKit { + @Resource + private JwtProperties jwtProperties; + + /** + * 生成Token + * + * @param user 自定义要存储的用户对象信息 + * @return string(Token) + */ + public String generateToken(T user) { + Map claims = new HashMap(10); + claims.put("username", user.toString()); + claims.put("createdate", new Date()); + claims.put("id", System.currentTimeMillis()); + // 要存储的数据 + return Jwts.builder().addClaims(claims) + // 过期时间 + .setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getExpiration())) + // 加密算法和密钥 + .signWith(SignatureAlgorithm.HS256, jwtProperties.getSecret()) + .compact(); // 打包返回 3部分 + } + + public JwtKit() { + } + + public JwtKit(JwtProperties jwtProperties) { + this.jwtProperties = jwtProperties; + } + + /** + * 校验Token是否合法 + * + * @param token 要校验的Token + * @return Claims (过期时间,用户信息,创建时间) + */ + public Claims parseJwtToken(String token) { + Claims claims = null; + // 根据哪个密钥解密 + claims = Jwts.parser().setSigningKey(jwtProperties.getSecret()) + // 设置要解析的Token + .parseClaimsJws(token) + .getBody(); + return claims; + } +} diff --git a/src/main/java/com/book/backend/utils/NetUtils.java b/src/main/java/com/book/backend/utils/NetUtils.java new file mode 100644 index 0000000..400720b --- /dev/null +++ b/src/main/java/com/book/backend/utils/NetUtils.java @@ -0,0 +1,53 @@ +package com.book.backend.utils; + +import javax.servlet.http.HttpServletRequest; +import java.net.InetAddress; + +/** + * 网络工具类 + * + */ +public class NetUtils { + + /** + * 获取客户端 IP 地址 + * + * @param request + * @return + */ + public static String getIpAddress(HttpServletRequest request) { + String ip = request.getHeader("x-forwarded-for"); + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + if (ip.equals("127.0.0.1")) { + // 根据网卡取本机配置的 IP + InetAddress inet = null; + try { + inet = InetAddress.getLocalHost(); + } catch (Exception e) { + e.printStackTrace(); + } + if (inet != null) { + ip = inet.getHostAddress(); + } + } + } + // 多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割 + if (ip != null && ip.length() > 15) { + if (ip.indexOf(",") > 0) { + ip = ip.substring(0, ip.indexOf(",")); + } + } + if (ip == null) { + return "127.0.0.1"; + } + return ip; + } + +} diff --git a/src/main/java/com/book/backend/utils/NumberUtil.java b/src/main/java/com/book/backend/utils/NumberUtil.java new file mode 100644 index 0000000..7c32d13 --- /dev/null +++ b/src/main/java/com/book/backend/utils/NumberUtil.java @@ -0,0 +1,77 @@ +package com.book.backend.utils; +import java.math.BigDecimal; +import java.util.Random; + +/** + * @author 程序员小白条 + */ +public class NumberUtil { + public static Integer getLibraryInt(){ + return new Random().nextInt(3); + } + public static Integer getStatusInt() { + //[0 - 5) + return new Random().nextInt(5); + } + + public static Integer getAgeInt() { + // [10 - 60) 60 = 10 + 50 + return new Random().nextInt(50) + 10; + } + public static Integer getBarrageTime(){ + // [5,9] 整数 + return new Random().nextInt(5)+5; + } + public static Integer getSexInt() { + //[0 - 2) + return new Random().nextInt(2); + } + + public static Integer getLevelInt() { + return new Random().nextInt(100); + } + + public static BigDecimal accountDecimal() { + float minF = 1000000.0f; + float maxF = 1000.0f; + + //生成随机数 + BigDecimal bd = new BigDecimal(Math.random() * (maxF - minF) + minF); + + //返回保留两位小数的随机数。不进行四舍五入 + return bd.setScale(4,BigDecimal.ROUND_DOWN); + } + + public static BigDecimal balanceDecimal() { + float minF = 1000000.0f; + float maxF = 1000.0f; + + //生成随机数 + BigDecimal bd = new BigDecimal(Math.random() * (maxF - minF) + minF); + + //返回保留两位小数的随机数。不进行四舍五入 + return bd.setScale(4,BigDecimal.ROUND_DOWN); + } + + public static Long randomLong() { + return Math.abs(new Random().nextLong()); + } + //num为随机数字字符串的长度 + public static StringBuilder getNumber(int num){ + StringBuilder sb = new StringBuilder(); + Random random = new Random(); + for (int i = 0; i < num; i++){ + sb.append(random.nextInt(10)); + } + return sb; + } + + public static void main(String[] args) { + for (int i = 0; i < 1000; i++) { + Integer libraryInt = getLibraryInt(); + System.out.println(libraryInt); + } + } + +} + diff --git a/src/main/java/com/book/backend/utils/RandomNameUtils.java b/src/main/java/com/book/backend/utils/RandomNameUtils.java new file mode 100644 index 0000000..55925bf --- /dev/null +++ b/src/main/java/com/book/backend/utils/RandomNameUtils.java @@ -0,0 +1,92 @@ +package com.book.backend.utils; +import cn.hutool.core.util.RandomUtil; + +import java.io.UnsupportedEncodingException; +import java.util.Random; + + +/** + * @author 程序员小白条 + */ +public class RandomNameUtils { + + /** + * 随机获取姓名 + * + * @return + */ + public static String fullName() { + return surname() + name(2); + } + + /** + * 随机获取姓 + * + * @return + */ + public static String surname() { + return SURNAME[new Random().nextInt(SURNAME.length - 1)]; + } + + /** + * 获取N位常用字 + * + * @param len + * @return + */ + public static String name(int len) { + StringBuilder ret = new StringBuilder(); + for (int i = 0; i < len; i++) { + String str = null; + // 定义高低位 + int highPos, lowPos; + Random random = new Random(); + //获取高位值 + highPos = (176 + Math.abs(random.nextInt(39))); + //获取低位值 + lowPos = (161 + Math.abs(random.nextInt(93))); + byte[] b = new byte[2]; + b[0] = (new Integer(highPos).byteValue()); + b[1] = (new Integer(lowPos).byteValue()); + try { + //转成中文 + str = new String(b, "GBK"); + } catch (UnsupportedEncodingException ex) { + ex.printStackTrace(); + } + ret.append(str); + } + return ret.toString(); + } + public static String getRandomLocation(){ + int i=(int)Math.round(Math.random()*26); + int j=(int)'A'+i; + char ch=(char)j; + String s = RandomUtil.randomNumbers(6); + return ch+s; + } + /** + * 2021年姓排行100 + */ + private final static String[] SURNAME = { + "李", "王", "张", "刘", "陈", + "杨", "赵", "黄", "周", "吴", + "徐", "孙", "胡", "朱", "高", + "林", "何", "郭", "马", "罗", + "梁", "宋", "郑", "谢", "韩", + "唐", "冯", "于", "董", "萧", + "程", "曹", "袁", "邓", "许", + "傅", "沈", "曾", "彭", "吕", + "苏", "卢", "蒋", "蔡", "贾", + "丁", "魏", "薛", "叶", "阎", + "余", "潘", "杜", "戴", "夏", + "钟", "汪", "田", "任", "姜", + "范", "方", "石", "姚", "谭", + "廖", "邹", "熊", "金", "陆", + "郝", "孔", "白", "崔", "康", + "毛", "邱", "秦", "江", "史", + "顾", "侯", "邵", "孟", "龙", + "万", "段", "漕", "钱", "汤", + "尹", "黎", "易", "常", "武", + "乔", "贺", "赖", "龚", "文"}; +} diff --git a/src/main/java/com/book/backend/utils/RedisUtil.java b/src/main/java/com/book/backend/utils/RedisUtil.java new file mode 100644 index 0000000..bc04a15 --- /dev/null +++ b/src/main/java/com/book/backend/utils/RedisUtil.java @@ -0,0 +1,524 @@ +package com.book.backend.utils; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + + +@Configuration +public class RedisUtil { + + @Autowired + private RedisTemplate redisTemplate; + /** + * 给一个指定的 key 值附加过期时间 + * + * @param key + * @param time + * @return + */ + public boolean expire(String key, long time) { + return redisTemplate.expire(key, time, TimeUnit.SECONDS); + } + /** + * 根据key 获取过期时间 + * + * @param key + * @return + */ + public long getTime(String key) { + return redisTemplate.getExpire(key, TimeUnit.SECONDS); + } + /** + * 根据key 获取过期时间 + * + * @param key + * @return + */ + public boolean hasKey(String key) { + return redisTemplate.hasKey(key); + } + /** + * 移除指定key 的过期时间 + * + * @param key + * @return + */ + public boolean persist(String key) { + return redisTemplate.boundValueOps(key).persist(); + } + + //- - - - - - - - - - - - - - - - - - - - - String类型 - - - - - - - - - - - - - - - - - - - - + + /** + * 根据key获取值 + * + * @param key 键 + * @return 值 + */ + public Object get(String key) { + return key == null ? null : redisTemplate.opsForValue().get(key); + } + + /** + * 将值放入缓存 + * + * @param key 键 + * @param value 值 + * @return true成功 false 失败 + */ + public void set(String key, String value) { + redisTemplate.opsForValue().set(key, value); + } + + /** + * 将值放入缓存并设置时间 + * + * @param key 键 + * @param value 值 + * @param time 时间(秒) -1为无期限 + * @return true成功 false 失败 + */ + public void set(String key, String value, long time) { + if (time > 0) { + redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); + } else { + redisTemplate.opsForValue().set(key, value); + } + } + + /** + * 批量添加 key (重复的键会覆盖) + * + * @param keyAndValue + */ + public void batchSet(Map keyAndValue) { + redisTemplate.opsForValue().multiSet(keyAndValue); + } + + /** + * 批量添加 key-value 只有在键不存在时,才添加 + * map 中只要有一个key存在,则全部不添加 + * + * @param keyAndValue + */ + public void batchSetIfAbsent(Map keyAndValue) { + redisTemplate.opsForValue().multiSetIfAbsent(keyAndValue); + } + + /** + * 对一个 key-value 的值进行加减操作, + * 如果该 key 不存在 将创建一个key 并赋值该 number + * 如果 key 存在,但 value 不是长整型 ,将报错 + * + * @param key + * @param number + */ + public Long increment(String key, long number) { + return redisTemplate.opsForValue().increment(key, number); + } + + /** + * 对一个 key-value 的值进行加减操作, + * 如果该 key 不存在 将创建一个key 并赋值该 number + * 如果 key 存在,但 value 不是 纯数字 ,将报错 + * + * @param key + * @param number + */ + public Double increment(String key, double number) { + return redisTemplate.opsForValue().increment(key, number); + } + + //- - - - - - - - - - - - - - - - - - - - - set类型 - - - - - - - - - - - - - - - - - - - - + + /** + * 将数据放入set缓存 + * + * @param key 键 + * @return + */ + public void sSet(String key, String value) { + redisTemplate.opsForSet().add(key, value); + } + + /** + * 获取变量中的值 + * + * @param key 键 + * @return + */ + public Set members(String key) { + return redisTemplate.opsForSet().members(key); + } + + /** + * 随机获取变量中指定个数的元素 + * + * @param key 键 + * @param count 值 + * @return + */ + public void randomMembers(String key, long count) { + redisTemplate.opsForSet().randomMembers(key, count); + } + + /** + * 随机获取变量中的元素 + * + * @param key 键 + * @return + */ + public Object randomMember(String key) { + return redisTemplate.opsForSet().randomMember(key); + } + + /** + * 弹出变量中的元素 + * + * @param key 键 + * @return + */ + public Object pop(String key) { + return redisTemplate.opsForSet().pop("setValue"); + } + + /** + * 获取变量中值的长度 + * + * @param key 键 + * @return + */ + public long size(String key) { + return redisTemplate.opsForSet().size(key); + } + + /** + * 根据value从一个set中查询,是否存在 + * + * @param key 键 + * @param value 值 + * @return true 存在 false不存在 + */ + public boolean sHasKey(String key, Object value) { + return redisTemplate.opsForSet().isMember(key, value); + } + + /** + * 检查给定的元素是否在变量中。 + * + * @param key 键 + * @param obj 元素对象 + * @return + */ + public boolean isMember(String key, Object obj) { + return redisTemplate.opsForSet().isMember(key, obj); + } + + /** + * 转移变量的元素值到目的变量。 + * + * @param key 键 + * @param value 元素对象 + * @param destKey 元素对象 + * @return + */ + public boolean move(String key, String value, String destKey) { + return redisTemplate.opsForSet().move(key, value, destKey); + } + + /** + * 批量移除set缓存中元素 + * + * @param key 键 + * @param values 值 + * @return + */ + public void remove(String key, Object... values) { + redisTemplate.opsForSet().remove(key, values); + } + + /** + * 通过给定的key求2个set变量的差值 + * + * @param key 键 + * @param destKey 键 + * @return + */ + public Set difference(String key, String destKey) { + return redisTemplate.opsForSet().difference(key, destKey); + } + + + //- - - - - - - - - - - - - - - - - - - - - hash类型 - - - - - - - - - - - - - - - - - - - - + + /** + * 加入缓存 + * + * @param key 键 + * @param map 键 + * @return + */ + public void add(String key, Map map) { + redisTemplate.opsForHash().putAll(key, map); + } + + /** + * 获取 key 下的 所有 hashkey 和 value + * + * @param key 键 + * @return + */ + public Map getHashEntries(String key) { + return redisTemplate.opsForHash().entries(key); + } + + /** + * 验证指定 key 下 有没有指定的 hashkey + * + * @param key + * @param hashKey + * @return + */ + public boolean hashKey(String key, String hashKey) { + return redisTemplate.opsForHash().hasKey(key, hashKey); + } + + /** + * 获取指定key的值string + * + * @param key 键 + * @param key2 键 + * @return + */ + public String getMapString(String key, String key2) { + return redisTemplate.opsForHash().get("map1", "key1").toString(); + } + + /** + * 获取指定的值Int + * + * @param key 键 + * @param key2 键 + * @return + */ + public Integer getMapInt(String key, String key2) { + return (Integer) redisTemplate.opsForHash().get("map1", "key1"); + } + + /** + * 弹出元素并删除 + * + * @param key 键 + * @return + */ + public String popValue(String key) { + return redisTemplate.opsForSet().pop(key).toString(); + } + + /** + * 删除指定 hash 的 HashKey + * + * @param key + * @param hashKeys + * @return 删除成功的 数量 + */ + public Long delete(String key, String... hashKeys) { + return redisTemplate.opsForHash().delete(key, hashKeys); + } + + /** + * 给指定 hash 的 hashkey 做增减操作 + * + * @param key + * @param hashKey + * @param number + * @return + */ + public Long increment(String key, String hashKey, long number) { + return redisTemplate.opsForHash().increment(key, hashKey, number); + } + + /** + * 给指定 hash 的 hashkey 做增减操作 + * + * @param key + * @param hashKey + * @param number + * @return + */ + public Double increment(String key, String hashKey, Double number) { + return redisTemplate.opsForHash().increment(key, hashKey, number); + } + + /** + * 获取 key 下的 所有 hashkey 字段 + * + * @param key + * @return + */ + public Set hashKeys(String key) { + return redisTemplate.opsForHash().keys(key); + } + + /** + * 获取指定 hash 下面的 键值对 数量 + * + * @param key + * @return + */ + public Long hashSize(String key) { + return redisTemplate.opsForHash().size(key); + } + + //- - - - - - - - - - - - - - - - - - - - - list类型 - - - - - - - - - - - - - - - - - - - - + + /** + * 在变量左边添加元素值 + * + * @param key + * @param value + * @return + */ + public void leftPush(String key, Object value) { + redisTemplate.opsForList().leftPush(key, value); + } + + /** + * 获取集合指定位置的值。 + * + * @param key + * @param index + * @return + */ + public Object index(String key, long index) { + return redisTemplate.opsForList().index("list", 1); + } + + /** + * 获取指定区间的值。 + * + * @param key + * @param start + * @param end + * @return + */ + public List range(String key, long start, long end) { + return redisTemplate.opsForList().range(key, start, end); + } + + /** + * 把最后一个参数值放到指定集合的第一个出现中间参数的前面, + * 如果中间参数值存在的话。 + * + * @param key + * @param pivot + * @param value + * @return + */ + public void leftPush(String key, String pivot, String value) { + redisTemplate.opsForList().leftPush(key, pivot, value); + } + + /** + * 向左边批量添加参数元素。 + * + * @param key + * @param values + * @return + */ + public void leftPushAll(String key, String... values) { +// redisTemplate.opsForList().leftPushAll(key,"w","x","y"); + redisTemplate.opsForList().leftPushAll(key, values); + } + + /** + * 向集合最右边添加元素。 + * + * @param key + * @param value + * @return + */ + public void leftPushAll(String key, String value) { + redisTemplate.opsForList().rightPush(key, value); + } + + /** + * 向左边批量添加参数元素。 + * + * @param key + * @param values + * @return + */ + public void rightPushAll(String key, String... values) { + //redisTemplate.opsForList().leftPushAll(key,"w","x","y"); + redisTemplate.opsForList().rightPushAll(key, values); + } + + /** + * 向已存在的集合中添加元素。 + * + * @param key + * @param value + * @return + */ + public void rightPushIfPresent(String key, Object value) { + redisTemplate.opsForList().rightPushIfPresent(key, value); + } + + /** + * 向已存在的集合中添加元素。 + * + * @param key + * @return + */ + public long listLength(String key) { + return redisTemplate.opsForList().size(key); + } + + /** + * 移除集合中的左边第一个元素。 + * + * @param key + * @return + */ + public void leftPop(String key) { + redisTemplate.opsForList().leftPop(key); + } + + /** + * 移除集合中左边的元素在等待的时间里,如果超过等待的时间仍没有元素则退出。 + * + * @param key + * @return + */ + public void leftPop(String key, long timeout, TimeUnit unit) { + redisTemplate.opsForList().leftPop(key, timeout, unit); + } + + /** + * 移除集合中右边的元素。 + * + * @param key + * @return + */ + public void rightPop(String key) { + redisTemplate.opsForList().rightPop(key); + } + + /** + * 移除集合中右边的元素在等待的时间里,如果超过等待的时间仍没有元素则退出。 + * + * @param key + * @return + */ + public void rightPop(String key, long timeout, TimeUnit unit) { + redisTemplate.opsForList().rightPop(key, timeout, unit); + } +} diff --git a/src/main/java/com/book/backend/utils/SpringBootUtil.java b/src/main/java/com/book/backend/utils/SpringBootUtil.java new file mode 100644 index 0000000..4c6f592 --- /dev/null +++ b/src/main/java/com/book/backend/utils/SpringBootUtil.java @@ -0,0 +1,63 @@ +package com.book.backend.utils; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +/** + * Spring上下文工具类,用以让普通类获取Spring容器中的Bean + * @author 程序员小白条 + */ +@Component +public class SpringBootUtil implements ApplicationContextAware { + + private static ApplicationContext applicationContext = null; + + /** + * 获取applicationContext + * @return ApplicationContext + */ + public static ApplicationContext getApplicationContext() { + return applicationContext; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + if (SpringBootUtil.applicationContext == null) { + SpringBootUtil.applicationContext = applicationContext; + } + } + + /** + * 通过name获取 Bean + * @param name 容器名 + * @return Object + */ + public static Object getBean(String name) { + return getApplicationContext().getBean(name); + } + + /** + * 通过class获取Bean + * @param clazz 类 + * @return 任意的容器 + * @param 泛型 + */ + public static T getBean(Class clazz) { + return getApplicationContext().getBean(clazz); + } + + /** + * 通过name,以及Clazz返回指定的Bean + * @param name 容器名 + * @param clazz 容器类 + * @return 任意容器(组件) + * @param 泛型 + */ + public static T getBean(String name, Class clazz) { + return getApplicationContext().getBean(name, clazz); + } +} + + diff --git a/src/main/java/com/book/backend/utils/SqlUtils.java b/src/main/java/com/book/backend/utils/SqlUtils.java new file mode 100644 index 0000000..1b7c3e2 --- /dev/null +++ b/src/main/java/com/book/backend/utils/SqlUtils.java @@ -0,0 +1,23 @@ +package com.book.backend.utils; + +import org.apache.commons.lang3.StringUtils; + +/** + * SQL 工具 + * + */ +public class SqlUtils { + + /** + * 校验排序字段是否合法(防止 SQL 注入) + * + * @param sortField + * @return + */ + public static boolean validSortField(String sortField) { + if (StringUtils.isBlank(sortField)) { + return false; + } + return !StringUtils.containsAny(sortField, "=", "(", ")", " "); + } +} diff --git a/src/main/java/com/book/backend/utils/Utility.java b/src/main/java/com/book/backend/utils/Utility.java new file mode 100644 index 0000000..b32f32f --- /dev/null +++ b/src/main/java/com/book/backend/utils/Utility.java @@ -0,0 +1,191 @@ +package com.book.backend.utils; + + +/** + * 工具类的作用: + * 处理各种情况的用户输入,并且能够按照程序员的需求,得到用户的控制台输入。 + */ + +import java.util.Scanner; + + +/** + * @author 程序员小白条 + */ +public class Utility { + /** + * 静态属性 + * 控制台 + * 静态常量 + */ + private static final Scanner SCANNER = new Scanner(System.in); + + + /** + * 功能:读取键盘输入的一个菜单选项,值:1——5的范围 + * @return 1——5 + */ + public static char readMenuSelection() { + char c; + for (; ; ) { + //包含一个字符的字符串 + String str = readKeyBoard(1, false); + //将字符串转换成字符char类型 + c = str.charAt(0); + if (c != '1' && c != '2' && + c != '3' && c != '4' && c != '5') { + System.out.print("选择错误,请重新输入:"); + } else { + break; + } ; + } + return c; + } + + /** + * 功能:读取键盘输入的一个字符 + * @return 一个字符 + */ + public static char readChar() { + //就是一个字符 + String str = readKeyBoard(1, false); + return str.charAt(0); + } + + /** + * 功能:读取键盘输入的一个字符,如果直接按回车,则返回指定的默认值;否则返回输入的那个字符 + * @param defaultValue 指定的默认值 + * @return 默认值或输入的字符 + */ + + public static char readChar(char defaultValue) { + //要么是空字符串,要么是一个字符 + String str = readKeyBoard(1, true); + return (str.length() == 0) ? defaultValue : str.charAt(0); + } + + /** + * 功能:读取键盘输入的整型,长度小于2位 + * @return 整数 + */ + public static int readInt() { + int n; + for (; ; ) { + + String str = readKeyBoard(11, false); + try { + //将字符串转换成整数 + n = Integer.parseInt(str); + break; + } catch (NumberFormatException e) { + System.out.print("数字输入错误,请重新输入:"); + } + } + return n; + } + + /** + * 功能:读取键盘输入的 整数或默认值,如果直接回车,则返回默认值,否则返回输入的整数 + * @param defaultValue 指定的默认值 + * @return 整数或默认值 + */ + public static int readInt(int defaultValue) { + int n; + for (; ; ) { + String str = readKeyBoard(10, true); + if (str.equals("")) { + return defaultValue; + } + + //异常处理... + try { + n = Integer.parseInt(str); + break; + } catch (NumberFormatException e) { + System.out.print("数字输入错误,请重新输入:"); + } + } + return n; + } + + /** + * 功能:读取键盘输入的指定长度的字符串 + * @param limit 限制的长度 + * @return 指定长度的字符串 + */ + + public static String readString(int limit) { + return readKeyBoard(limit, false); + } + + /** + * 功能:读取键盘输入的指定长度的字符串或默认值,如果直接回车,返回默认值,否则返回字符串 + * @param limit 限制的长度 + * @param defaultValue 指定的默认值 + * @return 指定长度的字符串 + */ + + public static String readString(int limit, String defaultValue) { + String str = readKeyBoard(limit, true); + return str.equals("") ? defaultValue : str; + } + + + /** + * 功能:读取键盘输入的确认选项,Y或N + * 将小的功能,封装到一个方法中. + * @return Y或N + */ + public static char readConfirmSelection() { + System.out.print("确认是否预订(Y/N): "); + char c; + for (; ; ) {//无限循环 + //在这里,将接受到字符,转成了大写字母 + //y => Y n=>N + String str = readKeyBoard(1, false).toUpperCase(); + c = str.charAt(0); + if (c == 'Y' || c == 'N') { + break; + } else { + System.out.print("选择错误,请重新输入:"); + } + } + return c; + } + + /** + * 功能: 读取一个字符串 + * @param limit 读取的长度 + * @param blankReturn 如果为true ,表示 可以读空字符串。 + * 如果为false表示 不能读空字符串。 + * + * 如果输入为空,或者输入大于limit的长度,就会提示重新输入。 + * @return + */ + private static String readKeyBoard(int limit, boolean blankReturn) { + + //定义了字符串 + String line = ""; + + //scanner.hasNextLine() 判断有没有下一行 + while (SCANNER.hasNextLine()) { + line = SCANNER.nextLine();//读取这一行 + + //如果line.length=0, 即用户没有输入任何内容,直接回车 + if (line.length() == 0) { + if (blankReturn) return line;//如果blankReturn=true,可以返回空串 + else continue; //如果blankReturn=false,不接受空串,必须输入内容 + } + + //如果用户输入的内容大于了 limit,就提示重写输入 + //如果用户如的内容 >0 <= limit ,我就接受 + if (line.length() < 1 || line.length() > limit) { + System.out.print("输入长度(不能大于" + limit + ")错误,请重新输入:"); + continue; + } + break; + } + + return line; + } +} diff --git a/src/main/java/com/book/backend/ws/WebSocket.java b/src/main/java/com/book/backend/ws/WebSocket.java new file mode 100644 index 0000000..514c461 --- /dev/null +++ b/src/main/java/com/book/backend/ws/WebSocket.java @@ -0,0 +1,388 @@ +package com.book.backend.ws; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.book.backend.config.HttpSessionConfigurator; +import com.book.backend.pojo.Chat; +import com.book.backend.pojo.Users; +import com.book.backend.pojo.dto.chat.MessageRequest; +import com.book.backend.pojo.vo.ChatVo; +import com.book.backend.service.ChatService; +import com.book.backend.service.UsersService; +import com.google.gson.Gson; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import javax.annotation.Resource; +import javax.servlet.http.HttpSession; +import javax.websocket.*; +import javax.websocket.server.PathParam; +import javax.websocket.server.ServerEndpoint; +import java.io.IOException; +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; + +import static com.book.backend.constant.ChatConstant.HALL_CHAT; +import static com.book.backend.constant.ChatConstant.PRIVATE_CHAT; + + +/** + * @author qimu + */ +@Component +@Slf4j +@ServerEndpoint(value = "/websocket/{userId}", configurator = HttpSessionConfigurator.class) +public class WebSocket { + /** + * 保存队伍的连接信息 + */ +// private static final Map> ROOMS = new HashMap<>(); + /** + * 线程安全的无序的集合 + */ + private static final CopyOnWriteArraySet SESSIONS = new CopyOnWriteArraySet<>(); + /** + * 存储在线连接数 + */ + private static final Map SESSION_POOL = new HashMap<>(0); + private static UsersService usersService; + private static ChatService chatService; + /** + * 房间在线人数 + */ + private static int onlineCount = 0; + /** + * 当前信息 + */ + private Session session; + private HttpSession httpSession; + + public static synchronized int getOnlineCount() { + return onlineCount; + } + + public static synchronized void addOnlineCount() { + WebSocket.onlineCount++; + } + + public static synchronized void subOnlineCount() { + WebSocket.onlineCount--; + } + + @Resource + public void setHeatMapService(UsersService usersService) { + WebSocket.usersService = usersService; + } + @Resource + public void setHeatMapService(ChatService chatService) { + WebSocket.chatService = chatService; + } + /** + * 队伍内群发消息 + * + * @param teamId + * @param msg + * @throws Exception + */ + public static void broadcast(String teamId, String msg) { +// ConcurrentHashMap map = ROOMS.get(teamId); + // keySet获取map集合key的集合 然后在遍历key即可 +// for (String key : map.keySet()) { +// try { +// WebSocket webSocket = map.get(key); +// webSocket.sendMessage(msg); +// } catch (Exception e) { +// e.printStackTrace(); +// } +// } + } + + /** + * 发送消息 + * + * @param message + * @throws IOException + */ + public void sendMessage(String message) throws IOException { + this.session.getBasicRemote().sendText(message); + } + + @OnOpen + public void onOpen(Session session, @PathParam(value = "userId") String userId, EndpointConfig config) { + try { + if (StringUtils.isBlank(userId) || "undefined".equals(userId)) { + sendError(userId, "参数有误"); + return; + } + HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName()); + +// if (!"NaN".equals(teamId)) { +// if (!ROOMS.containsKey(teamId)) { +// ConcurrentHashMap room = new ConcurrentHashMap<>(0); +// room.put(userId, this); +// ROOMS.put(String.valueOf(teamId), room); +// // 在线数加1 +// addOnlineCount(); +// } else { +// if (!ROOMS.get(teamId).containsKey(userId)) { +// ROOMS.get(teamId).put(userId, this); +// // 在线数加1 +// addOnlineCount(); +// } +// } +// log.info("有新连接加入!当前在线人数为" + getOnlineCount()); +// } else { + SESSIONS.add(session); + SESSION_POOL.put(userId, session); + log.info("有新用户加入,userId={}, 当前在线人数为:{}", userId, SESSION_POOL.size()); +// sendAllUsers(); +// } + } catch (Exception e) { + e.printStackTrace(); + } + } + + @OnClose + public void onClose(@PathParam("userId") String userId, Session session) { + try { + + if (!SESSION_POOL.isEmpty()) { + SESSION_POOL.remove(userId); + SESSIONS.remove(session); + } + log.info("【WebSocket消息】连接断开,总数为:" + SESSION_POOL.size()); +// sendAllUsers(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @OnMessage + public void onMessage(String message,@PathParam("userId") String userId) { + + if ("PING".equals(message)) { + sendOneMessage(userId, "pong"); + log.error("心跳包,发送给={},在线:{}人", userId, getOnlineCount()); + return; + } + log.info("服务端收到用户username={}的消息:{}", userId, message); + MessageRequest messageRequest = new Gson().fromJson(message, MessageRequest.class); + Long toId = messageRequest.getToId(); + String text = messageRequest.getText(); + Integer chatType = messageRequest.getChatType(); + Users user = usersService.getById(userId); + if(chatType== PRIVATE_CHAT){ + privateChat(user,toId,text,chatType); + }else if(chatType == HALL_CHAT){ + log.info("群聊功能"); + } +// User fromUser = userService.getById(userId); +// Team team = teamService.getById(teamId); +// if (chatType == PRIVATE_CHAT) { +// // 私聊 +// privateChat(fromUser, toId, text, chatType); +// } else if (chatType == TEAM_CHAT) { +// // 队伍内聊天 +// teamChat(fromUser, text, team, chatType); +// } else { +// // 群聊 +// hallChat(fromUser, text, chatType); +// } + } + + /** + * 队伍聊天 + * + * @param user + * @param text + * @param team + * @param chatType + */ +// private void teamChat(User user, String text, Team team, Integer chatType) { +// MessageVo messageVo = new MessageVo(); +// WebSocketVo fromWebSocketVo = new WebSocketVo(); +// BeanUtils.copyProperties(user, fromWebSocketVo); +// messageVo.setFormUser(fromWebSocketVo); +// messageVo.setText(text); +// messageVo.setTeamId(team.getId()); +// messageVo.setChatType(chatType); +// messageVo.setCreateTime(DateUtil.format(new Date(), "yyyy年MM月dd日 HH:mm:ss")); +// if (user.getId() == team.getUserId() || user.getUserRole() == ADMIN_ROLE) { +// messageVo.setIsAdmin(true); +// } +// User loginUser = (User) this.httpSession.getAttribute(LOGIN_USER_STATUS); +// if (loginUser.getId() == user.getId()) { +// messageVo.setIsMy(true); +// } +// String toJson = new Gson().toJson(messageVo); +// try { +// broadcast(String.valueOf(team.getId()), toJson); +// savaChat(user.getId(), null, text, team.getId(), chatType); +// chatService.deleteKey(CACHE_CHAT_TEAM, String.valueOf(team.getId())); +// log.error("队伍聊天,发送给={},队伍={},在线:{}人", user.getId(), team.getId(), getOnlineCount()); +// } catch (Exception e) { +// throw new RuntimeException(e); +// } +// } + + /** + * 大厅聊天 + * + * @param user + * @param text + */ +// private void hallChat(User user, String text, Integer chatType) { +// MessageVo messageVo = new MessageVo(); +// WebSocketVo fromWebSocketVo = new WebSocketVo(); +// BeanUtils.copyProperties(user, fromWebSocketVo); +// messageVo.setFormUser(fromWebSocketVo); +// messageVo.setText(text); +// messageVo.setChatType(chatType); +// messageVo.setCreateTime(DateUtil.format(new Date(), "yyyy年MM月dd日 HH:mm:ss")); +// if (user.getUserRole() == ADMIN_ROLE) { +// messageVo.setIsAdmin(true); +// } +// User loginUser = (User) this.httpSession.getAttribute(LOGIN_USER_STATUS); +// if (loginUser.getId() == user.getId()) { +// messageVo.setIsMy(true); +// } +// String toJson = new Gson().toJson(messageVo); +// sendAllMessage(toJson); +// savaChat(user.getId(), null, text, null, chatType); +// chatService.deleteKey(CACHE_CHAT_HALL, String.valueOf(user.getId())); +// } + + /** + * 私人聊天 + * + * @param user 使用者 + * @param toId 至id + * @param text 文本 + * @param chatType 聊天类型 + */ + private void privateChat(Users user, Long toId, String text, Integer chatType) { + Session toSession = SESSION_POOL.get(toId.toString()); + if (toSession != null) { +// MessageVo messageVo = chatService.chatResult(user.getId(), toId, text, chatType, DateUtil.date(System.currentTimeMillis())); +// Users loginUser = (Users) this.httpSession.getAttribute(LOGIN_USER_STATUS); +// if (loginUser.getId() == user.getId()) { +// messageVo.setIsMy(true); +// } +// String toJson = new Gson().toJson(messageVo); + sendOneMessage(toId.toString(), text); +// log.info("发送给用户username={},消息:{}", messageVo.getToUser(), toJson); + } else { + log.info("用户不在线username={}的session", toId); + } + savaChat(user.getUserId(), toId, text, null, chatType); +// chatService.deleteKey(CACHE_CHAT_PRIVATE, user.getId() + "" + toId); +// chatService.deleteKey(CACHE_CHAT_PRIVATE, toId + "" + user.getId()); + } + + /** + * 保存聊天 + * + * @param userId 用户id + * @param toId 至id + * @param text 文本 + * @param teamId 团队id + * @param chatType 聊天类型 + */ + private void savaChat(Long userId, Long toId, String text, Long teamId, Integer chatType) { + if (chatType == PRIVATE_CHAT) { + Users user = usersService.getById(userId); +// Set userIds = stringJsonListToLongSet(user.getUserIds()); +// if (!userIds.contains(toId)) { +// sendError(String.valueOf(userId), "该用户不是你的好友"); +// return; +// } + } + Chat chat = new Chat(); + chat.setFromId(userId); + chat.setText(String.valueOf(text)); + chat.setChatType(chatType); + if (toId != null && toId > 0) { + chat.setToId(toId); + } + chatService.save(chat); + } + + /** + * 发送失败 + * + * @param userId 用户id + * @param errorMessage 错误消息 + */ + private void sendError(String userId, String errorMessage) { + JSONObject obj = new JSONObject(); + obj.set("error", errorMessage); + sendOneMessage(userId, obj.toString()); + } + + /** + * 此为广播消息 + * + * @param message 消息 + */ + public void sendAllMessage(String message) { + log.info("【WebSocket消息】广播消息:" + message); + for (Session session : SESSIONS) { + try { + if (session.isOpen()) { + synchronized (session) { + session.getBasicRemote().sendText(message); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + + /** + * 此为单点消息 + * + * @param userId 用户编号 + * @param message 消息 + */ + public void sendOneMessage(String userId, String message) { + Session session = SESSION_POOL.get(userId); + if (session != null && session.isOpen()) { + try { + synchronized (session) { + log.info("【WebSocket消息】单点消息:" + message); + session.getAsyncRemote().sendText(message); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + /** + * 发送所有在线用户信息 + */ +// public void sendAllUsers() { +// log.info("【WebSocket消息】发送所有在线用户信息"); +// HashMap> stringListHashMap = new HashMap<>(0); +// List webSocketVos = new ArrayList<>(); +// stringListHashMap.put("users", webSocketVos); +// for (Serializable key : SESSION_POOL.keySet()) { +// User user = userService.getById(key); +// WebSocketVo webSocketVo = new WebSocketVo(); +// BeanUtils.copyProperties(user, webSocketVo); +// webSocketVos.add(webSocketVo); +// } +// sendAllMessage(JSONUtil.toJsonStr(stringListHashMap)); +// } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..7122a77 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,35 @@ +# application-dev.yml 开发配置 热更新(数据库、端口、JWT) +spring: + datasource: + # 配置数据源类型 + type: com.zaxxer.hikari.HikariDataSource + # 配置连接数据库信息 + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://110.40.62.21:3306/book?characterEncoding=utf8&useSSL=false + username: book + password: 123456 +# todo 开启 redis 取消注释即可 +# redis: +# host: 106.14.33.49 +# port: 6379 +# password: 663057 +## database: 0 +# lettuce: +# pool: +# max-active: 10 +# max-idle: 10 +# min-idle: 1 +# time-between-eviction-runs: 10s + +server: + port: 8889 #配置后端接口的端口号 + servlet: #配置后端请求的根路径 http://127.0.0.1:8889/api + context-path: "/api" +#jwt config +jwt: + tokenHeader: Authorization #JWT存储的请求头 + secret: mall-jwt-test #jwt加解密使用的密钥 + expiration: 36000000 #JWT的超时时间 + tokenHead: Bearer #JWT负载中拿到的开头 + + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..872f642 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,43 @@ +spring: + # 配置数据源信息 + application: + #应用的名称 + name: BookManageSystem + profiles: + active: 'dev' # 指定开发配置文件 + # 指定返回给前端的时区和时间的格式化模式 + jackson: + time-zone: GMT+8 + date-format: yyyy-MM-dd HH:mm:ss +# 配置MyBatisPlus日志 +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + #在映射实体类或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射 + map-underscore-to-camel-case: true + global-config: + db-config: + # 配置MyBatis-Plus操作表的默认前缀 + table-prefix: t_ + # 配置MyBatis-Plus的主键策略 + # id-type: assign_id + # 配置实体类的扫描包路径 注入容器 + type-aliases-package: com.book.backend.pojo + +# knife4j接口文档配置 +knife4j: + enable: true + openapi: + title: "图书管理系统后台接口文档" + description: "vue_book_backend" + # aaa" + version: "1.0" + license: Apache 2.0 + license-url: https://stackoverflow.com/ + terms-of-service-url: https://www.xiaobaitiao.top + group: + test1: + group-name: default + api-rule: package + api-rule-resources: + - com.book.backend.controller diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 0000000..547c5ba --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,5 @@ +by 程序员小白条 +GitEE: https://gitee.com/falle22222n-leaves +GitHub: https://github.com/luoye6 +个人博客: https://luoye6.github.io/ +Knife4j后端接口在线调试地址: http://localhost:8889/api/doc.html diff --git a/src/main/resources/mapper/AdminsMapper.xml b/src/main/resources/mapper/AdminsMapper.xml new file mode 100644 index 0000000..61d7bb7 --- /dev/null +++ b/src/main/resources/mapper/AdminsMapper.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + admin_id,username,password, + admin_name,status,create_time, + update_time + + diff --git a/src/main/resources/mapper/AiIntelligentMapper.xml b/src/main/resources/mapper/AiIntelligentMapper.xml new file mode 100644 index 0000000..401c085 --- /dev/null +++ b/src/main/resources/mapper/AiIntelligentMapper.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + id,input_message,is_user, + user_id,createTime,updateTime + + diff --git a/src/main/resources/mapper/BookAdminsMapper.xml b/src/main/resources/mapper/BookAdminsMapper.xml new file mode 100644 index 0000000..fad872e --- /dev/null +++ b/src/main/resources/mapper/BookAdminsMapper.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + book_admin_id,username,password, + book_admin_name,status,email, + create_time,update_time + + diff --git a/src/main/resources/mapper/BookRuleMapper.xml b/src/main/resources/mapper/BookRuleMapper.xml new file mode 100644 index 0000000..b957f94 --- /dev/null +++ b/src/main/resources/mapper/BookRuleMapper.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + rule_id,book_rule_id,book_days, + book_limit_number,book_limit_library,book_overdue_fee, + create_time,update_time + + diff --git a/src/main/resources/mapper/BookTypeMapper.xml b/src/main/resources/mapper/BookTypeMapper.xml new file mode 100644 index 0000000..6b1ae59 --- /dev/null +++ b/src/main/resources/mapper/BookTypeMapper.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + type_id,type_name,type_content, + create_time,update_time + + diff --git a/src/main/resources/mapper/BooksBorrowMapper.xml b/src/main/resources/mapper/BooksBorrowMapper.xml new file mode 100644 index 0000000..cb61150 --- /dev/null +++ b/src/main/resources/mapper/BooksBorrowMapper.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + borrow_id,card_number,book_number, + borrow_date,close_date,return_date, + create_time,update_time + + diff --git a/src/main/resources/mapper/BooksMapper.xml b/src/main/resources/mapper/BooksMapper.xml new file mode 100644 index 0000000..9b74a77 --- /dev/null +++ b/src/main/resources/mapper/BooksMapper.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + book_id,book_number,book_name, + book_author,book_library,book_type, + book_location,book_status,book_description, + create_time,update_time + + diff --git a/src/main/resources/mapper/ChartMapper.xml b/src/main/resources/mapper/ChartMapper.xml new file mode 100644 index 0000000..a47dbec --- /dev/null +++ b/src/main/resources/mapper/ChartMapper.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + id,name,goal, + chartData,chartType,genChart, + genResult,status,execMessage, + userId,createTime,updateTime, + isDelete + + diff --git a/src/main/resources/mapper/ChatMapper.xml b/src/main/resources/mapper/ChatMapper.xml new file mode 100644 index 0000000..c0ac2bf --- /dev/null +++ b/src/main/resources/mapper/ChatMapper.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + id,from_id,to_id, + text,chat_type,create_time, + update_time + + diff --git a/src/main/resources/mapper/CommentMapper.xml b/src/main/resources/mapper/CommentMapper.xml new file mode 100644 index 0000000..f477410 --- /dev/null +++ b/src/main/resources/mapper/CommentMapper.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + comment_id,comment_avatar,comment_barrage_style, + comment_message,comment_time,create_time, + update_time + + diff --git a/src/main/resources/mapper/NoticeMapper.xml b/src/main/resources/mapper/NoticeMapper.xml new file mode 100644 index 0000000..7a70a9a --- /dev/null +++ b/src/main/resources/mapper/NoticeMapper.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + notice_id,notice_title,notice_content, + notice_admin_id,create_time,update_time + + diff --git a/src/main/resources/mapper/UserInterfaceInfoMapper.xml b/src/main/resources/mapper/UserInterfaceInfoMapper.xml new file mode 100644 index 0000000..2299f62 --- /dev/null +++ b/src/main/resources/mapper/UserInterfaceInfoMapper.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + id,user_id,interface_id, + total_num,left_num,create_time, + update_time + + diff --git a/src/main/resources/mapper/UsersMapper.xml b/src/main/resources/mapper/UsersMapper.xml new file mode 100644 index 0000000..988810d --- /dev/null +++ b/src/main/resources/mapper/UsersMapper.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + user_id,username,password, + card_name,card_number,rule_number, + status,create_time,update_time + + diff --git a/src/main/resources/mapper/ViolationMapper.xml b/src/main/resources/mapper/ViolationMapper.xml new file mode 100644 index 0000000..5b09444 --- /dev/null +++ b/src/main/resources/mapper/ViolationMapper.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + violation_id,card_number,book_number, + borrow_date,close_date,return_date, + violation_message,violation_admin_id,create_time, + update_time + + diff --git a/src/main/resources/test_excel.xlsx b/src/main/resources/test_excel.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..88a8fce7d9d73a6842e33478cd47596a4a9e2da7 GIT binary patch literal 9467 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 w4*-C@0s#IYyT8N#9uxlxj|VlK{ty0VgjA4$0小白条 + * @from 个人博客 + */ +@SpringBootTest +public class AITest { + + @Resource + private BooksService booksService; + @Resource + private AiIntelligentService aiIntelligentService; + @Resource + private AdminsService adminsService; + @Resource + private UserInterfaceInfoService userInterfaceInfoService; + @Resource + private GuavaRateLimiterManager guavaRateLimiterManager; + @Resource + private BooksBorrowService booksBorrowService; + @Resource + private ChartService chartService; + public static void main(String[] args) { + new AITest().test2(); + } + public void test1(){ + // 调用自己的密钥 + String accessKey = "xxxxx"; + String secretKey = "xxxxxx"; + YuCongMingClient client = new YuCongMingClient(accessKey, secretKey); + // 创建开发请求,设置模型id和消息内容 + DevChatRequest devChatRequest = new DevChatRequest(); + devChatRequest.setModelId(1694279925663539202L); + devChatRequest.setMessage("请问Java怎么能够爬取网站的内容,请给我实例"); + // 发送请求给AI,进行对话 + BaseResponse response = client.doChat(devChatRequest); + // 得到消息 + System.out.println(response.getData()); + } + @Test + public void test2(){ + // 调用自己的密钥 + String accessKey = "xxxx"; + String secretKey = "xxxxxx"; + YuCongMingClient client = new YuCongMingClient(accessKey, secretKey); + // 创建开发请求,设置模型id和消息内容 + DevChatRequest devChatRequest = new DevChatRequest(); + devChatRequest.setModelId(1694279925663539202L); + + List list = booksService.list(); + StringBuilder stringBuilder = new StringBuilder(); + HashSet hashSet = new HashSet<>(); + String presetInformation = "请根据数据库内容和游客信息作出推荐,书籍必须是数据库里面有的,可以是一本也可以是多本,根据游客喜欢的信息作出推荐。"; + + stringBuilder.append(presetInformation).append("\n").append("数据库内容: "); + for (Books books : list) { + if(!hashSet.contains(books.getBookName())){ + hashSet.add(books.getBookName()); + stringBuilder.append(books.getBookName()).append(","); + } + } + stringBuilder.append("\n"); + String customerInformation = "我喜欢关于动物的书籍,请给我推荐图书"; + stringBuilder.append("游客信息: ").append(customerInformation).append("\n"); +// list.forEach(System.out::println); +// System.out.println(stringBuilder.toString()); + + devChatRequest.setMessage(stringBuilder.toString()); + // 发送请求给AI,进行对话 + BaseResponse response = client.doChat(devChatRequest); + // 得到消息 + System.out.println(response.getData()); + } + @Test + public void test3(){ + // POST 请求 URL + String url = "https://api.deepai.org/make_me_a_pizza"; + + // 构建请求参数 + String chatStyle = "chat"; + String chatHistory = "[{\"role\":\"user\",\"content\":\"你好\"}]"; + + // 使用 HuTool 发送 POST 请求并携带参数 + HttpResponse response = HttpRequest.post(url) + .form("chat_style", chatStyle) + .form("chatHistory", chatHistory) + .execute(); + + // 获取响应结果 + String result = response.body(); + System.out.println(result); + + } + @Test + public void test4(){ + AiIntelligent aiIntelligent = new AiIntelligent(); + aiIntelligent.setInputMessage("我喜欢神话类的书籍,请给我推荐"); + aiIntelligent.setUserId(1923L); + boolean save = aiIntelligentService.save(aiIntelligent); + if(save){ + System.out.println("插入成功"); + } + } + @Test + public void test5(){ + Long userId = 1923L; + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq(userId!=null&&userId>0,"user_id",userId); + queryWrapper.orderByDesc("create_time"); + queryWrapper.last("LIMIT 5"); + List list = aiIntelligentService.list(queryWrapper); + list.forEach(System.out::println); + } + @Test + public void test6() throws InterruptedException { + String userId = "1923"; + // 每秒允许的最大访问速率为1个许可 + RateLimiter rateLimiter = RateLimiter.create(4); + for (int i = 0; i < 10; i++) { + if (rateLimiter.tryAcquire()) { + // 可以进行处理的代码,表示限流允许通过 + System.out.println(("用户id: " + userId + "请求一个令牌成功")); + } else { + // 限流超过了速率限制的处理代码 + System.out.println(("用户id: " + userId + "请求一个令牌失败")); + } + Thread.sleep(100); + } + + } + @Test + public void test7(){ + ExecutorService executor = Executors.newSingleThreadExecutor(); + int timeout = 30; // 超时时间,单位为秒 + Future future = executor.submit(() -> { + // 在这里执行你的任务 + // 调用自己的密钥 + String accessKey = "xxxxxx"; + String secretKey = "xxxxx"; + YuCongMingClient client = new YuCongMingClient(accessKey, secretKey); + // 创建开发请求,设置模型id和消息内容 + DevChatRequest devChatRequest = new DevChatRequest(); + devChatRequest.setModelId(1694279925663539202L); + + List list = booksService.list(); + StringBuilder stringBuilder = new StringBuilder(); + HashSet hashSet = new HashSet<>(); + String presetInformation = "请根据数据库内容和游客信息作出推荐,书籍必须是数据库里面有的,可以是一本也可以是多本,根据游客喜欢的信息作出推荐。"; + + stringBuilder.append(presetInformation).append("\n").append("数据库内容: "); + for (Books books : list) { + if(!hashSet.contains(books.getBookName())){ + hashSet.add(books.getBookName()); + stringBuilder.append(books.getBookName()).append(","); + } + } + stringBuilder.append("\n"); + String customerInformation = "我喜欢编程类的书籍,请给我推荐图书"; + stringBuilder.append("游客信息: ").append(customerInformation).append("\n"); +// list.forEach(System.out::println); +// System.out.println(stringBuilder.toString()); + + devChatRequest.setMessage(stringBuilder.toString()); + // 发送请求给AI,进行对话 + BaseResponse response = client.doChat(devChatRequest); + // 得到消息 + System.out.println(response.getData()); + return "任务执行结果"; + }); + + String result; + try { + result = future.get(timeout, TimeUnit.SECONDS); + } catch (TimeoutException e) { + // 超时处理,返回错误信息 + result = "请求超时,请稍后再试"; + } catch (Exception e) { + // 其他异常处理 + result = "请求异常:" + e.getMessage(); + } + System.out.println(result); +// 关闭ExecutorService + executor.shutdown(); + } + @Test + public void test8(){ + String name = "测试图表1"; + String goal = "我想要分析用户增长趋势"; + String chartType = "折线图"; + // 这里把逻辑写死,AI功能现在测试阶段 + Admins admin = adminsService.getById(1623L); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + // 功能做限制,用户只能调用AI接口聊天,管理员只能调用图表生成,这边用管理员ID + queryWrapper.eq(UserInterfaceInfo::getUserId,admin.getAdminId()); + UserInterfaceInfo interfaceInfo = userInterfaceInfoService.getOne(queryWrapper); + if(interfaceInfo == null){ + System.out.println("该接口已废弃"); + } + Integer leftNum = interfaceInfo.getLeftNum(); + Integer totalNum = interfaceInfo.getTotalNum(); + if(leftNum<=0){ + System.out.println("AI接口次数不足,请明天再来"); + } + // 限流判断,每个管理员一个限流器 + boolean limit = guavaRateLimiterManager.doRateLimit(admin.getAdminId()); + if(!limit){ + System.out.println("请求次数过多,请稍后重试"); + } + // 用户输入 + final String prompt = "你是一个数据分析师和前端开发专家,接下来我会按照以下固定格式给你提供内容:\n" + + "分析需求:\n" + + "{数据分析的需求或者目标}\n" + + "原始数据:\n" + + "{csv格式的原始数据,用,作为分隔符}\n" + + "请根据这两部分内容,按照以下指定格式生成内容(此外不要输出任何多余的开头、结尾、注释)\n" + + "【【【【【\n" + + "{前端 Echarts V5 的 option 配置对象js代码,合理地将数据进行可视化,不要生成任何多余的内容,比如注释}\n" + + "【【【【【\n" + + "{明确的数据分析结论、越详细越好,不要生成多余的注释}"+"\n"; + // 构造用户输入 + StringBuilder userInput = new StringBuilder(); + userInput.append(prompt); + userInput.append("你是一个数据分析师,接下来我会给你我的分析目标和原始数据,请告诉我分析结论。").append("\n"); + // 拼接分析目标 + String userGoal = goal; + if (StringUtils.isNotBlank(chartType)) { + userGoal += ",请使用" + chartType; + } + userInput.append("分析目标:").append(userGoal).append("\n"); + userInput.append("分析需求:"); + String csvData = + "分析网站用户的增长情况\n" + + "原始数据:\n" + + "日期,用户数\n" + + "1号,10\n" + + "2号,20\n" + + "3号,30"; + userInput.append(csvData).append("\n"); + String result; + SparkAIManager sparkAIManager = new SparkAIManager(1923 + "", false); + try { + result = sparkAIManager.sendMessageAndGetResponse(userInput.toString(),20); + } catch (Exception e) { + throw new RuntimeException(e); + } + String[] splits = result.split("【【【【【"); + if (splits.length < 3) { + System.out.println("AI生成错误,请稍后重试"); +// throw new BusinessException(ErrorCode.SYSTEM_ERROR, "AI 生成错误"); + } + + Pattern pattern = Pattern.compile("option = ([^;]+);"); + Matcher matcher = pattern.matcher(splits[1]); + String genChart = ""; + if (matcher.find()) { + genChart = matcher.group(1); + }else{ + System.out.println("AI生成错误,请稍后重试"); + } + String genResult = splits[2].split("}")[1].trim(); + // 插入到数据库 + Chart chart = new Chart(); + chart.setName(name); + chart.setGoal(goal); + chart.setChartData(csvData); + chart.setChartType(chartType); + chart.setGenChart(genChart.trim()); + chart.setGenResult(genResult); + chart.setAdminId(1923L); + boolean saveResult = chartService.save(chart); + ThrowUtils.throwIf(!saveResult, ErrorCode.SYSTEM_ERROR, "图表保存失败"); + // 更新调用接口的次数 剩余接口调用次数-1.总共调用次数+1 + interfaceInfo.setLeftNum(leftNum-1); + interfaceInfo.setTotalNum(totalNum+1); + boolean update = userInterfaceInfoService.updateById(interfaceInfo); + if(!update){ + System.out.println("调用接口信息失败"); + } + BiResponse biResponse = new BiResponse(); + biResponse.setGenChart(genChart); + biResponse.setGenResult(genResult); + biResponse.setChartId(chart.getId()); + System.out.println("图表生成成功"); + } + @Test + public void test9(){ + List list = booksBorrowService.list(); + list.stream().map(BooksBorrow::getBorrowDate).forEach(System.out::println); + final String prompt = "你是一个数据分析师和前端开发专家,接下来我会按照以下固定格式给你提供内容:\n" + + "分析需求:\n" + + "{数据分析的需求或者目标}\n" + + "原始数据:\n" + + "{csv格式的原始数据,用,作为分隔符}\n" + + "请根据这两部分内容,按照以下指定格式生成内容(此外不要输出任何多余的开头、结尾、注释)\n" + + "【【【【【\n" + + "{前端 Echarts V5 的 option 配置对象js代码,合理地将数据进行可视化,不要生成任何多余的内容,比如注释}\n" + + "【【【【【\n" + + "{明确的数据分析结论、越详细越好,不要生成多余的注释}"+"\n"; + System.out.println(prompt); + } + @Test + public void test10(){ + // 查询无效的数据,并进行删除操作 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.isNull(AiIntelligent::getAiResult).or().eq(AiIntelligent::getAiResult,""); + List list = aiIntelligentService.list(queryWrapper); + // 先为用户恢复次数 + list.forEach(item -> { + Long userId = item.getUserId(); + UserInterfaceInfo user = userInterfaceInfoService.getById(userId); + synchronized (IncSyncDeleteAIMessage.class) { + // 用户恢复次数 + if (user != null) { + user.setLeftNum(user.getLeftNum() + 1); + boolean save = userInterfaceInfoService.save(user); + if (!save) { + throw new BusinessException(ErrorCode.OPERATION_ERROR, "操作定时任务失败"); + } + } + } + }); +// // 将无效的记录删除 + boolean remove = aiIntelligentService.remove(queryWrapper); + if(!remove){ + throw new BusinessException(ErrorCode.OPERATION_ERROR, "操作定时任务失败"); + } + } +} diff --git a/src/test/java/com/book/backend/AliAITest.java b/src/test/java/com/book/backend/AliAITest.java new file mode 100644 index 0000000..ca83d26 --- /dev/null +++ b/src/test/java/com/book/backend/AliAITest.java @@ -0,0 +1,90 @@ +package com.book.backend; + +import cn.hutool.core.date.StopWatch; +import com.aliyun.broadscope.bailian.sdk.AccessTokenClient; +import com.aliyun.broadscope.bailian.sdk.ApplicationClient; +import com.aliyun.broadscope.bailian.sdk.models.BaiLianConfig; +import com.aliyun.broadscope.bailian.sdk.models.CompletionsRequest; +import com.aliyun.broadscope.bailian.sdk.models.CompletionsResponse; +import com.aliyun.broadscope.bailian.sdk.utils.UUIDGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * @author 小白条 + * @from 个人博客 + */ +@SpringBootTest +public class AliAITest { + public static void main(String[] args) { + + } + + // 单次对话 + @Test + public void test1() { + String accessKeyId = "xxxx"; + String accessKeySecret = "xxxxx"; + String agentKey = "xxxxx"; + + AccessTokenClient accessTokenClient = new AccessTokenClient(accessKeyId, accessKeySecret, agentKey); + String token = accessTokenClient.getToken(); + + BaiLianConfig config = new BaiLianConfig() + .setApiKey(token); + + String appId = "xxxxxx"; + String prompt = "我想要Vue3的登录页面"; + CompletionsRequest request = new CompletionsRequest() + .setAppId(appId) + .setPrompt(prompt); + + ApplicationClient client = new ApplicationClient(config); + CompletionsResponse response = client.completions(request); + System.out.println(response); + } + + // 多次会话有记忆功能 + @Test + public void test2() { + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + String accessKeyId = "xxxxx"; + String accessKeySecret = "xxxx"; + String agentKey = "xxxx"; + + AccessTokenClient accessTokenClient = new AccessTokenClient(accessKeyId, accessKeySecret, agentKey); + String token = accessTokenClient.getToken(); + + BaiLianConfig config = new BaiLianConfig() + .setApiKey(token); + + String appId = "xxxx"; + String prompt = "我想要Vue3的登录页面"; + String sessionId = UUIDGenerator.generate(); + CompletionsRequest request = new CompletionsRequest() + .setAppId(appId) + .setPrompt(prompt) + .setSessionId(sessionId); + ApplicationClient client = new ApplicationClient(config); + CompletionsResponse response = client.completions(request); + System.out.println(response); + stopWatch.stop(); + System.out.println(stopWatch.getTotal(TimeUnit.SECONDS)); + + + +//阿里云百炼也支持调用侧自己维护上下文对话历史, 同时传入sessionId和history,会优先采用调用侧传入的上下文历史 +// List history = new ArrayList<>(); +// CompletionsRequest.ChatQaPair chatQaPair = new CompletionsRequest.ChatQaPair("我想去北京", "北京的天气很不错"); +// CompletionsRequest.ChatQaPair chatQaPair2 = new CompletionsRequest.ChatQaPair("北京有哪些景点", "北京有故宫、长城等"); +// history.add(chatQaPair); +// history.add(chatQaPair2); +// request.setHistory(history); + } +} diff --git a/src/test/java/com/book/backend/BenchmarkTest.java b/src/test/java/com/book/backend/BenchmarkTest.java new file mode 100644 index 0000000..bc24233 --- /dev/null +++ b/src/test/java/com/book/backend/BenchmarkTest.java @@ -0,0 +1,46 @@ +package com.book.backend; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.results.format.ResultFormatType; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@State(Scope.Thread) +@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(1) +@Threads(2) +public class BenchmarkTest { + @Benchmark + public long shift() { + long t = 455565655225562L; + long a = 0; + for (int i = 0; i < 1000; i++) { + a = t >> 30; + } + return a; + } + + @Benchmark + public long div() { + long t = 455565655225562L; + long a = 0; + for (int i = 0; i < 1000; i++) { + a = t / 1024 / 1024 / 1024; + } + return a; + } + + public static void main(String[] args) throws Exception { + Options opts = new OptionsBuilder() + .include(BenchmarkTest.class.getSimpleName()) + .resultFormat(ResultFormatType.JSON) + .build(); + new Runner(opts).run(); + } +} diff --git a/src/test/java/com/book/backend/BigModelNew.java b/src/test/java/com/book/backend/BigModelNew.java new file mode 100644 index 0000000..ff0b21a --- /dev/null +++ b/src/test/java/com/book/backend/BigModelNew.java @@ -0,0 +1,298 @@ +package com.book.backend; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.google.gson.Gson; +import okhttp3.*; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.*; + +public class BigModelNew extends WebSocketListener { + // 地址与鉴权信息 https://spark-api.xf-yun.com/v1.1/chat 1.5地址 domain参数为general + // 地址与鉴权信息 https://spark-api.xf-yun.com/v2.1/chat 2.0地址 domain参数为generalv2 + public static final String hostUrl = "https://spark-api.xf-yun.com/v2.1/chat"; + public static final String appid = "xxxxx"; + public static final String apiSecret = "xxxxx"; + public static final String apiKey = "xxxxxxx"; + + public static List historyList=new ArrayList<>(); // 对话历史存储集合 + + public static String totalAnswer=""; // 大模型的答案汇总 + + // 环境治理的重要性 环保 人口老龄化 我爱我的祖国 + public static String NewQuestion = ""; + + public static final Gson gson = new Gson(); + public static MyThread myThread; + // 个性化参数 + private String userId; + private Boolean wsCloseFlag; + + private static Boolean totalFlag=true; // 控制提示用户是否输入 + // 构造函数 + public BigModelNew(String userId, Boolean wsCloseFlag) { + this.userId = userId; + this.wsCloseFlag = wsCloseFlag; + } + + // 主函数 + public String sendMessageAndGetResponse(String message) throws Exception { + // 个性化参数入口,如果是并发使用,可以在这里模拟 + while (true){ + if(totalFlag){ + System.out.print("我:"); + totalFlag=false; + NewQuestion=message; + // 构建鉴权url + String authUrl = getAuthUrl(hostUrl, apiKey, apiSecret); + OkHttpClient client = new OkHttpClient.Builder().build(); + String url = authUrl.toString().replace("http://", "ws://").replace("https://", "wss://"); + Request request = new Request.Builder().url(url).build(); + for (int i = 0; i < 1; i++) { + totalAnswer=""; + WebSocket webSocket = client.newWebSocket(request, new BigModelNew(i + "", + false)); + } + }else{ + Thread.sleep(10000); + return totalAnswer; + + + } + } + } + + public static boolean canAddHistory(){ // 由于历史记录最大上线1.2W左右,需要判断是能能加入历史 + int history_length=0; + for(RoleContent temp:historyList){ + history_length=history_length+temp.content.length(); + } + if(history_length>12000){ + historyList.remove(0); + historyList.remove(1); + historyList.remove(2); + historyList.remove(3); + historyList.remove(4); + return false; + }else{ + return true; + } + } + + // 线程来发送音频与参数 + class MyThread extends Thread { + private WebSocket webSocket; + + public MyThread(WebSocket webSocket) { + this.webSocket = webSocket; + } + + public void run() { + try { + JSONObject requestJson=new JSONObject(); + + JSONObject header=new JSONObject(); // header参数 + header.put("app_id",appid); + header.put("uid",UUID.randomUUID().toString().substring(0, 10)); + + JSONObject parameter=new JSONObject(); // parameter参数 + JSONObject chat=new JSONObject(); + chat.put("domain","generalv2"); + chat.put("temperature",0.5); + chat.put("max_tokens",4096); + parameter.put("chat",chat); + + JSONObject payload=new JSONObject(); // payload参数 + JSONObject message=new JSONObject(); + JSONArray text=new JSONArray(); + + // 历史问题获取 + if(historyList.size()>0){ + for(RoleContent tempRoleContent:historyList){ + text.add(JSON.toJSON(tempRoleContent)); + } + } + + // 最新问题 + RoleContent roleContent=new RoleContent(); + roleContent.role="user"; + roleContent.content=NewQuestion; + text.add(JSON.toJSON(roleContent)); + historyList.add(roleContent); + + + message.put("text",text); + payload.put("message",message); + + + requestJson.put("header",header); + requestJson.put("parameter",parameter); + requestJson.put("payload",payload); + // System.err.println(requestJson); // 可以打印看每次的传参明细 + webSocket.send(requestJson.toString()); + // 等待服务端返回完毕后关闭 + while (true) { + // System.err.println(wsCloseFlag + "---"); + Thread.sleep(200); + if (wsCloseFlag) { + break; + } + } + webSocket.close(1000, ""); + myThread.interrupt(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @Override + public void onOpen(WebSocket webSocket, Response response) { + super.onOpen(webSocket, response); + System.out.print("大模型:"); + myThread= new MyThread(webSocket); + myThread.start(); + + } + + @Override + public void onMessage(WebSocket webSocket, String text) { + // System.out.println(userId + "用来区分那个用户的结果" + text); + JsonParse myJsonParse = gson.fromJson(text, JsonParse.class); + if (myJsonParse.header.code != 0) { + System.out.println("发生错误,错误码为:" + myJsonParse.header.code); + System.out.println("本次请求的sid为:" + myJsonParse.header.sid); + webSocket.close(1000, ""); + } + List textList = myJsonParse.payload.choices.text; + for (Text temp : textList) { +// System.out.print(temp.content); + totalAnswer=totalAnswer+temp.content; + } + if (myJsonParse.header.status == 2) { + // 可以关闭连接,释放资源 + System.out.println(); + System.out.println("*************************************************************************************"); + if(canAddHistory()){ + RoleContent roleContent=new RoleContent(); + roleContent.setRole("assistant"); + roleContent.setContent(totalAnswer); + historyList.add(roleContent); + }else{ + historyList.remove(0); + RoleContent roleContent=new RoleContent(); + roleContent.setRole("assistant"); + roleContent.setContent(totalAnswer); + historyList.add(roleContent); + } + wsCloseFlag = true; + totalFlag=true; + } + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, Response response) { + super.onFailure(webSocket, t, response); + try { + if (null != response) { + int code = response.code(); + System.out.println("onFailure code:" + code); + System.out.println("onFailure body:" + response.body().string()); + if (101 != code) { + System.out.println("connection failed"); + System.exit(0); + } + } + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + + // 鉴权方法 + public static String getAuthUrl(String hostUrl, String apiKey, String apiSecret) throws Exception { + URL url = new URL(hostUrl); + // 时间 + SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); + format.setTimeZone(TimeZone.getTimeZone("GMT")); + String date = format.format(new Date()); + // 拼接 + String preStr = "host: " + url.getHost() + "\n" + + "date: " + date + "\n" + + "GET " + url.getPath() + " HTTP/1.1"; + // System.err.println(preStr); + // SHA256加密 + Mac mac = Mac.getInstance("hmacsha256"); + SecretKeySpec spec = new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "hmacsha256"); + mac.init(spec); + + byte[] hexDigits = mac.doFinal(preStr.getBytes(StandardCharsets.UTF_8)); + // Base64加密 + String sha = Base64.getEncoder().encodeToString(hexDigits); + // System.err.println(sha); + // 拼接 + String authorization = String.format("api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", apiKey, "hmac-sha256", "host date request-line", sha); + // 拼接地址 + HttpUrl httpUrl = Objects.requireNonNull(HttpUrl.parse("https://" + url.getHost() + url.getPath())).newBuilder().// + addQueryParameter("authorization", Base64.getEncoder().encodeToString(authorization.getBytes(StandardCharsets.UTF_8))).// + addQueryParameter("date", date).// + addQueryParameter("host", url.getHost()).// + build(); + + // System.err.println(httpUrl.toString()); + return httpUrl.toString(); + } + + //返回的json结果拆解 + class JsonParse { + Header header; + Payload payload; + } + + class Header { + int code; + int status; + String sid; + } + + class Payload { + Choices choices; + } + + class Choices { + List text; + } + + class Text { + String role; + String content; + } + class RoleContent{ + String role; + String content; + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + } +} diff --git a/src/test/java/com/book/backend/CrawlerTest.java b/src/test/java/com/book/backend/CrawlerTest.java new file mode 100644 index 0000000..15f56d9 --- /dev/null +++ b/src/test/java/com/book/backend/CrawlerTest.java @@ -0,0 +1,179 @@ +package com.book.backend; + +import cn.hutool.core.lang.Console; +import cn.hutool.http.Header; +import cn.hutool.http.HttpRequest; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.book.backend.pojo.Books; +import com.book.backend.utils.NumberUtil; +import com.book.backend.utils.RandomNameUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Random; + +/** + * @author 小白条 + * @from 个人博客 + */ +@SpringBootTest +public class CrawlerTest { + @Test + public void testFetch() { + String json = "{\n" + + " \"filter\": [\n" + + " {\n" + + " \"fieldName\": \"FULLTEXT\",\n" + + " \"values\": [\n" + + " \"*\"\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"fieldName\": \"TZCID\",\n" + + " \"values\": [\n" + + " \"K\"\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"indexName\": \"\",\n" + + " \"searchtype\": \"\",\n" + + " \"sortFiedld\": \"\",\n" + + " \"size\": 20,\n" + + " \"from\": 1,\n" + + " \"searchType\": \"\"\n" + + "}"; + String url = "https://z.library.sh.cn/ui/zh/search"; + String result = HttpRequest.post(url).body(json).execute().body(); + System.out.println(result); + } + + @Test + public void test() { + String url = "http://library.mingyuefusu.top/admin/bookList?page=500&limit=100"; + //链式构建请求 + String result2 = HttpRequest.get(url) + .header(Header.USER_AGENT, "Hutool http")//头信息,多个头信息多次调用此方法即可 + .timeout(20000)//超时,毫秒 + .execute().body(); + Console.log(result2); + } + + @Test + public void test1() { + String[] bookLibraries = {"南图", "北图", "教师之家"}; + String url = "https://api.ituring.com.cn/api/Search/Advanced"; + String json = "{\n" + + " \"categoryId\": 108,\n" + + " \"sort\": \"new\",\n" + + " \"page\": 1,\n" + + " \"name\": \"\",\n" + + " \"edition\": 1\n" + + "}"; + String result2 = HttpRequest.post(url) + .body(json) + .execute().body(); + Map map = JSONUtil.toBean(result2, Map.class); + JSONArray bookItems = (JSONArray) map.get("bookItems"); + List booksList = new ArrayList<>(); + for (Object record : bookItems) { + int randomLibrary = NumberUtil.getLibraryInt(); + JSONObject tempRecord = (JSONObject) record; + Books books = new Books(); + // 生成11位数字的图书编号 + StringBuilder stringNumber = NumberUtil.getNumber(11); + long bookNumber = Long.parseLong(new String(stringNumber)); + String bookName = tempRecord.getStr("name"); + String author = tempRecord.getStr("translatorNameString"); + String bookDescription = tempRecord.getStr("abstract"); + if (bookDescription.length() >= 255) { + bookDescription = bookDescription.substring(0, 254); + } + books.setBookNumber(bookNumber); + books.setBookName(bookName); + books.setBookAuthor(author); + books.setBookLibrary(bookLibraries[randomLibrary]); + books.setBookType("计算机"); + String location = RandomNameUtils.getRandomLocation(); + books.setBookLocation(location); + books.setBookStatus("未借出"); + books.setBookDescription(bookDescription); + booksList.add(books); + } + booksList.forEach(System.out::println); + } + + @Test + public void fetchLibrary() throws IOException { + String url = "https://search.bilibili.com/all?keyword=Java&page=1"; + List小白条 + * @from 个人博客 + */ +@SpringBootTest +public class OpenAPITest { + @Test + public void testOpenApi() { + String txt = "Hello"; + + Map paramMap = new HashMap<>(); + paramMap.put("model", "gpt-3.5-turbo"); + List> dataList = new ArrayList<>(); + dataList.add(new HashMap() {{ + put("role", "user"); + put("content", txt); + }}); + paramMap.put("messages", dataList); + JSONObject message = null; + try { + String body = HttpRequest.post("https://api.openai.com/v1/chat/completions").header("Authorization", "xxxxx").header("Content-Type", "application/json").body(JSONUtil.toJsonStr(paramMap)).execute().body(); + JSONObject jsonObject = JSONUtil.parseObj(body); + JSONArray choices = jsonObject.getJSONArray("choices"); + JSONObject result = choices.get(0, JSONObject.class, Boolean.TRUE); + message = result.getJSONObject("message"); + System.out.println(message); + }catch (Exception e){ + System.out.println(e); + } + + } + @Test + public void test2(){ + String str = "{前端 Echarts V5 的 option 配置对象js代码}\n" + + "var option = {\n" + + " title: {\n" + + " text: '用户增长趋势'\n" + + " },\n" + + " tooltip: {},\n" + + " legend: {\n" + + " data:['用户数']\n" + + " },\n" + + " xAxis: {\n" + + " data: [\"1号\",\"2号\",\"3号\"]\n" + + " },\n" + + " yAxis: {},\n" + + " series: [{\n" + + " name: '用户数',\n" + + " type: 'line',\n" + + " data: [10, 20, 30]\n" + + " }]\n" + + "};"; + Pattern pattern = Pattern.compile("var option = ([^;]+);"); + Matcher matcher = pattern.matcher(str); + + if (matcher.find()) { + String match = matcher.group(1); + System.out.println(match); + } + } +} + diff --git a/src/test/java/com/book/backend/RedisTest.java b/src/test/java/com/book/backend/RedisTest.java new file mode 100644 index 0000000..a94c69f --- /dev/null +++ b/src/test/java/com/book/backend/RedisTest.java @@ -0,0 +1,47 @@ +package com.book.backend; + +import cn.hutool.core.date.StopWatch; +import com.book.backend.common.R; +import com.book.backend.pojo.dto.CommentDTO; +import com.book.backend.service.CommentService; +import com.book.backend.utils.RedisUtil; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.RedisTemplate; + +import java.time.LocalDate; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * @author 小白条 + * @from 个人博客 + */ +@SpringBootTest +class RedisTest { + @Autowired + private RedisUtil redisUtil; + @Autowired + private RedisTemplate redisTemplate; + @Autowired + private CommentService commentService; + @Test + public void test1(){ + Boolean cat = redisTemplate.expire("comment:2023-11-26", 1, TimeUnit.MINUTES); + System.out.println(cat); + } + @Test + public void convertCommentListToRedis(){ + List range = (List) redisTemplate.opsForList().range("comment:2023-11-26", 0, -1); + range.stream().forEach(System.out::println); + } + @Test + public void getCommentListFromRedis(){ + LocalDate localDate = LocalDate.now(); + String key = "comment:"+localDate; + R> commentList = commentService.getCommentList(); + List data = commentList.getData(); + redisTemplate.opsForList().rightPushAll(key,data); + } +} diff --git a/src/test/java/com/book/backend/SparkAITest.java b/src/test/java/com/book/backend/SparkAITest.java new file mode 100644 index 0000000..65579e0 --- /dev/null +++ b/src/test/java/com/book/backend/SparkAITest.java @@ -0,0 +1,66 @@ +package com.book.backend; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.book.backend.common.R; +import com.book.backend.common.exception.VueBookException; +import com.book.backend.manager.GuavaRateLimiterManager; +import com.book.backend.manager.SparkAIManager; +import com.book.backend.pojo.AiIntelligent; +import com.book.backend.pojo.Books; +import com.book.backend.pojo.UserInterfaceInfo; +import com.book.backend.service.BooksService; +import com.book.backend.service.impl.UserInterfaceInfoServiceImpl; +import com.yupi.yucongming.dev.client.YuCongMingClient; +import com.yupi.yucongming.dev.common.BaseResponse; +import com.yupi.yucongming.dev.model.DevChatRequest; +import com.yupi.yucongming.dev.model.DevChatResponse; +import okhttp3.Request; +import okhttp3.WebSocket; +import okio.ByteString; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import javax.annotation.Resource; +import java.util.HashSet; +import java.util.List; + +/** + * @author 小白条 + * @from 个人博客 + */ +@SpringBootTest +public class SparkAITest { + @Resource + private BooksService booksService; + @Resource + private GuavaRateLimiterManager guavaRateLimiterManager; + @Resource + private UserInterfaceInfoServiceImpl userInterfaceInfoService; + @Test + public void test() throws Exception { + // 判断用户输入文本是否过长,超过128字,直接返回,防止资源耗尽 + BigModelNew bigModelNew = new BigModelNew("23", false); + StringBuilder stringBuilder = new StringBuilder(); + HashSet hashSet = new HashSet<>(); + String presetInformation = "请根据数据库内容和游客信息作出推荐,书籍必须是数据库里面有的,可以是一本也可以是多本,根据游客喜欢的信息作出推荐。"; + + List list = booksService.list(); + stringBuilder.append(presetInformation).append("\n").append("数据库内容: "); + for (Books books : list) { + if(!hashSet.contains(books.getBookName())){ + hashSet.add(books.getBookName()); + stringBuilder.append(books.getBookName()).append(","); + } + } + stringBuilder.append("\n"); + String customerInformation = "我喜欢关于动物的书籍,请给我推荐图书"; + stringBuilder.append("游客信息: ").append(customerInformation).append("\n"); +// System.out.println(stringBuilder.toString()); + String reallyMessage = stringBuilder.toString(); + String result = bigModelNew.sendMessageAndGetResponse(reallyMessage); + System.out.println(result); + } + +} diff --git a/src/test/java/com/book/backend/SparkClientTest.java b/src/test/java/com/book/backend/SparkClientTest.java new file mode 100644 index 0000000..42d5046 --- /dev/null +++ b/src/test/java/com/book/backend/SparkClientTest.java @@ -0,0 +1,165 @@ +package com.book.backend; + +import cn.hutool.core.date.StopWatch; +import com.book.backend.manager.SparkClient; +import com.book.backend.manager.constant.SparkApiVersion; +import com.book.backend.manager.exception.SparkException; +import com.book.backend.manager.listener.SparkConsoleListener; +import com.book.backend.manager.model.SparkMessage; +import com.book.backend.manager.model.SparkSyncChatResponse; +import com.book.backend.manager.model.request.SparkRequest; +import com.book.backend.manager.model.request.function.SparkFunctionBuilder; +import com.book.backend.manager.model.response.SparkResponseFunctionCall; +import com.book.backend.manager.model.response.SparkTextUsage; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * SparkClientTest + * + * @author briqt + */ +public class SparkClientTest { + + /** + * 客户端实例,线程安全 + */ + SparkClient sparkClient = new SparkClient(); + + // 设置认证信息 + { + sparkClient.appid = "xxxxxxx"; + sparkClient.apiKey = "xxxxx"; + sparkClient.apiSecret = "xxxxxx"; + } + + @Test + void chatStreamTest() throws InterruptedException { + // 消息列表,可以在此列表添加历史对话记录 + List messages = new ArrayList<>(); + messages.add(SparkMessage.systemContent("请你扮演我的语文老师李老师,问我讲解问题问题,希望你可以保证知识准确,逻辑严谨。")); + messages.add(SparkMessage.userContent("鲁迅和周树人小时候打过架吗?")); + + // 构造请求 + SparkRequest sparkRequest = SparkRequest.builder() + // 消息列表 + .messages(messages) + // 模型回答的tokens的最大长度,非必传,默认为2048。 + // V1.5取值为[1,4096] + // V2.0取值为[1,8192] + // V3.0取值为[1,8192] + .maxTokens(2048) + // 核采样阈值。用于决定结果随机性,取值越高随机性越强即相同的问题得到的不同答案的可能性越高 非必传,取值为[0,1],默认为0.5 + .temperature(0.2) + // 指定请求版本,默认使用2.0版本 + .apiVersion(SparkApiVersion.V3_0) + .build(); + + // 使用默认的控制台监听器,流式调用; + // 实际使用时请继承SparkBaseListener自定义监听器实现 + sparkClient.chatStream(sparkRequest, new SparkConsoleListener()); + + Thread.sleep(60000); + } + + @Test + void chatSyncTest() throws JsonProcessingException { + // 消息列表,可以在此列表添加历史对话记录 + List messages = new ArrayList<>(); +// messages.add(SparkMessage.userContent("请你扮演我的语文老师李老师,问我讲解问题问题,希望你可以保证知识准确,逻辑严谨。")); +// messages.add(SparkMessage.assistantContent("好的,这位同学,有什么问题需要李老师为你解答吗?")); + messages.add(SparkMessage.userContent("查询北京未来七天的天气预报")); + + // 构造请求 + SparkRequest sparkRequest = SparkRequest.builder() + // 消息列表 + .messages(messages) + // 模型回答的tokens的最大长度,非必传,默认为2048。 + // V1.5取值为[1,4096] + // V2.0取值为[1,8192] + // V3.0取值为[1,8192] + .maxTokens(2048) + // 核采样阈值。用于决定结果随机性,取值越高随机性越强即相同的问题得到的不同答案的可能性越高 非必传,取值为[0,1],默认为0.5 + .temperature(0.2) + .build(); + + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + System.out.println("提问:" + objectMapper.writeValueAsString(messages)); + + try { + // 同步调用 + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + SparkSyncChatResponse chatResponse = sparkClient.chatSync(sparkRequest); + SparkTextUsage textUsage = chatResponse.getTextUsage(); + stopWatch.stop(); + long total = stopWatch.getTotal(TimeUnit.SECONDS); + System.out.println("耗时"+total+"\n"); + System.out.println("\n回答:" + chatResponse.getContent()); + System.out.println("\n提问tokens:" + textUsage.getPromptTokens() + + ",回答tokens:" + textUsage.getCompletionTokens() + + ",总消耗tokens:" + textUsage.getTotalTokens()); + } catch (SparkException e) { + System.out.println("发生异常了:" + e.getMessage()); + } + } + + @Test + void functionCallTest() throws JsonProcessingException { + // 消息列表,可以在此列表添加历史对话记录 + List messages = new ArrayList<>(); + messages.add(SparkMessage.userContent("科大讯飞的最新股票价格是多少")); + + // 构造请求 + SparkRequest sparkRequest = SparkRequest.builder() + // 消息列表 + .messages(messages) + // 使用functionCall功能版本需要大于等于3.0 + .apiVersion(SparkApiVersion.V3_0) + // 添加方法,可多次调用添加多个方法 + .addFunction( + // 回调时回传的方法名 + SparkFunctionBuilder.functionName("stockPrice") + // 让大模型理解方法意图 方法描述 + .description("根据公司名称查询最新股票价格") + // 方法需要的参数。可多次调用添加多个参数 + .addParameterProperty("companyName", "string", "公司名称") + // 指定以上的参数哪些是必传的 + .addParameterRequired("companyName").build() + ).build(); + + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + System.out.println("request:" + objectMapper.writeValueAsString(sparkRequest)); + + // 同步调用 + SparkSyncChatResponse chatResponse = sparkClient.chatSync(sparkRequest); + SparkTextUsage textUsage = chatResponse.getTextUsage(); + SparkResponseFunctionCall functionCall = chatResponse.getFunctionCall(); + + if (null != functionCall) { + String functionName = functionCall.getName(); + Map arguments = functionCall.getMapArguments(); + + System.out.println("\n收到functionCall:方法名称:" + functionName + ",参数:" + objectMapper.writeValueAsString(arguments)); + + // 在这里根据方法名和参数自行调用方法实现 + + } else { + System.out.println("\n回答:" + chatResponse.getContent()); + } + + System.out.println("\n提问tokens:" + textUsage.getPromptTokens() + + ",回答tokens:" + textUsage.getCompletionTokens() + + ",总消耗tokens:" + textUsage.getTotalTokens()); + } +} diff --git a/src/test/java/com/book/backend/VueBookBackendApplicationTests.java b/src/test/java/com/book/backend/VueBookBackendApplicationTests.java new file mode 100644 index 0000000..1783c0e --- /dev/null +++ b/src/test/java/com/book/backend/VueBookBackendApplicationTests.java @@ -0,0 +1,82 @@ +package com.book.backend; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.book.backend.pojo.Admins; +import com.book.backend.pojo.Users; +import com.book.backend.pojo.Violation; +import com.book.backend.service.AdminsService; +import com.book.backend.service.BookAdminsService; +import com.book.backend.service.UsersService; +import com.book.backend.service.ViolationService; +import com.book.backend.utils.BorrowDateUtil; +import com.book.backend.utils.JwtKit; +import com.book.backend.utils.NumberUtil; +import com.book.backend.utils.RandomNameUtils; +import io.jsonwebtoken.Claims; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import javax.swing.text.DateFormatter; +import java.text.DateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +@SpringBootTest + +class VueBookBackendApplicationTests { + @Autowired + private UsersService usersService; + @Autowired + private AdminsService adminsService; + @Autowired + private BookAdminsService bookAdminsService; + @Autowired + private ViolationService violationService; + @Autowired + private JwtKit jwtKit; + @Test + public void getUser(){ + List list = usersService.list(); + list.forEach(System.out::println); + } + @Test + public void getAdmins(){ + List list = adminsService.list(); + list.forEach(System.out::println); + } + @Test + public void getBookAdmins(){ + bookAdminsService.list().forEach(System.out::println); + } + @Test + public void tokenUse(){ + Admins admins = new Admins(); + admins.setAdminName("张三"); + admins.setPassword("2313"); + String s = jwtKit.generateToken(admins); + Claims claims = jwtKit.parseJwtToken(s); + System.out.println( claims.get("username")); + } + @Test + public void testNumber(){ + StringBuilder number = NumberUtil.getNumber(11); + System.out.println(number); + } + @Test + public void testName(){ + String s = RandomNameUtils.fullName(); + System.out.println(s); + } + @Test + public void testBorrowData(){ + LocalDateTime now = LocalDateTime.now(); + String[] dateArray = BorrowDateUtil.getDateArray(now); + for (String s : dateArray) { + System.out.println(s); + } + } + +} diff --git a/src/test/java/com/book/backend/VueBookBackendUserTest.java b/src/test/java/com/book/backend/VueBookBackendUserTest.java new file mode 100644 index 0000000..e859421 --- /dev/null +++ b/src/test/java/com/book/backend/VueBookBackendUserTest.java @@ -0,0 +1,145 @@ +package com.book.backend; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.book.backend.common.exception.CommonError; +import com.book.backend.common.exception.ErrorCode; +import com.book.backend.common.exception.VueBookException; +import com.book.backend.mapper.BooksMapper; +import com.book.backend.pojo.Books; +import com.book.backend.pojo.BooksBorrow; +import com.book.backend.pojo.Users; +import com.book.backend.service.*; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import javax.annotation.Resource; +import java.lang.reflect.Array; +import java.time.LocalDateTime; +import java.util.*; + +@SpringBootTest +public class VueBookBackendUserTest { + @Autowired + private BooksService booksService; + @Resource + private BooksMapper booksMapper; + @Autowired + private BookRuleService bookRuleService; + @Autowired + private NoticeService noticeService; + @Autowired + private UsersService usersService; + @Autowired + private BooksBorrowService booksBorrowService; + @Autowired + private ViolationService violationService; + @Test + public void getAllBooks(){ + booksService.list().forEach(System.out::println); + } + + /** + * 查询图书 + * 按照查询条件和查询内容 + */ + @Test + public void getBooksByCondition(){ + QueryWrapper queryWrapper = new QueryWrapper<>(); + String search = "bookName"; + String input = "红"; + queryWrapper.like(search,input); + HashMap map = new HashMap<>(); + map.put(search,input); + LambdaQueryWrapper queryWrapper1 = new LambdaQueryWrapper<>(); + queryWrapper1.orderByAsc(Books::getBookId); +// List list = booksService.list(queryWrapper); +// List list = booksMapper.selectList(queryWrapper); +// List list = booksService.listByMap(map); + List list = booksMapper.selectList(queryWrapper1); + System.out.println(list); + } + + /** + * 查询所有读者规则 + */ + @Test + public void getRuleList(){ + bookRuleService.list().forEach(System.out::println); + } + + /** + * 查询所有公告列表 + */ + @Test + public void getNoticeList(){ + noticeService.list().forEach(System.out::println); + } + + /** + * 根据用户id 查询用户信息 + */ + @Test + public void getUserByUserId(){ + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(Users::getUserId,1923); + Users usersServiceOne = usersService.getOne(queryWrapper); + System.out.println(usersServiceOne); + } + + /** + * 查询图书借阅信息 + */ + @Test + public void getBookBorrow(){ + String condition = "return_date"; + String search = "2023-02-25"; + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.like(condition,search); + List list = booksBorrowService.list(queryWrapper); + System.out.println(list); + } + @Test + public void getViolationList(){ + violationService.list().forEach(System.out::println); + } + + @Test + public void testBorrowBooks(){ + BooksBorrow booksBorrow1 = new BooksBorrow(); + booksBorrow1.setBorrowId(null); + booksBorrow1.setBookNumber(50970375442L); + booksBorrow1.setCardNumber(18012345678L); + booksBorrow1.setBorrowDate(LocalDateTime.now()); + booksBorrow1.setCloseDate(LocalDateTime.now().plusDays(30)); + booksBorrow1.setReturnDate(null); + boolean flag = booksBorrowService.save(booksBorrow1); + System.out.println(flag); + } + @Test + public void testBooksBorrowTypeStatistic(){ + HashMap hashMap = new HashMap<>(); + BooksBorrowService borrowService = booksBorrowService; + List booksBorrowList = borrowService.list(); + for (BooksBorrow booksBorrow : booksBorrowList) { + Long bookNumber = booksBorrow.getBookNumber(); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(Books::getBookNumber,bookNumber); + Books book = booksService.getOne(queryWrapper); + if(book == null){ + VueBookException.cast(CommonError.OBJECT_NULL); + } + String bookType = book.getBookType(); + hashMap.put(bookType, hashMap.getOrDefault(bookType,0)+1); + } + ArrayList> list = new ArrayList<>(); + Set> entries = hashMap.entrySet(); + for (Map.Entry entry : entries) { + HashMap map = new HashMap(); + map.put(entry.getKey(),entry.getValue()); + list.add(map); + } + System.out.println(list); + } +} diff --git a/src/test/java/com/book/backend/domain/BookData.java b/src/test/java/com/book/backend/domain/BookData.java new file mode 100644 index 0000000..477984e --- /dev/null +++ b/src/test/java/com/book/backend/domain/BookData.java @@ -0,0 +1,63 @@ +package com.book.backend.domain; + +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.io.Serializable; + +/** + * + * @TableName t_books + */ +@Data +public class BookData implements Serializable { + + + + + /** + * 图书名称 + */ + @ExcelProperty(value="图书名称",index = 0) + private String bookName; + + /** + * 图书作者 + */ + @ExcelProperty(value="图书作者",index = 1) + private String bookAuthor; + + /** + * 图书所在图书馆名称 + */ + @ExcelProperty(value="图书馆",index = 2) + private String bookLibrary; + + /** + * 图书类别 + */ + @ExcelProperty(value="图书类别",index = 3) + private String bookType; + + /** + * 图书位置 + * 例如A12 B06 + */ + @ExcelProperty(value="图书位置",index = 4) + private String bookLocation; + + /** + * 图书状态(已借出/未借出) + */ + @ExcelProperty(value="图书状态",index = 5) + private String bookStatus; + + /** + * 图书描述 + */ + @ExcelProperty(value="图书描述",index = 6) + private String bookDescription; + + + +} diff --git a/src/test/java/com/book/backend/domain/DemoData.java b/src/test/java/com/book/backend/domain/DemoData.java new file mode 100644 index 0000000..ea62613 --- /dev/null +++ b/src/test/java/com/book/backend/domain/DemoData.java @@ -0,0 +1,22 @@ +package com.book.backend.domain; + +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.util.Date; + +/** + * @author 小白条 + * @from 个人博客 + */ +@Data +public class DemoData { + @ExcelProperty(value="整数",index = 0) + private Integer number; + @ExcelProperty(value="字符串",index = 1) + private String string; + @ExcelProperty(value="小数",index =2) + private Double price; + @ExcelProperty(value="日期",index = 3) + private Date datetime; +} diff --git a/src/test/java/com/book/backend/testutils/EasyExcelTest.java b/src/test/java/com/book/backend/testutils/EasyExcelTest.java new file mode 100644 index 0000000..395132d --- /dev/null +++ b/src/test/java/com/book/backend/testutils/EasyExcelTest.java @@ -0,0 +1,85 @@ +package com.book.backend.testutils; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import com.alibaba.excel.support.ExcelTypeEnum; +import com.book.backend.domain.BookData; +import com.book.backend.pojo.Books; +import com.book.backend.service.BooksService; +import com.book.backend.utils.NumberUtil; +import org.junit.jupiter.api.Test; +import org.springframework.beans.BeanUtils; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.util.ResourceUtils; + +import javax.annotation.Resource; +import java.io.File; +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * EasyExcel 测试 + * + * @author 程序员小白条 + * + */ +@SpringBootTest +public class EasyExcelTest { + @Resource + private BooksService booksService; + + @Test + public void doImport() throws FileNotFoundException { + File file = ResourceUtils.getFile("classpath:test_excel.xlsx"); + List> list = EasyExcel.read(file) + .excelType(ExcelTypeEnum.XLSX) + .sheet() + .headRowNumber(0) + .doReadSync(); + System.out.println(list); + } + @Test + public void testReadExcel() { + // 读取的excel文件路径 + String filename = "D:\\IDEAproject\\vue_book_backend\\src\\main\\resources\\test_excel.xlsx"; + // 读取excel + EasyExcel.read(filename, BookData.class, new AnalysisEventListener() { + ArrayList booksList = new ArrayList<>(); + // 每解析一行数据,该方法会被调用一次 + @Override + public void invoke(BookData bookData, AnalysisContext analysisContext) { + Books books = new Books(); + // 生成11位数字的图书编号 + StringBuilder stringBuilder = NumberUtil.getNumber(11); + long bookNumber = Long.parseLong(new String(stringBuilder)); + BeanUtils.copyProperties(bookData,books); + books.setBookNumber(bookNumber); + booksList.add(books); +// System.out.println("解析数据为:" + bookData.toString()); + } + // 全部解析完成被调用 + @Override + public void doAfterAllAnalysed(AnalysisContext analysisContext) { + // 全部加入到容器list中后,一次性批量导入,先判断容器是否为空 + if(!booksList.isEmpty()){ + // 可以将解析的数据保存到数据库 + boolean flag = booksService.saveBatch(booksList); + // 如果数据添加成功 + if(flag){ + System.out.println("Excel批量添加图书成功"); + }else{ + System.out.println("Excel批量添加图书失败"); + } + }else{ + System.out.println("空表无法进行数据导入"); + } + + } + }).sheet().doRead(); + } + + +}