Repository: liyupi/yudada Branch: master Commit: e5eb265817f1 Files: 264 Total size: 513.2 KB Directory structure: gitextract_ta69pkyf/ ├── .gitignore ├── README.md ├── mbti-test-mini/ │ ├── .editorconfig │ ├── .eslintrc │ ├── babel.config.js │ ├── config/ │ │ ├── dev.ts │ │ ├── index.ts │ │ └── prod.ts │ ├── package.json │ ├── project.config.json │ ├── project.private.config.json │ ├── project.tt.json │ ├── src/ │ │ ├── app.config.ts │ │ ├── app.scss │ │ ├── app.ts │ │ ├── components/ │ │ │ └── GlobalFooter/ │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── data/ │ │ │ ├── question_results.json │ │ │ └── questions.json │ │ ├── index.html │ │ ├── pages/ │ │ │ ├── doQuestion/ │ │ │ │ ├── index.config.ts │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── index/ │ │ │ │ ├── index.config.ts │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ └── result/ │ │ │ ├── index.config.ts │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ └── utils/ │ │ └── bizUtils.ts │ ├── tsconfig.json │ └── types/ │ ├── custom.d.ts │ └── global.d.ts ├── yudada-backend/ │ ├── .gitignore │ ├── .mvn/ │ │ └── wrapper/ │ │ └── maven-wrapper.properties │ ├── Dockerfile │ ├── README.md │ ├── mvnw │ ├── mvnw.cmd │ ├── pom.xml │ ├── sql/ │ │ ├── create_table.sql │ │ ├── init_data.sql │ │ └── post_es_mapping.json │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── yupi/ │ │ │ └── yudada/ │ │ │ ├── MainApplication.java │ │ │ ├── annotation/ │ │ │ │ └── AuthCheck.java │ │ │ ├── aop/ │ │ │ │ ├── AuthInterceptor.java │ │ │ │ └── LogInterceptor.java │ │ │ ├── common/ │ │ │ │ ├── BaseResponse.java │ │ │ │ ├── DeleteRequest.java │ │ │ │ ├── ErrorCode.java │ │ │ │ ├── PageRequest.java │ │ │ │ ├── ResultUtils.java │ │ │ │ └── ReviewRequest.java │ │ │ ├── config/ │ │ │ │ ├── AiConfig.java │ │ │ │ ├── CorsConfig.java │ │ │ │ ├── CosClientConfig.java │ │ │ │ ├── JsonConfig.java │ │ │ │ ├── MyBatisPlusConfig.java │ │ │ │ ├── RedissonConfig.java │ │ │ │ └── VipSchedulerConfig.java │ │ │ ├── constant/ │ │ │ │ ├── CommonConstant.java │ │ │ │ ├── FileConstant.java │ │ │ │ └── UserConstant.java │ │ │ ├── controller/ │ │ │ │ ├── AppController.java │ │ │ │ ├── AppStatisticController.java │ │ │ │ ├── FileController.java │ │ │ │ ├── PostController.java │ │ │ │ ├── PostFavourController.java │ │ │ │ ├── PostThumbController.java │ │ │ │ ├── QuestionController.java │ │ │ │ ├── ScoringResultController.java │ │ │ │ ├── UserAnswerController.java │ │ │ │ └── UserController.java │ │ │ ├── exception/ │ │ │ │ ├── BusinessException.java │ │ │ │ ├── GlobalExceptionHandler.java │ │ │ │ └── ThrowUtils.java │ │ │ ├── generate/ │ │ │ │ └── CodeGenerator.java │ │ │ ├── manager/ │ │ │ │ ├── AiManager.java │ │ │ │ └── CosManager.java │ │ │ ├── mapper/ │ │ │ │ ├── AppMapper.java │ │ │ │ ├── PostFavourMapper.java │ │ │ │ ├── PostMapper.java │ │ │ │ ├── PostThumbMapper.java │ │ │ │ ├── QuestionMapper.java │ │ │ │ ├── ScoringResultMapper.java │ │ │ │ ├── UserAnswerMapper.java │ │ │ │ └── UserMapper.java │ │ │ ├── model/ │ │ │ │ ├── dto/ │ │ │ │ │ ├── app/ │ │ │ │ │ │ ├── AppAddRequest.java │ │ │ │ │ │ ├── AppEditRequest.java │ │ │ │ │ │ ├── AppQueryRequest.java │ │ │ │ │ │ └── AppUpdateRequest.java │ │ │ │ │ ├── file/ │ │ │ │ │ │ └── UploadFileRequest.java │ │ │ │ │ ├── post/ │ │ │ │ │ │ ├── PostAddRequest.java │ │ │ │ │ │ ├── PostEditRequest.java │ │ │ │ │ │ ├── PostQueryRequest.java │ │ │ │ │ │ └── PostUpdateRequest.java │ │ │ │ │ ├── postfavour/ │ │ │ │ │ │ ├── PostFavourAddRequest.java │ │ │ │ │ │ └── PostFavourQueryRequest.java │ │ │ │ │ ├── postthumb/ │ │ │ │ │ │ └── PostThumbAddRequest.java │ │ │ │ │ ├── question/ │ │ │ │ │ │ ├── AiGenerateQuestionRequest.java │ │ │ │ │ │ ├── QuestionAddRequest.java │ │ │ │ │ │ ├── QuestionAnswerDTO.java │ │ │ │ │ │ ├── QuestionContentDTO.java │ │ │ │ │ │ ├── QuestionEditRequest.java │ │ │ │ │ │ ├── QuestionQueryRequest.java │ │ │ │ │ │ └── QuestionUpdateRequest.java │ │ │ │ │ ├── scoringResult/ │ │ │ │ │ │ ├── ScoringResultAddRequest.java │ │ │ │ │ │ ├── ScoringResultEditRequest.java │ │ │ │ │ │ ├── ScoringResultQueryRequest.java │ │ │ │ │ │ └── ScoringResultUpdateRequest.java │ │ │ │ │ ├── statistic/ │ │ │ │ │ │ ├── AppAnswerCountDTO.java │ │ │ │ │ │ └── AppAnswerResultCountDTO.java │ │ │ │ │ ├── user/ │ │ │ │ │ │ ├── UserAddRequest.java │ │ │ │ │ │ ├── UserLoginRequest.java │ │ │ │ │ │ ├── UserQueryRequest.java │ │ │ │ │ │ ├── UserRegisterRequest.java │ │ │ │ │ │ ├── UserUpdateMyRequest.java │ │ │ │ │ │ └── UserUpdateRequest.java │ │ │ │ │ └── userAnswer/ │ │ │ │ │ ├── UserAnswerAddRequest.java │ │ │ │ │ ├── UserAnswerEditRequest.java │ │ │ │ │ ├── UserAnswerQueryRequest.java │ │ │ │ │ └── UserAnswerUpdateRequest.java │ │ │ │ ├── entity/ │ │ │ │ │ ├── App.java │ │ │ │ │ ├── Post.java │ │ │ │ │ ├── PostFavour.java │ │ │ │ │ ├── PostThumb.java │ │ │ │ │ ├── Question.java │ │ │ │ │ ├── ScoringResult.java │ │ │ │ │ ├── User.java │ │ │ │ │ └── UserAnswer.java │ │ │ │ ├── enums/ │ │ │ │ │ ├── AppScoringStrategyEnum.java │ │ │ │ │ ├── AppTypeEnum.java │ │ │ │ │ ├── FileUploadBizEnum.java │ │ │ │ │ ├── ReviewStatusEnum.java │ │ │ │ │ └── UserRoleEnum.java │ │ │ │ └── vo/ │ │ │ │ ├── AppVO.java │ │ │ │ ├── LoginUserVO.java │ │ │ │ ├── PostVO.java │ │ │ │ ├── QuestionVO.java │ │ │ │ ├── ScoringResultVO.java │ │ │ │ ├── UserAnswerVO.java │ │ │ │ └── UserVO.java │ │ │ ├── scoring/ │ │ │ │ ├── AiTestScoringStrategy.java │ │ │ │ ├── CustomScoreScoringStrategy.java │ │ │ │ ├── CustomTestScoringStrategy.java │ │ │ │ ├── ScoringStrategy.java │ │ │ │ ├── ScoringStrategyConfig.java │ │ │ │ ├── ScoringStrategyContext.java │ │ │ │ └── ScoringStrategyExecutor.java │ │ │ ├── service/ │ │ │ │ ├── AppService.java │ │ │ │ ├── PostFavourService.java │ │ │ │ ├── PostService.java │ │ │ │ ├── PostThumbService.java │ │ │ │ ├── QuestionService.java │ │ │ │ ├── ScoringResultService.java │ │ │ │ ├── UserAnswerService.java │ │ │ │ ├── UserService.java │ │ │ │ └── impl/ │ │ │ │ ├── AppServiceImpl.java │ │ │ │ ├── PostFavourServiceImpl.java │ │ │ │ ├── PostServiceImpl.java │ │ │ │ ├── PostThumbServiceImpl.java │ │ │ │ ├── QuestionServiceImpl.java │ │ │ │ ├── ScoringResultServiceImpl.java │ │ │ │ ├── UserAnswerServiceImpl.java │ │ │ │ └── UserServiceImpl.java │ │ │ └── utils/ │ │ │ ├── NetUtils.java │ │ │ ├── SpringContextUtils.java │ │ │ └── SqlUtils.java │ │ └── resources/ │ │ ├── META-INF/ │ │ │ └── additional-spring-configuration-metadata.json │ │ ├── application-prod.yml │ │ ├── application-test.yml │ │ ├── application.yml │ │ ├── banner.txt │ │ ├── mapper/ │ │ │ ├── AppMapper.xml │ │ │ ├── PostFavourMapper.xml │ │ │ ├── PostMapper.xml │ │ │ ├── PostThumbMapper.xml │ │ │ ├── QuestionMapper.xml │ │ │ ├── ScoringResultMapper.xml │ │ │ ├── UserAnswerMapper.xml │ │ │ └── UserMapper.xml │ │ ├── templates/ │ │ │ ├── TemplateController.java.ftl │ │ │ ├── TemplateService.java.ftl │ │ │ ├── TemplateServiceImpl.java.ftl │ │ │ └── model/ │ │ │ ├── TemplateAddRequest.java.ftl │ │ │ ├── TemplateEditRequest.java.ftl │ │ │ ├── TemplateQueryRequest.java.ftl │ │ │ ├── TemplateUpdateRequest.java.ftl │ │ │ └── TemplateVO.java.ftl │ │ └── test_excel.xlsx │ └── test/ │ └── java/ │ └── com/ │ └── yupi/ │ └── yudada/ │ ├── MainApplicationTests.java │ ├── QuestionControllerTest.java │ ├── RxJavaTest.java │ ├── UserAnswerShardingTest.java │ ├── ZhiPuAiTest.java │ ├── manager/ │ │ └── CosManagerTest.java │ ├── mapper/ │ │ ├── PostFavourMapperTest.java │ │ └── PostMapperTest.java │ ├── service/ │ │ ├── PostFavourServiceTest.java │ │ ├── PostThumbServiceTest.java │ │ └── UserServiceTest.java │ └── utils/ │ └── EasyExcelTest.java └── yudada-frontend/ ├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── Dockerfile ├── README.md ├── babel.config.js ├── docker/ │ └── nginx.conf ├── openapi.config.ts ├── package.json ├── public/ │ └── index.html ├── src/ │ ├── App.vue │ ├── access/ │ │ ├── accessEnum.ts │ │ ├── checkAccess.ts │ │ └── index.ts │ ├── api/ │ │ ├── appController.ts │ │ ├── appStatisticController.ts │ │ ├── fileController.ts │ │ ├── index.ts │ │ ├── postController.ts │ │ ├── postFavourController.ts │ │ ├── postThumbController.ts │ │ ├── questionController.ts │ │ ├── scoringResultController.ts │ │ ├── typings.d.ts │ │ ├── userAnswerController.ts │ │ └── userController.ts │ ├── components/ │ │ ├── AppCard.vue │ │ ├── GlobalHeader.vue │ │ ├── MdEditor.vue │ │ ├── MdViewer.vue │ │ ├── PictureUploader.vue │ │ └── ShareModal.vue │ ├── constant/ │ │ └── app.ts │ ├── layouts/ │ │ ├── BasicLayout.vue │ │ └── UserLayout.vue │ ├── main.ts │ ├── request.ts │ ├── router/ │ │ ├── index.ts │ │ └── routes.ts │ ├── shims-vue.d.ts │ ├── store/ │ │ └── userStore.ts │ └── views/ │ ├── HomePage.vue │ ├── NoAuthPage.vue │ ├── add/ │ │ ├── AddAppPage.vue │ │ ├── AddQuestionPage.vue │ │ ├── AddScoringResultPage.vue │ │ └── components/ │ │ ├── AiGenerateQuestionDrawer.vue │ │ └── ScoringResultTable.vue │ ├── admin/ │ │ ├── AdminAppPage.vue │ │ ├── AdminQuestionPage.vue │ │ ├── AdminScoringResultPage.vue │ │ ├── AdminUserAnswerPage.vue │ │ └── AdminUserPage.vue │ ├── answer/ │ │ ├── AnswerResultPage.vue │ │ ├── DoAnswerPage.vue │ │ └── MyAnswerPage.vue │ ├── app/ │ │ └── AppDetailPage.vue │ ├── statistic/ │ │ └── AppStatisticPage.vue │ └── user/ │ ├── UserLoginPage.vue │ └── UserRegisterPage.vue ├── tsconfig.json └── vue.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ ### Intellij template # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf # AWS User-specific .idea/**/aws.xml # Generated files .idea/**/contentModel.xml # Sensitive or high-churn files .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml .idea/**/dbnavigator.xml # Gradle .idea/**/gradle.xml .idea/**/libraries # Gradle and Maven with auto-import # When using Gradle or Maven with auto-import, you should exclude module files, # since they will be recreated, and may cause churn. Uncomment if using # auto-import. # .idea/artifacts # .idea/compiler.xml # .idea/jarRepositories.xml # .idea/modules.xml # .idea/*.iml # .idea/modules # *.iml # *.ipr # CMake cmake-build-*/ # Mongo Explorer plugin .idea/**/mongoSettings.xml # File-based project format *.iws # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml # SonarLint plugin .idea/sonarlint/ # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties # Editor-based Rest Client .idea/httpRequests # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser ### WebStorm template # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf # AWS User-specific .idea/**/aws.xml # Generated files .idea/**/contentModel.xml # Sensitive or high-churn files .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml .idea/**/dbnavigator.xml # Gradle .idea/**/gradle.xml .idea/**/libraries # Gradle and Maven with auto-import # When using Gradle or Maven with auto-import, you should exclude module files, # since they will be recreated, and may cause churn. Uncomment if using # auto-import. # .idea/artifacts # .idea/compiler.xml # .idea/jarRepositories.xml # .idea/modules.xml # .idea/*.iml # .idea/modules # *.iml # *.ipr # CMake cmake-build-*/ # Mongo Explorer plugin .idea/**/mongoSettings.xml # File-based project format *.iws # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml # SonarLint plugin .idea/sonarlint/ # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties # Editor-based Rest Client .idea/httpRequests # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser ================================================ FILE: README.md ================================================ # 鱼答答 - AI 答题应用平台 > 作者:[程序员鱼皮](https://yuyuanweb.feishu.cn/wiki/Abldw5WkjidySxkKxU2cQdAtnah) > > ⭐️ 加入项目系列学习:[加入编程导航](https://yuyuanweb.feishu.cn/wiki/SDtMwjR1DituVpkz5MLc3fZLnzb) ## 项目简介 ### 项目介绍 深入业务场景的企业级实战项目,基于 Vue 3 + Spring Boot + Redis + ChatGLM AI + RxJava + SSE 的 **AI 答题应用平台。** 用户可以基于 AI 快速制作并发布多种答题应用,支持检索和分享应用、在线答题并基于评分算法或 AI 得到回答总结;管理员可以审核应用、集中管理整站内容,并进行统计分析。 > 视频介绍:https://www.bilibili.com/video/BV1m142197hg ### 项目四大阶段 该项目选题新颖、业务完整、技术亮点多,为了帮大家循序渐进地学习,鱼皮将项目设计为 4 个阶段,通俗易懂: 1)第一阶段,开发本地的 `MBTI 性格测试小程序`。带大家熟悉答题应用的开发流程和原理,从 0 到 1 实战 Taro 跨端微信小程序开发,并分享小程序开发经验。 ![](https://pic.yupi.icu/1/image-20240604145837172.png) 2)第二阶段,上升一个层次,开发 `答题应用平台`。用户可以通过上传题目和自定义评分规则,创建答题应用,供其他用户检索和使用。该阶段涉及 Vue 3 + Spring Boot 前后端全栈项目从 0 到 1 的开发。 ![](https://pic.yupi.icu/1/20240604145229177.png) 3)第三阶段,让 AI 为平台赋能,开发 `AI 智能答题应用平台`。用户只需设定主题,就能通过 AI 快速生成题目、让 AI 分析用户答案,极大降低创建答题应用的成本、提高回答多样性。是从 0 到 1 的 AI 应用开发教程,封装 AI 通用模块并教你成为 Prompt 大师! ![](https://pic.yupi.icu/1/20240604145229383.png) 4)第四阶段,通过多种企业开发技术手段进行 `项目优化`。包括 RxJava + SSE 优化 AI 生成体验、通过缓存和分库分表优化性能、通过幂等设计和线程池隔离提高系统安全性、通过统计分析和应用分享功能来将应用 “产品化” 等等,涉及大量干货! 在这个项目中,鱼皮还会带大家用 AI 工具 `CodeGeeX 智能编程助手` 提高开发效率,是不是已经迫不及待了呢? ### 项目展示 本项目涉及 10 多个页面,前面已经展示部分页面。 应用详情页: ![](https://pic.yupi.icu/1/20240604145229915.png) 用户答题页面: ![](https://pic.yupi.icu/1/20240604145230156.png) 创建应用页: ![](https://pic.yupi.icu/1/20240604145230361.png) 创建题目页,涉及复杂动态嵌套表单的开发: ![](https://pic.yupi.icu/1/20240604145230557.png) 应用管理页面: ![](https://pic.yupi.icu/1/20240604145230731.png) 统计分析页面: ![](https://pic.yupi.icu/1/20240604145230905.png) 应用分享功能: ![](https://pic.yupi.icu/1/20240604145231269.png) ### 免费试看 感兴趣的同学可以 **免费试看** 第一期项目回放:https://www.bilibili.com/video/BV1m142197hg ## 项目特点 鱼皮 **从 0 到 1 全程直播无剪辑** 地带大家开发完成项目,包括 **视频教程** 和 **文字教程**!从需求分析、技术选型、项目设计、项目初始化、Demo 编写、前后端开发实现、项目优化、部署上线等,每个环节我都 **从理论到实践** 给大家讲的明明白白、每个细节都不放过! 细致入微的教程: ![](https://pic.yupi.icu/1/20240604145231642.png) 满满的项目正反馈: ![](https://pic.yupi.icu/1/20240604145231908.png) ### 为什么要带做这个项目? 1)**业务真实新颖**:别人做答题应用,你做 AI 应用平台。需求实用价值更高,可以扩展出很多新奇有趣的热门应用。 2)**技术主流新颖**:基于企业主流前后端技术实现,再结合当下最热门的 AI 技术,比传统项目更有亮点。 3)**能学到东西**:不再是增删改查的项目,而是包含了大量的实际业务场景、系统设计优化、企业级解决方案。 4)**教程资料少**:市面上虽然有 AI 应用平台,但几乎没有从 0 到 1 的实战教程,鱼皮将提供细致入微的讲解。 5)**增加竞争力**:区别于各种管理平台项目,本项目涉及响应式编程、分库分表、设计模式、性能优化、多角度系统优化、产品优化的实战,给你的简历增加竞争力。 ### 项目收获 鱼皮给大家讲的都是 **通用的项目开发方法和架构设计套路**,从这个项目中你可以学到: - 如何拆解复杂业务,从 0 开始设计实现系统? - 如何快速开发小程序、响应式网站和后端项目? - 如何自己制作一套 Vue 3 万用前端模板? - 如何巧用 JSON 实现复杂评分策略? - 如何巧妙利用设计模式来优化代码? - 如何利用 AI 工具 `CodeGeeX 智能编程助手` 提高开发效率? - 如何利用 SSE 技术实时推送通知? - 如何利用 Redis + Caffeine + 分布式锁实现稳定高效的缓存? - 如何通过 RxJava 反应式编程 + 分库分表提升服务性能? - 如何通过幂等设计、线程池隔离提升系统安全稳定性? 此外,还能学会很多思考问题、对比方案、产品设计的方法,提升排查问题、自主解决 Bug、产品理解的能力,成为一个项目负责人。 ### 鱼皮系列项目优势 鱼皮原创项目系列以 **实战** 为主,用 **全程直播** 的方式,**从 0 到 1** 带大家学习技术知识,并立即实践运用到项目中,做到学以致用。 此外,还提供如下服务: - 详细的直播笔记(本项目有全套文字教程) - 完整的项目源码(分节的代码,更易学习) - 答疑解惑 - 专属项目交流群 - ⭐️ 现成的简历写法(直接写满简历) - ⭐️ 项目的扩展思路(拉开和其他人的差距) - ⭐️ 项目相关面试题、题解和真实面经(提前准备,面试不懵逼) - ⭐️ 前端 + Java 后端万用项目模板(快速创建项目) 比起看网上的教程学习,鱼皮项目系列的优势: > 从学知识 => 实践项目 => 复习笔记 => 项目答疑 => 简历写法 => 面试题解的一条龙服务 从需求分析、技术选型、项目设计、项目初始化、Demo 编写、前后端开发实现、项目优化、部署上线等,每个环节我都 **从理论到实践** 给大家讲的明明白白、每个细节都不放过! 编程导航已有 **10 多套项目教程!** 每个项目的学习重点不同,几乎全都是前端 + 后端的 **全栈** 项目 。 详细请见:https://yuyuanweb.feishu.cn/wiki/SePYwTc9tipQiCktw7Uc7kujnCd ## 架构设计 ### 1、核心业务流程图 ![](https://pic.yupi.icu/1/20240604145232082.png) ### 2、时序图 ![](https://pic.yupi.icu/1/20240604145232239.png) ### 3、架构设计图 ![](https://pic.yupi.icu/1/20240604145232474.png) ## 技术选型 ### 后端 - Java Spring Boot 开发框架(万用后端模板) - 存储层:MySQL 数据库 + Redis 缓存 + 腾讯云 COS 对象存储 - MyBatis-Plus 及 MyBatis X 自动生成 - Redisson 分布式锁 - Caffeine 本地缓存 - ⭐️ 基于 ChatGLM 大模型的通用 AI 能力 - ⭐️ RxJava 响应式框架 + 线程池隔离实战 - ⭐️ SSE 服务端推送 - ⭐️ Shardingsphere 分库分表 - ⭐️ 幂等设计 + 分布式 ID 雪花算法 - ⭐️ 多种设计模式 - ⭐️ 多角度项目优化:性能、稳定性、成本优化、产品优化等 ### 前端 #### Web 网页开发 - Vue 3 - Vue-CLI 脚手架 - Axios 请求库 - Arco Design 组件库 - 前端工程化:ESLint + Prettier + TypeScript - 富文本编辑器 - QRCode.js 二维码生成 - ⭐️ Pinia 状态管理 - ⭐️ OpenAPI 前端代码生成 #### 小程序开发 - React - Taro 跨端开发框架 - Taro UI 组件库 ### 开发工具 - 前端 IDE:JetBrains WebStorm - 后端 IDE:JetBrains IDEA - [CodeGeeX 智能编程助手](https://codegeex.cn/) ## 项目大纲 ### 第一阶段:MBTI 性格测试小程序 1. 项目介绍 | 项目背景和优势 2. 项目介绍 | 核心业务流程 3. 项目介绍 | 项目功能梳理 4. 项目介绍 | 技术选型和架构设计 5. MBTI 小程序 | 性格测试应用介绍 6. MBTI 小程序 | 实现方案和评分原理 7. MBTI 小程序 | Taro + React 小程序入门 8. MBTI 小程序 | 小程序开发实战 9. MBTI 小程序 | 小程序开发常用解决方案 ### 第二阶段:Web 答题应用平台 1. 平台开发 | 需求分析 2. 平台开发 | 库表设计 3. 平台开发 | 后端初始化 4. 平台开发 | 后端基础开发 5. 平台开发 | 后端核心业务流程开发 6. 平台开发 | 前端技术选型 7. 平台开发 | 前端项目初始化 8. 平台开发 | 前端 Vue 3万用模板开发 9. 平台开发 | 前端基础页面开发(管理页面) 10. 平台开发 | 前端应用主页开发 11. 平台开发 | 前端应用详情页开发 12. 平台开发 | 前端创建模块开发 - 创建应用 13. 平台开发 | 前端创建模块开发 - 创建题目 14. 平台开发 | 前端创建模块开发 - 创建评分 15. 平台开发 | 前端答题模块开发 - 应用答题 16. 平台开发 | 前端答题模块开发 - 答题结果 17. 平台开发 | 前端答题模块开发 - 我的回答 ### 第三阶段:AI 智能答题应用平台 1. 平台智能化 | 智谱 AI 大模型介绍 2. 平台智能化 | 智谱 AI SDK 接入 3. 平台智能化 | 通用 AI 模块封装 4. 平台智能化 | AI 生成题目 - 方案设计(Prompt) 5. 平台智能化 | AI 生成题目 - 后端开发 6. 平台智能化 | AI 生成题目 - 前端开发 7. 平台智能化 | AI 智能评分 - 方案设计(Prompt) 8. 平台智能化 | AI 智能评分 - 后端开发 9. 平台智能化 | AI 智能评分 - 前端开发 10. 扩展知识 | Spring AI 11. 扩展知识 | 智谱 AI + Spring AI 整合应用 ### 第四阶段:多角度项目优化 1. 性能优化 | RxJava 响应式编程 - 核心概念 2. 性能优化 | RxJava 响应式编程 - Demo 实操 3. 性能优化 | AI 生成题目优化 - 需求分析 4. 性能优化 | AI 生成题目优化 - 前后端实时通讯(SSE 技术) 5. 性能优化 | AI 生成题目优化 - 后端开发 6. 性能优化 | AI 生成题目优化 - 前端开发 7. 性能优化 | AI 评分优化 - 需求分析 8. 性能优化 | AI 评分优化 - 方案设计(缓存设计) 9. 性能优化 | AI 评分优化 - 后端本地缓存开发 10. 性能优化 | AI 评分优化 - Redisson 解决缓存击穿 11. 性能优化 | 分库分表 - 核心概念 12. 性能优化 | 分库分表 - 技术选型 13. 性能优化 | 分库分表 - Sharding JDBC 实战 14. 系统优化 | 幂等设计 - 主流方案 15. 系统优化 | 幂等设计 - 分布式唯一 id 生成 16. 系统优化 | 幂等设计 - 后端开发 17. 系统优化 | 幂等设计 - 前端开发 18. 系统优化 | 线程池隔离 - 方案设计 19. 系统优化 | 线程池隔离 - 开发实现 20. 系统优化 | 统计分析 - 方案选型 21. 系统优化 | 统计分析 - 自定义 SQL 22. 系统优化 | 统计分析 - 后端开发 23. 系统优化 | 统计分析 - 前端可视化 24. 系统优化 | 应用分享 - 移动端扫码分享 25. 系统优化 | 应用分享 - 通用分享组件 ## 项目资料 包括: - 学习计划、视频教程、文字教程、项目源码 - 项目答疑、项目交流群、学员笔记 - 简历写法、面试题解、扩展思路 以上资料均可在编程导航网站获取:https://www.code-nav.cn/course/1790274408835506178 点击 [加入编程导航](https://yuyuanweb.feishu.cn/wiki/SDtMwjR1DituVpkz5MLc3fZLnzb) 后,可以按照帖子 https://t.zsxq.com/eJxjY 的引导认证并解锁项目资料的权限。如图: ![](https://pic.yupi.icu/1/20240604145232643.png) ## 更多项目 请见:[项目实战 - 鱼皮原创项目教程系列](https://yuyuanweb.feishu.cn/wiki/SePYwTc9tipQiCktw7Uc7kujnCd) ## 加入学习 欢迎 [点此加入编程导航](https://yuyuanweb.feishu.cn/wiki/SDtMwjR1DituVpkz5MLc3fZLnzb) ,学习大量优质原创项目,享受更多原创资料,开启你的编程起飞之旅~ ================================================ FILE: mbti-test-mini/.editorconfig ================================================ # http://editorconfig.org root = true [*] indent_style = space indent_size = 2 charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.md] trim_trailing_whitespace = false ================================================ FILE: mbti-test-mini/.eslintrc ================================================ { "extends": ["taro/react"], "rules": { "react/jsx-uses-react": "off", "react/react-in-jsx-scope": "off", "jsx-quotes": "off" } } ================================================ FILE: mbti-test-mini/babel.config.js ================================================ // babel-preset-taro 更多选项和默认值: // https://github.com/NervJS/taro/blob/next/packages/babel-preset-taro/README.md module.exports = { presets: [ ['taro', { framework: 'react', ts: true }] ] } ================================================ FILE: mbti-test-mini/config/dev.ts ================================================ module.exports = { env: { NODE_ENV: '"development"' }, defineConstants: { }, mini: {}, h5: { esnextModules: ['taro-ui'] } } ================================================ FILE: mbti-test-mini/config/index.ts ================================================ const config = { projectName: 'mbti-test-mini', date: '2024-5-7', designWidth: 750, deviceRatio: { 640: 2.34 / 2, 750: 1, 828: 1.81 / 2 }, sourceRoot: 'src', outputRoot: 'dist', plugins: [], defineConstants: { }, copy: { patterns: [ ], options: { } }, framework: 'react', compiler: 'webpack5', cache: { enable: false // Webpack 持久化缓存配置,建议开启。默认配置请参考:https://docs.taro.zone/docs/config-detail#cache }, mini: { postcss: { pxtransform: { enable: true, config: { } }, url: { enable: true, config: { limit: 1024 // 设定转换尺寸上限 } }, cssModules: { enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true config: { namingPattern: 'module', // 转换模式,取值为 global/module generateScopedName: '[name]__[local]___[hash:base64:5]' } } } }, h5: { publicPath: '/', staticDirectory: 'static', postcss: { autoprefixer: { enable: true, config: { } }, cssModules: { enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true config: { namingPattern: 'module', // 转换模式,取值为 global/module generateScopedName: '[name]__[local]___[hash:base64:5]' } } } } } module.exports = function (merge) { if (process.env.NODE_ENV === 'development') { return merge({}, config, require('./dev')) } return merge({}, config, require('./prod')) } ================================================ FILE: mbti-test-mini/config/prod.ts ================================================ module.exports = { env: { NODE_ENV: '"production"' }, defineConstants: { }, mini: {}, h5: { /** * WebpackChain 插件配置 * @docs https://github.com/neutrinojs/webpack-chain */ // webpackChain (chain) { // /** // * 如果 h5 端编译后体积过大,可以使用 webpack-bundle-analyzer 插件对打包体积进行分析。 // * @docs https://github.com/webpack-contrib/webpack-bundle-analyzer // */ // chain.plugin('analyzer') // .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, []) // /** // * 如果 h5 端首屏加载时间过长,可以使用 prerender-spa-plugin 插件预加载首页。 // * @docs https://github.com/chrisvfritz/prerender-spa-plugin // */ // const path = require('path') // const Prerender = require('prerender-spa-plugin') // const staticDir = path.join(__dirname, '..', 'dist') // chain // .plugin('prerender') // .use(new Prerender({ // staticDir, // routes: [ '/pages/index/index' ], // postProcess: (context) => ({ ...context, outputPath: path.join(staticDir, 'index.html') }) // })) // } } } ================================================ FILE: mbti-test-mini/package.json ================================================ { "name": "mbti-test-mini", "version": "1.0.0", "private": true, "description": "", "templateInfo": { "name": "taro-ui", "typescript": true, "css": "Sass", "framework": "React" }, "scripts": { "build:weapp": "taro build --type weapp", "build:swan": "taro build --type swan", "build:alipay": "taro build --type alipay", "build:tt": "taro build --type tt", "build:h5": "taro build --type h5", "build:rn": "taro build --type rn", "build:qq": "taro build --type qq", "build:jd": "taro build --type jd", "build:quickapp": "taro build --type quickapp", "dev:weapp": "npm run build:weapp -- --watch", "dev:swan": "npm run build:swan -- --watch", "dev:alipay": "npm run build:alipay -- --watch", "dev:tt": "npm run build:tt -- --watch", "dev:h5": "npm run build:h5 -- --watch", "dev:rn": "npm run build:rn -- --watch", "dev:qq": "npm run build:qq -- --watch", "dev:jd": "npm run build:jd -- --watch", "dev:quickapp": "npm run build:quickapp -- --watch" }, "browserslist": [ "last 3 versions", "Android >= 4.1", "ios >= 8" ], "author": "", "dependencies": { "@babel/runtime": "^7.7.7", "@tarojs/components": "3.6.28", "@tarojs/helper": "3.6.28", "@tarojs/plugin-platform-weapp": "3.6.28", "@tarojs/plugin-platform-alipay": "3.6.28", "@tarojs/plugin-platform-tt": "3.6.28", "@tarojs/plugin-platform-swan": "3.6.28", "@tarojs/plugin-platform-jd": "3.6.28", "@tarojs/plugin-platform-qq": "3.6.28", "@tarojs/plugin-platform-h5": "3.6.28", "@tarojs/runtime": "3.6.28", "@tarojs/shared": "3.6.28", "@tarojs/taro": "3.6.28", "@tarojs/plugin-framework-react": "3.6.28", "lodash": "4.17.15", "taro-ui": "^3.2.1", "@tarojs/react": "3.6.28", "react": "^18.0.0", "react-dom": "^18.0.0" }, "devDependencies": { "@babel/core": "^7.8.0", "@tarojs/cli": "3.6.28", "postcss": "^8.4.18", "webpack": "^5.78.0", "@tarojs/taro-loader": "3.6.28", "@tarojs/webpack5-runner": "3.6.28", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.5", "react-refresh": "^0.11.0", "@types/webpack-env": "^1.13.6", "babel-preset-taro": "3.6.28", "eslint": "^8.12.0", "eslint-config-taro": "3.6.28", "stylelint": "9.3.0", "@typescript-eslint/parser": "^5.20.0", "@typescript-eslint/eslint-plugin": "^5.20.0", "typescript": "^4.1.0", "@types/react": "^18.0.0", "eslint-plugin-react": "^7.8.2", "eslint-plugin-import": "^2.12.0", "eslint-plugin-react-hooks": "^4.2.0", "ts-node": "^10.9.1", "@types/node": "^18.15.11" } } ================================================ FILE: mbti-test-mini/project.config.json ================================================ { "miniprogramRoot": "dist/", "projectname": "mbti-test-mini", "description": "", "appid": "wx370d9458c983e21d", "setting": { "urlCheck": true, "es6": false, "enhance": false, "compileHotReLoad": false, "postcss": false, "preloadBackgroundData": false, "minified": false, "newFeature": true, "autoAudits": false, "coverView": true, "showShadowRootInWxmlPanel": false, "scopeDataCheck": false, "useCompilerModule": false, "babelSetting": { "ignore": [], "disablePlugins": [], "outputPath": "" } }, "compileType": "miniprogram", "simulatorType": "wechat", "simulatorPluginLibVersion": {}, "condition": {}, "libVersion": "3.4.3", "srcMiniprogramRoot": "dist/", "packOptions": { "ignore": [], "include": [] }, "editorSetting": { "tabIndent": "insertSpaces", "tabSize": 2 } } ================================================ FILE: mbti-test-mini/project.private.config.json ================================================ { "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html", "projectname": "mbti-test-mini", "setting": { "compileHotReLoad": true } } ================================================ FILE: mbti-test-mini/project.tt.json ================================================ { "miniprogramRoot": "./", "projectname": "mbti-test-mini", "description": "", "appid": "touristappid", "setting": { "urlCheck": true, "es6": false, "postcss": false, "minified": false }, "compileType": "miniprogram" } ================================================ FILE: mbti-test-mini/src/app.config.ts ================================================ export default defineAppConfig({ pages: ["pages/index/index", "pages/result/index", "pages/doQuestion/index"], window: { backgroundTextStyle: "light", navigationBarBackgroundColor: "#fff", navigationBarTitleText: "鱼皮 MBTI 性格测试", navigationBarTextStyle: "black", }, }); ================================================ FILE: mbti-test-mini/src/app.scss ================================================ .at-button--primary { background: #806497; border-color: #806497; } ================================================ FILE: mbti-test-mini/src/app.ts ================================================ import Taro, { useLaunch } from "@tarojs/taro"; import { PropsWithChildren } from "react"; import "taro-ui/dist/style/index.scss"; // 引入组件样式 - 方式一 import "./app.scss"; function App({ children }: PropsWithChildren) { useLaunch(async () => { const res = await Taro.login(); console.log(res); // todo 拿到 res.code 后,调用后端登录 }); return children; } export default App; ================================================ FILE: mbti-test-mini/src/components/GlobalFooter/index.scss ================================================ .globalFooter { position: fixed; bottom: 16px; left: 0; right: 0; text-align: center; } ================================================ FILE: mbti-test-mini/src/components/GlobalFooter/index.tsx ================================================ import { View } from "@tarojs/components"; import "./index.scss"; /** * 全局底部栏组件 * @author 程序员鱼皮 * @from 编程导航学习圈 */ export default () => { return ( 作者:程序员鱼皮 ); }; ================================================ FILE: mbti-test-mini/src/data/question_results.json ================================================ [ { "resultProp": [ "I", "S", "T", "J" ], "resultDesc": "忠诚可靠,被公认为务实,注重细节。", "resultPicture": "icon_url_istj", "resultName": "ISTJ(物流师)" }, { "resultProp": [ "I", "S", "F", "J" ], "resultDesc": "善良贴心,以同情心和责任为特点。", "resultPicture": "icon_url_isfj", "resultName": "ISFJ(守护者)" }, { "resultProp": [ "I", "N", "F", "J" ], "resultDesc": "理想主义者,有着深刻的洞察力,善于理解他人。", "resultPicture": "icon_url_infj", "resultName": "INFJ(占有者)" }, { "resultProp": [ "I", "N", "T", "J" ], "resultDesc": "独立思考者,善于规划和实现目标,理性而果断。", "resultPicture": "icon_url_intj", "resultName": "INTJ(设计师)" }, { "resultProp": [ "I", "S", "T", "P" ], "resultDesc": "冷静自持,善于解决问题,擅长实践技能。", "resultPicture": "icon_url_istp", "resultName": "ISTP(运动员)" }, { "resultProp": [ "I", "S", "F", "P" ], "resultDesc": "具有艺术感和敏感性,珍视个人空间和自由。", "resultPicture": "icon_url_isfp", "resultName": "ISFP(艺术家)" }, { "resultProp": [ "I", "N", "F", "P" ], "resultDesc": "理想主义者,富有创造力,以同情心和理解他人著称。", "resultPicture": "icon_url_infp", "resultName": "INFP(治愈者)" }, { "resultProp": [ "I", "N", "T", "P" ], "resultDesc": "思维清晰,探索精神,独立思考且理性。", "resultPicture": "icon_url_intp", "resultName": "INTP(学者)" }, { "resultProp": [ "E", "S", "T", "P" ], "resultDesc": "敢于冒险,乐于冒险,思维敏捷,行动果断。", "resultPicture": "icon_url_estp", "resultName": "ESTP(拓荒者)" }, { "resultProp": [ "E", "S", "F", "P" ], "resultDesc": "热情开朗,善于社交,热爱生活,乐于助人。", "resultPicture": "icon_url_esfp", "resultName": "ESFP(表演者)" }, { "resultProp": [ "E", "N", "F", "P" ], "resultDesc": "富有想象力,充满热情,善于激发他人的活力和潜力。", "resultPicture": "icon_url_enfp", "resultName": "ENFP(倡导者)" }, { "resultProp": [ "E", "N", "T", "P" ], "resultDesc": "充满创造力,善于辩论,挑战传统,喜欢探索新领域。", "resultPicture": "icon_url_entp", "resultName": "ENTP(发明家)" }, { "resultProp": [ "E", "S", "T", "J" ], "resultDesc": "务实果断,善于组织和管理,重视效率和目标。", "resultPicture": "icon_url_estj", "resultName": "ESTJ(主管)" }, { "resultProp": [ "E", "S", "F", "J" ], "resultDesc": "友善热心,以协调、耐心和关怀为特点,善于团队合作。", "resultPicture": "icon_url_esfj", "resultName": "ESFJ(尽责者)" }, { "resultProp": [ "E", "N", "F", "J" ], "resultDesc": "热情关爱,善于帮助他人,具有领导力和社交能力。", "resultPicture": "icon_url_enfj", "resultName": "ENFJ(教导着)" }, { "resultProp": [ "E", "N", "T", "J" ], "resultDesc": "果断自信,具有领导才能,善于规划和执行目标。", "resultPicture": "icon_url_entj", "resultName": "ENTJ(统帅)" } ] ================================================ FILE: mbti-test-mini/src/data/questions.json ================================================ [ { "options": [ { "result": "I", "value": "独自工作", "key": "A" }, { "result": "E", "value": "与他人合作", "key": "B" } ], "title": "你通常更喜欢" }, { "options": [ { "result": "J", "value": "喜欢有明确的计划", "key": "A" }, { "result": "P", "value": "更愿意随机应变", "key": "B" } ], "title": "当安排活动时" }, { "options": [ { "result": "T", "value": "认为应该严格遵守", "key": "A" }, { "result": "F", "value": "认为应灵活运用", "key": "B" } ], "title": "你如何看待规则" }, { "options": [ { "result": "E", "value": "经常是说话的人", "key": "A" }, { "result": "I", "value": "更倾向于倾听", "key": "B" } ], "title": "在社交场合中" }, { "options": [ { "result": "J", "value": "先研究再行动", "key": "A" }, { "result": "P", "value": "边做边学习", "key": "B" } ], "title": "面对新的挑战" }, { "options": [ { "result": "S", "value": "注重细节和事实", "key": "A" }, { "result": "N", "value": "注重概念和想象", "key": "B" } ], "title": "在日常生活中" }, { "options": [ { "result": "T", "value": "更多基于逻辑分析", "key": "A" }, { "result": "F", "value": "更多基于个人情感", "key": "B" } ], "title": "做决定时" }, { "options": [ { "result": "S", "value": "喜欢有结构和常规", "key": "A" }, { "result": "N", "value": "喜欢自由和灵活性", "key": "B" } ], "title": "对于日常安排" }, { "options": [ { "result": "P", "value": "首先考虑可能性", "key": "A" }, { "result": "J", "value": "首先考虑后果", "key": "B" } ], "title": "当遇到问题时" }, { "options": [ { "result": "T", "value": "时间是一种宝贵的资源", "key": "A" }, { "result": "F", "value": "时间是相对灵活的概念", "key": "B" } ], "title": "你如何看待时间" } ] ================================================ FILE: mbti-test-mini/src/index.html ================================================ mbti-test-mini
================================================ FILE: mbti-test-mini/src/pages/doQuestion/index.config.ts ================================================ export default definePageConfig({ // navigationBarTitleText: '' }) ================================================ FILE: mbti-test-mini/src/pages/doQuestion/index.scss ================================================ .doQuestionPage { .title { margin-bottom: 48px; } .options-wrapper { margin-bottom: 48px; } .controlBtn { margin: 24px 48px; } } ================================================ FILE: mbti-test-mini/src/pages/doQuestion/index.tsx ================================================ import {View} from "@tarojs/components"; import Taro from "@tarojs/taro"; import {AtButton, AtRadio} from "taro-ui"; import {useEffect, useState} from "react"; import GlobalFooter from "../../components/GlobalFooter"; import questions from "../../data/questions.json"; import "./index.scss"; /** * 做题页面 * @author 程序员鱼皮 * @from 编程导航学习圈 */ export default () => { // 当前题目序号(从 1 开始) const [current, setCurrent] = useState(1); // 当前题目 const [currentQuestion, setCurrentQuestion] = useState(questions[0]); const questionOptions = currentQuestion.options.map((option) => { return {label: `${option.key}. ${option.value}`, value: option.key}; }); // 当前答案 const [currentAnswer, setCurrentAnswer] = useState(); // 回答列表 const [answerList] = useState([]); // 序号变化时,切换当前题目和当前回答 useEffect(() => { setCurrentQuestion(questions[current - 1]); setCurrentAnswer(answerList[current - 1]); }, [current]); return ( {current}、{currentQuestion.title} { setCurrentAnswer(value); // 记录回答 answerList[current - 1] = value; }} /> {current < questions.length && ( setCurrent(current + 1)} > 下一题 )} {current == questions.length && ( { // 传递答案 Taro.setStorageSync("answerList", answerList); // 跳转到结果页面 Taro.navigateTo({ url: "/pages/result/index", }); }} > 查看结果 )} {current > 1 && ( setCurrent(current - 1)} > 上一题 )} ); }; ================================================ FILE: mbti-test-mini/src/pages/index/index.config.ts ================================================ export default definePageConfig({ // navigationBarTitleText: '' }) ================================================ FILE: mbti-test-mini/src/pages/index/index.scss ================================================ .indexPage { background: #A2C7D7; .title { color: white; padding-top: 48px; text-align: center; } .subTitle { color: white; margin-bottom: 48px; } .enterBtn { width: 60vw; } } ================================================ FILE: mbti-test-mini/src/pages/index/index.tsx ================================================ import {View, Image} from "@tarojs/components"; import {AtButton} from "taro-ui"; import Taro from "@tarojs/taro"; import headerBg from "../../assets/headerBg.jpg"; import GlobalFooter from "../../components/GlobalFooter"; import "./index.scss"; /** * 主页 * @author 程序员鱼皮 * @from 编程导航学习圈 */ export default () => { return ( MBTI 性格测试 只需 2 分钟,就能非常准确地描述出你是谁,以及你的性格特点 { Taro.navigateTo({ url: "/pages/doQuestion/index", }); }} > 开始测试 ); }; ================================================ FILE: mbti-test-mini/src/pages/result/index.config.ts ================================================ export default definePageConfig({ navigationBarTitleText: '查看结果' }) ================================================ FILE: mbti-test-mini/src/pages/result/index.scss ================================================ .resultPage { background: #A2C7D7; .title { color: white; padding-top: 48px; text-align: center; } .subTitle { color: white; margin-bottom: 48px; } .enterBtn { width: 60vw; } } ================================================ FILE: mbti-test-mini/src/pages/result/index.tsx ================================================ import {View, Image} from "@tarojs/components"; import {AtButton} from "taro-ui"; import Taro from "@tarojs/taro"; import headerBg from "../../assets/headerBg.jpg"; import GlobalFooter from "../../components/GlobalFooter"; import {getBestQuestionResult} from "../../utils/bizUtils"; import questions from "../../data/questions.json"; import questionResults from "../../data/question_results.json"; import "./index.scss"; /** * 测试结果页面 * @author 程序员鱼皮 * @from 编程导航学习圈 */ export default () => { // 获取答案 const answerList = Taro.getStorageSync("answerList"); if (!answerList || answerList.length < 1) { Taro.showToast({ title: "答案为空", icon: "error", duration: 3000, }); } // 获取测试结果 const result = getBestQuestionResult(answerList, questions, questionResults); return ( {result.resultName} {result.resultDesc} { Taro.reLaunch({ url: "/pages/index/index", }); }} > 返回主页 ); }; ================================================ FILE: mbti-test-mini/src/utils/bizUtils.ts ================================================ /** * 获取最佳题目评分结果 * @param answerList * @param questions * @param question_results */ export function getBestQuestionResult(answerList, questions, question_results) { // 初始化一个对象,用于存储每个选项的计数 const optionCount = {}; // 用户选择 A, B, C // 对应 result:I, I, J // optionCount[I] = 2; optionCount[J] = 1 // 遍历题目列表 for (const question of questions) { // 遍历答案列表 for (const answer of answerList) { // 遍历题目中的选项 for (const option of question.options) { // 如果答案和选项的key匹配 if (option.key === answer) { // 获取选项的result属性 const result = option.result; // 如果result属性不在optionCount中,初始化为0 if (!optionCount[result]) { optionCount[result] = 0; } // 在optionCount中增加计数 optionCount[result]++; } } } } // 初始化最高分数和最高分数对应的评分结果 let maxScore = 0; let maxScoreResult = question_results[0]; // 遍历评分结果列表 for (const result of question_results) { // 计算当前评分结果的分数 const score = result.resultProp.reduce((count, prop) => { return count + (optionCount[prop] || 0); }, 0); // 如果分数高于当前最高分数,更新最高分数和最高分数对应的评分结果 if (score > maxScore) { maxScore = score; maxScoreResult = result; } } // 返回最高分数和最高分数对应的评分结果 return maxScoreResult; } // 示例数据 const answerList = ["B","B","B","A"]; const questions = [ { title: "你通常更喜欢", options: [ { result: "I", value: "独自工作", key: "A", }, { result: "E", value: "与他人合作", key: "B", }, ], }, { options: [ { result: "S", value: "喜欢有结构和常规", key: "A", }, { result: "N", value: "喜欢自由和灵活性", key: "B", }, ], title: "对于日常安排", }, { options: [ { result: "P", value: "首先考虑可能性", key: "A", }, { result: "J", value: "首先考虑后果", key: "B", }, ], title: "当遇到问题时", }, { options: [ { result: "T", value: "时间是一种宝贵的资源", key: "A", }, { result: "F", value: "时间是相对灵活的概念", key: "B", }, ], title: "你如何看待时间", }, ]; const question_results = [ { resultProp: ["I", "S", "T", "J"], resultDesc: "忠诚可靠,被公认为务实,注重细节。", resultPicture: "icon_url_istj", resultName: "ISTJ(物流师)", }, { resultProp: ["I", "S", "F", "J"], resultDesc: "善良贴心,以同情心和责任为特点。", resultPicture: "icon_url_isfj", resultName: "ISFJ(守护者)", }, ]; console.log(getBestQuestionResult(answerList, questions, question_results)); ================================================ FILE: mbti-test-mini/tsconfig.json ================================================ { "compilerOptions": { "target": "es2017", "module": "commonjs", "removeComments": false, "preserveConstEnums": true, "moduleResolution": "node", "experimentalDecorators": true, "noImplicitAny": false, "allowSyntheticDefaultImports": true, "outDir": "lib", "noUnusedLocals": true, "noUnusedParameters": true, "strictNullChecks": true, "sourceMap": true, "baseUrl": ".", "rootDir": ".", "jsx": "react-jsx", "allowJs": true, "resolveJsonModule": true, "typeRoots": [ "node_modules/@types" ] }, "include": ["./src", "./types"], "compileOnSave": false } ================================================ FILE: mbti-test-mini/types/custom.d.ts ================================================ interface QuestionOption { result: T; value: string; key: T; } interface Question { title: string; options: QuestionOption[]; } ================================================ FILE: mbti-test-mini/types/global.d.ts ================================================ /// declare module '*.png'; declare module '*.gif'; declare module '*.jpg'; declare module '*.jpeg'; declare module '*.svg'; declare module '*.css'; declare module '*.less'; declare module '*.scss'; declare module '*.sass'; declare module '*.styl'; declare namespace NodeJS { interface ProcessEnv { TARO_ENV: 'weapp' | 'swan' | 'alipay' | 'h5' | 'rn' | 'tt' | 'quickapp' | 'qq' | 'jd' } } ================================================ FILE: yudada-backend/.gitignore ================================================ ### @author 程序员鱼皮 ### ### @from 编程导航知识星球 ### 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/ ### Java template # Compiled class file *.class # Log file *.log # BlueJ files *.ctxt # Mobile Tools for Java (J2ME) .mtj.tmp/ # Package Files # *.jar *.war *.nar *.ear *.zip *.tar.gz *.rar # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* ### Maven template target/ pom.xml.tag pom.xml.releaseBackup pom.xml.versionsBackup pom.xml.next release.properties dependency-reduced-pom.xml buildNumber.properties .mvn/timing.properties # https://github.com/takari/maven-wrapper#usage-without-binary-jar .mvn/wrapper/maven-wrapper.jar ### JetBrains template # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf # Generated files .idea/**/contentModel.xml # Sensitive or high-churn files .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml .idea/**/dbnavigator.xml # Gradle .idea/**/gradle.xml .idea/**/libraries # Gradle and Maven with auto-import # When using Gradle or Maven with auto-import, you should exclude module files, # since they will be recreated, and may cause churn. Uncomment if using # auto-import. # .idea/artifacts # .idea/compiler.xml # .idea/jarRepositories.xml # .idea/modules.xml # .idea/*.iml # .idea/modules # *.iml # *.ipr # CMake cmake-build-*/ # Mongo Explorer plugin .idea/**/mongoSettings.xml # File-based project format *.iws # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties # Editor-based Rest Client .idea/httpRequests # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser ================================================ FILE: yudada-backend/.mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.1/apache-maven-3.8.1-bin.zip wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar ================================================ FILE: yudada-backend/Dockerfile ================================================ # Docker 镜像构建 # @author 程序员鱼皮 # @from 编程导航知识星球 # 选择基础镜像 FROM maven:3.8.1-jdk-8-slim as builder # 解决容器时期与真实时间相差 8 小时的问题 RUN ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo Asia/Shanghai > /etc/timezone # 复制代码到容器内 WORKDIR /app COPY pom.xml . COPY src ./src # 打包构建 RUN mvn package -DskipTests # 容器启动时运行 jar 包 CMD ["java","-jar","/app/target/yudada-backend-0.0.1-SNAPSHOT.jar","--spring.profiles.active=prod"] ================================================ FILE: yudada-backend/README.md ================================================ # SpringBoot 项目初始模板 > 作者:[程序员鱼皮](https://github.com/liyupi) > 仅分享于 [编程导航知识星球](https://yupi.icu) 基于 Java SpringBoot 的项目初始模板,整合了常用框架和主流业务的示例代码。 只需 1 分钟即可完成内容网站的后端!!!大家还可以在此基础上快速开发自己的项目。 [toc] ## 模板特点 ### 主流框架 & 特性 - Spring Boot 2.7.x(贼新) - Spring MVC - MyBatis + MyBatis Plus 数据访问(开启分页) - Spring Boot 调试工具和项目处理器 - Spring AOP 切面编程 - Spring Scheduler 定时任务 - Spring 事务注解 ### 数据存储 - MySQL 数据库 - Redis 内存数据库 - Elasticsearch 搜索引擎 - 腾讯云 COS 对象存储 ### 工具类 - Easy Excel 表格处理 - Hutool 工具库 - Apache Commons Lang3 工具类 - Lombok 注解 ### 业务特性 - 业务代码生成器(支持自动生成 Service、Controller、数据模型代码) - Spring Session Redis 分布式登录 - 全局请求响应拦截器(记录日志) - 全局异常处理器 - 自定义错误码 - 封装通用响应类 - Swagger + Knife4j 接口文档 - 自定义权限注解 + 全局校验 - 全局跨域处理 - 长整数丢失精度解决 - 多环境配置 ## 业务功能 - 提供示例 SQL(用户、帖子、帖子点赞、帖子收藏表) - 用户登录、注册、注销、更新、检索、权限管理 - 帖子创建、删除、编辑、更新、数据库检索、ES 灵活检索 - 帖子点赞、取消点赞 - 帖子收藏、取消收藏、检索已收藏帖子 - 帖子全量同步 ES、增量同步 ES 定时任务 - 支持微信开放平台登录 - 支持微信公众号订阅、收发消息、设置菜单 - 支持分业务的文件上传 ### 单元测试 - JUnit5 单元测试 - 示例单元测试类 ### 架构设计 - 合理分层 ## 快速上手 > 所有需要修改的地方鱼皮都标记了 `todo`,便于大家找到修改的位置~ ### MySQL 数据库 1)修改 `application.yml` 的数据库配置为你自己的: ```yml spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/yudada username: root password: 123456 ``` 2)执行 `sql/create_table.sql` 中的数据库语句,自动创建库表 3)启动项目,访问 `http://localhost:8101/api/doc.html` 即可打开接口文档,不需要写前端就能在线调试接口了~ ![](doc/swagger.png) ### Redis 分布式登录 1)修改 `application.yml` 的 Redis 配置为你自己的: ```yml spring: redis: database: 1 host: localhost port: 6379 timeout: 5000 password: 123456 ``` 2)修改 `application.yml` 中的 session 存储方式: ```yml spring: session: store-type: redis ``` 3)移除 `MainApplication` 类开头 `@SpringBootApplication` 注解内的 exclude 参数: 修改前: ```java @SpringBootApplication(exclude = {RedisAutoConfiguration.class}) ``` 修改后: ```java @SpringBootApplication ``` ### Elasticsearch 搜索引擎 1)修改 `application.yml` 的 Elasticsearch 配置为你自己的: ```yml spring: elasticsearch: uris: http://localhost:9200 username: root password: 123456 ``` 2)复制 `sql/post_es_mapping.json` 文件中的内容,通过调用 Elasticsearch 的接口或者 Kibana Dev Tools 来创建索引(相当于数据库建表) ``` PUT post_v1 { 参数见 sql/post_es_mapping.json 文件 } ``` 这步不会操作的话需要补充下 Elasticsearch 的知识,或者自行百度一下~ 3)开启同步任务,将数据库的帖子同步到 Elasticsearch 找到 job 目录下的 `FullSyncPostToEs` 和 `IncSyncPostToEs` 文件,取消掉 `@Component` 注解的注释,再次执行程序即可触发同步: ```java // todo 取消注释开启任务 //@Component ``` ### 业务代码生成器 支持自动生成 Service、Controller、数据模型代码,配合 MyBatisX 插件,可以快速开发增删改查等实用基础功能。 找到 `generate.CodeGenerator` 类,修改生成参数和生成路径,并且支持注释掉不需要的生成逻辑,然后运行即可。 ``` // 指定生成参数 String packageName = "com.yupi.yudada"; String dataName = "用户评论"; String dataKey = "userComment"; String upperDataKey = "UserComment"; ``` 生成代码后,可以移动到实际项目中,并且按照 `// todo` 注释的提示来针对自己的业务需求进行修改。 ================================================ FILE: yudada-backend/mvnw ================================================ #!/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 "$@" ================================================ FILE: yudada-backend/mvnw.cmd ================================================ @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% ================================================ FILE: yudada-backend/pom.xml ================================================ 4.0.0 org.springframework.boot spring-boot-starter-parent 2.7.2 com.yupi yudada-backend 0.0.1-SNAPSHOT yudada-backend 1.8 org.springframework.boot spring-boot-starter-freemarker org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop org.mybatis.spring.boot mybatis-spring-boot-starter 2.2.2 com.baomidou mybatis-plus-boot-starter 3.5.2 org.springframework.boot spring-boot-starter-data-redis org.springframework.session spring-session-data-redis org.redisson redisson 3.21.0 com.github.ben-manes.caffeine caffeine 2.9.2 org.apache.shardingsphere shardingsphere-jdbc-core-spring-boot-starter 5.2.0 com.github.xiaoymin knife4j-openapi2-spring-boot-starter 4.4.0 com.qcloud cos_api 5.6.89 cn.bigmodel.openapi oapi-java-sdk release-V4-2.0.2 org.apache.commons commons-lang3 com.alibaba easyexcel 3.1.1 cn.hutool hutool-all 5.8.8 org.springframework.boot spring-boot-devtools runtime true mysql mysql-connector-java runtime org.springframework.boot spring-boot-configuration-processor true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok ================================================ FILE: yudada-backend/sql/create_table.sql ================================================ # 数据库初始化 # @author 程序员鱼皮 # @from 编程导航知识星球 -- 创建库 create database if not exists yudada; -- 切换库 use yudada; -- 用户表 create table if not exists user ( id bigint auto_increment comment 'id' primary key, userAccount varchar(256) not null comment '账号', userPassword varchar(512) not null comment '密码', unionId varchar(256) null comment '微信开放平台id', mpOpenId varchar(256) null comment '公众号openId', userName varchar(256) null comment '用户昵称', userAvatar varchar(1024) null comment '用户头像', userProfile varchar(512) null comment '用户简介', userRole varchar(256) default 'user' not null comment '用户角色:user/admin/ban', createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间', updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', isDelete tinyint default 0 not null comment '是否删除', index idx_unionId (unionId) ) comment '用户' collate = utf8mb4_unicode_ci; -- 应用表 create table if not exists app ( id bigint auto_increment comment 'id' primary key, appName varchar(128) not null comment '应用名', appDesc varchar(2048) null comment '应用描述', appIcon varchar(1024) null comment '应用图标', appType tinyint default 0 not null comment '应用类型(0-得分类,1-测评类)', scoringStrategy tinyint default 0 not null comment '评分策略(0-自定义,1-AI)', reviewStatus int default 0 not null comment '审核状态:0-待审核, 1-通过, 2-拒绝', reviewMessage varchar(512) null comment '审核信息', reviewerId bigint null comment '审核人 id', reviewTime datetime null comment '审核时间', userId bigint not null comment '创建用户 id', createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间', updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', isDelete tinyint default 0 not null comment '是否删除', index idx_appName (appName) ) comment '应用' collate = utf8mb4_unicode_ci; -- 题目表 create table if not exists question ( id bigint auto_increment comment 'id' primary key, questionContent text null comment '题目内容(json格式)', appId bigint not null comment '应用 id', userId bigint not null comment '创建用户 id', createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间', updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', isDelete tinyint default 0 not null comment '是否删除', index idx_appId (appId) ) comment '题目' collate = utf8mb4_unicode_ci; -- 评分结果表 create table if not exists scoring_result ( id bigint auto_increment comment 'id' primary key, resultName varchar(128) not null comment '结果名称,如物流师', resultDesc text null comment '结果描述', resultPicture varchar(1024) null comment '结果图片', resultProp varchar(128) null comment '结果属性集合 JSON,如 [I,S,T,J]', resultScoreRange int null comment '结果得分范围,如 80,表示 80及以上的分数命中此结果', appId bigint not null comment '应用 id', userId bigint not null comment '创建用户 id', createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间', updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', isDelete tinyint default 0 not null comment '是否删除', index idx_appId (appId) ) comment '评分结果' collate = utf8mb4_unicode_ci; -- 用户答题记录表 create table if not exists user_answer ( id bigint auto_increment primary key, appId bigint not null comment '应用 id', appType tinyint default 0 not null comment '应用类型(0-得分类,1-角色测评类)', scoringStrategy tinyint default 0 not null comment '评分策略(0-自定义,1-AI)', choices text null comment '用户答案(JSON 数组)', resultId bigint null comment '评分结果 id', resultName varchar(128) null comment '结果名称,如物流师', resultDesc text null comment '结果描述', resultPicture varchar(1024) null comment '结果图标', resultScore int null comment '得分', userId bigint not null comment '用户 id', createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间', updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', isDelete tinyint default 0 not null comment '是否删除', index idx_appId (appId), index idx_userId (userId) ) comment '用户答题记录' collate = utf8mb4_unicode_ci; -- 用户答题记录表(分表 0) create table if not exists user_answer_0 ( id bigint auto_increment primary key, appId bigint not null comment '应用 id', appType tinyint default 0 not null comment '应用类型(0-得分类,1-角色测评类)', scoringStrategy tinyint default 0 not null comment '评分策略(0-自定义,1-AI)', choices text null comment '用户答案(JSON 数组)', resultId bigint null comment '评分结果 id', resultName varchar(128) null comment '结果名称,如物流师', resultDesc text null comment '结果描述', resultPicture varchar(1024) null comment '结果图标', resultScore int null comment '得分', userId bigint not null comment '用户 id', createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间', updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', isDelete tinyint default 0 not null comment '是否删除', index idx_appId (appId), index idx_userId (userId) ) comment '用户答题记录分表 0' collate = utf8mb4_unicode_ci; -- 用户答题记录表(分表 1) create table if not exists user_answer_1 ( id bigint auto_increment primary key, appId bigint not null comment '应用 id', appType tinyint default 0 not null comment '应用类型(0-得分类,1-角色测评类)', scoringStrategy tinyint default 0 not null comment '评分策略(0-自定义,1-AI)', choices text null comment '用户答案(JSON 数组)', resultId bigint null comment '评分结果 id', resultName varchar(128) null comment '结果名称,如物流师', resultDesc text null comment '结果描述', resultPicture varchar(1024) null comment '结果图标', resultScore int null comment '得分', userId bigint not null comment '用户 id', createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间', updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', isDelete tinyint default 0 not null comment '是否删除', index idx_appId (appId), index idx_userId (userId) ) comment '用户答题记录分表 1' collate = utf8mb4_unicode_ci; ================================================ FILE: yudada-backend/sql/init_data.sql ================================================ -- 切换库 use yudada; -- 用户表初始数据 INSERT INTO user (id, userAccount, userPassword, unionId, mpOpenId, userName, userAvatar, userProfile, userRole, createTime, updateTime, isDelete) VALUES (1, 'yupi', 'b0dd3697a192885d7c055db46155b26a', null, null, '鱼皮', 'https://k.sinaimg.cn/n/sinakd20110/560/w1080h1080/20230930/915d-f3d7b580c33632b191e19afa0a858d31.jpg/w700d1q75cms.jpg', '欢迎来编程导航学习', 'admin', '2024-05-09 11:13:13', '2024-05-09 15:07:48', 0); -- 应用表初始数据 INSERT INTO app (id, appName, appDesc, appIcon, appType, scoringStrategy, reviewStatus, reviewMessage, reviewerId, reviewTime, userId, createTime, updateTime, isDelete) VALUES (1, '自定义MBTI性格测试', '测试性格', '11', 1, 0, 1, null, null, null, 1, '2024-04-24 15:58:05', '2024-05-09 15:09:53', 0); INSERT INTO app (id, appName, appDesc, appIcon, appType, scoringStrategy, reviewStatus, reviewMessage, reviewerId, reviewTime, userId, createTime, updateTime, isDelete) VALUES (2, '自定义得分测试', '测试得分', '22', 0, 0, 1, null, null, null, 1, '2024-04-25 11:39:30', '2024-05-09 15:09:53', 0); INSERT INTO app (id, appName, appDesc, appIcon, appType, scoringStrategy, reviewStatus, reviewMessage, reviewerId, reviewTime, userId, createTime, updateTime, isDelete) VALUES (3, 'AI MBTI 性格测试', '快来测测你的 MBTI', '11', 1, 1, 1, null, null, null, 1, '2024-04-26 16:38:12', '2024-05-09 15:09:53', 0); INSERT INTO app (id, appName, appDesc, appIcon, appType, scoringStrategy, reviewStatus, reviewMessage, reviewerId, reviewTime, userId, createTime, updateTime, isDelete) VALUES (4, 'AI 得分测试', '看看你熟悉多少首都', '22', 0, 1, 1, null, null, null, 1, '2024-04-26 16:38:56', '2024-05-09 15:09:53', 0); -- 题目表初始数据 INSERT INTO question (id, questionContent, appId, userId, createTime, updateTime, isDelete) VALUES (1, '[{"options":[{"result":"I","value":"独自工作","key":"A"},{"result":"E","value":"与他人合作","key":"B"}],"title":"1. 你通常更喜欢"},{"options":[{"result":"J","value":"喜欢有明确的计划","key":"A"},{"result":"P","value":"更愿意随机应变","key":"B"}],"title":"2. 当安排活动时"},{"options":[{"result":"T","value":"认为应该严格遵守","key":"A"},{"result":"F","value":"认为应灵活运用","key":"B"}],"title":"3. 你如何看待规则"},{"options":[{"result":"E","value":"经常是说话的人","key":"A"},{"result":"I","value":"更倾向于倾听","key":"B"}],"title":"4. 在社交场合中"},{"options":[{"result":"J","value":"先研究再行动","key":"A"},{"result":"P","value":"边做边学习","key":"B"}],"title":"5. 面对新的挑战"},{"options":[{"result":"S","value":"注重细节和事实","key":"A"},{"result":"N","value":"注重概念和想象","key":"B"}],"title":"6. 在日常生活中"},{"options":[{"result":"T","value":"更多基于逻辑分析","key":"A"},{"result":"F","value":"更多基于个人情感","key":"B"}],"title":"7. 做决定时"},{"options":[{"result":"S","value":"喜欢有结构和常规","key":"A"},{"result":"N","value":"喜欢自由和灵活性","key":"B"}],"title":"8. 对于日常安排"},{"options":[{"result":"P","value":"首先考虑可能性","key":"A"},{"result":"J","value":"首先考虑后果","key":"B"}],"title":"9. 当遇到问题时"},{"options":[{"result":"T","value":"时间是一种宝贵的资源","key":"A"},{"result":"F","value":"时间是相对灵活的概念","key":"B"}],"title":"10. 你如何看待时间"}]', 1, 1, '2024-04-24 16:41:53', '2024-05-09 12:28:58', 0); INSERT INTO question (id, questionContent, appId, userId, createTime, updateTime, isDelete) VALUES (2, '[{"options":[{"score":0,"value":"利马","key":"A"},{"score":0,"value":"圣多明各","key":"B"},{"score":0,"value":"圣萨尔瓦多","key":"C"},{"score":1,"value":"波哥大","key":"D"}],"title":"哥伦比亚的首都是?"},{"options":[{"score":0,"value":"蒙特利尔","key":"A"},{"score":0,"value":"多伦多","key":"B"},{"score":1,"value":"渥太华","key":"C"},{"score":0,"value":"温哥华","key":"D"}],"title":"加拿大的首都是?"},{"options":[{"score":0,"value":"大阪","key":"A"},{"score":1,"value":"东京","key":"B"},{"score":0,"value":"京都","key":"C"},{"score":0,"value":"名古屋","key":"D"}],"title":"日本的首都是?"},{"options":[{"score":0,"value":"墨尔本","key":"A"},{"score":0,"value":"悉尼","key":"B"},{"score":0,"value":"布里斯班","key":"C"},{"score":1,"value":"堪培拉","key":"D"}],"title":"澳大利亚的首都是?"},{"options":[{"score":1,"value":"雅加达","key":"A"},{"score":0,"value":"曼谷","key":"B"},{"score":0,"value":"胡志明市","key":"C"},{"score":0,"value":"吉隆坡","key":"D"}],"title":"印度尼西亚的首都是?"},{"options":[{"score":0,"value":"上海","key":"A"},{"score":0,"value":"杭州","key":"B"},{"score":1,"value":"北京","key":"C"},{"score":0,"value":"广州","key":"D"}],"title":"中国的首都是?"},{"options":[{"score":0,"value":"汉堡","key":"A"},{"score":0,"value":"慕尼黑","key":"B"},{"score":1,"value":"柏林","key":"C"},{"score":0,"value":"科隆","key":"D"}],"title":"德国的首都是?"},{"options":[{"score":0,"value":"釜山","key":"A"},{"score":1,"value":"首尔","key":"B"},{"score":0,"value":"大田","key":"C"},{"score":0,"value":"仁川","key":"D"}],"title":"韩国的首都是?"},{"options":[{"score":0,"value":"瓜达拉哈拉","key":"A"},{"score":0,"value":"蒙特雷","key":"B"},{"score":1,"value":"墨西哥城","key":"C"},{"score":0,"value":"坎昆","key":"D"}],"title":"墨西哥的首都是?"},{"options":[{"score":1,"value":"开罗","key":"A"},{"score":0,"value":"亚历山大","key":"B"},{"score":0,"value":"卢克索","key":"C"},{"score":0,"value":"卡利乌比亚","key":"D"}],"title":"埃及的首都是?"}]', 2, 1, '2024-04-25 15:03:07', '2024-05-09 12:28:58', 0); INSERT INTO question (id, questionContent, appId, userId, createTime, updateTime, isDelete) VALUES (3, '[{"options":[{"result":"I","value":"独自工作","key":"A"},{"result":"E","value":"与他人合作","key":"B"}],"title":"1. 你通常更喜欢"},{"options":[{"result":"J","value":"喜欢有明确的计划","key":"A"},{"result":"P","value":"更愿意随机应变","key":"B"}],"title":"2. 当安排活动时"},{"options":[{"result":"T","value":"认为应该严格遵守","key":"A"},{"result":"F","value":"认为应灵活运用","key":"B"}],"title":"3. 你如何看待规则"},{"options":[{"result":"E","value":"经常是说话的人","key":"A"},{"result":"I","value":"更倾向于倾听","key":"B"}],"title":"4. 在社交场合中"},{"options":[{"result":"J","value":"先研究再行动","key":"A"},{"result":"P","value":"边做边学习","key":"B"}],"title":"5. 面对新的挑战"},{"options":[{"result":"S","value":"注重细节和事实","key":"A"},{"result":"N","value":"注重概念和想象","key":"B"}],"title":"6. 在日常生活中"},{"options":[{"result":"T","value":"更多基于逻辑分析","key":"A"},{"result":"F","value":"更多基于个人情感","key":"B"}],"title":"7. 做决定时"},{"options":[{"result":"S","value":"喜欢有结构和常规","key":"A"},{"result":"N","value":"喜欢自由和灵活性","key":"B"}],"title":"8. 对于日常安排"},{"options":[{"result":"P","value":"首先考虑可能性","key":"A"},{"result":"J","value":"首先考虑后果","key":"B"}],"title":"9. 当遇到问题时"},{"options":[{"result":"T","value":"时间是一种宝贵的资源","key":"A"},{"result":"F","value":"时间是相对灵活的概念","key":"B"}],"title":"10. 你如何看待时间"}]', 3, 1, '2024-04-26 16:39:29', '2024-05-09 12:28:58', 0); INSERT INTO question (id, questionContent, appId, userId, createTime, updateTime, isDelete) VALUES (4, '[{"options":[{"score":0,"value":"利马","key":"A"},{"score":0,"value":"圣多明各","key":"B"},{"score":0,"value":"圣萨尔瓦多","key":"C"},{"score":1,"value":"波哥大","key":"D"}],"title":"哥伦比亚的首都是?"},{"options":[{"score":0,"value":"蒙特利尔","key":"A"},{"score":0,"value":"多伦多","key":"B"},{"score":1,"value":"渥太华","key":"C"},{"score":0,"value":"温哥华","key":"D"}],"title":"加拿大的首都是?"},{"options":[{"score":0,"value":"大阪","key":"A"},{"score":1,"value":"东京","key":"B"},{"score":0,"value":"京都","key":"C"},{"score":0,"value":"名古屋","key":"D"}],"title":"日本的首都是?"},{"options":[{"score":0,"value":"墨尔本","key":"A"},{"score":0,"value":"悉尼","key":"B"},{"score":0,"value":"布里斯班","key":"C"},{"score":1,"value":"堪培拉","key":"D"}],"title":"澳大利亚的首都是?"},{"options":[{"score":1,"value":"雅加达","key":"A"},{"score":0,"value":"曼谷","key":"B"},{"score":0,"value":"胡志明市","key":"C"},{"score":0,"value":"吉隆坡","key":"D"}],"title":"印度尼西亚的首都是?"},{"options":[{"score":0,"value":"上海","key":"A"},{"score":0,"value":"杭州","key":"B"},{"score":1,"value":"北京","key":"C"},{"score":0,"value":"广州","key":"D"}],"title":"中国的首都是?"},{"options":[{"score":0,"value":"汉堡","key":"A"},{"score":0,"value":"慕尼黑","key":"B"},{"score":1,"value":"柏林","key":"C"},{"score":0,"value":"科隆","key":"D"}],"title":"德国的首都是?"},{"options":[{"score":0,"value":"釜山","key":"A"},{"score":1,"value":"首尔","key":"B"},{"score":0,"value":"大田","key":"C"},{"score":0,"value":"仁川","key":"D"}],"title":"韩国的首都是?"},{"options":[{"score":0,"value":"瓜达拉哈拉","key":"A"},{"score":0,"value":"蒙特雷","key":"B"},{"score":1,"value":"墨西哥城","key":"C"},{"score":0,"value":"坎昆","key":"D"}],"title":"墨西哥的首都是?"},{"options":[{"score":1,"value":"开罗","key":"A"},{"score":0,"value":"亚历山大","key":"B"},{"score":0,"value":"卢克索","key":"C"},{"score":0,"value":"卡利乌比亚","key":"D"}],"title":"埃及的首都是?"}]', 4, 1, '2024-04-26 16:39:29', '2024-05-09 12:28:58', 0); -- 评分结果表初始数据 INSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime, updateTime, isDelete, appId, userId) VALUES (1, 'ISTJ(物流师)', '忠诚可靠,被公认为务实,注重细节。', 'icon_url_istj', '["I","S","T","J"]', null, '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1); INSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime, updateTime, isDelete, appId, userId) VALUES (2, 'ISFJ(守护者)', '善良贴心,以同情心和责任为特点。', 'icon_url_isfj', '["I","S","F","J"]', null, '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1); INSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime, updateTime, isDelete, appId, userId) VALUES (3, 'INFJ(占有者)', '理想主义者,有着深刻的洞察力,善于理解他人。', 'icon_url_infj', '["I","N","F","J"]', null, '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1); INSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime, updateTime, isDelete, appId, userId) VALUES (4, 'INTJ(设计师)', '独立思考者,善于规划和实现目标,理性而果断。', 'icon_url_intj', '["I","N","T","J"]', null, '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1); INSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime, updateTime, isDelete, appId, userId) VALUES (5, 'ISTP(运动员)', '冷静自持,善于解决问题,擅长实践技能。', 'icon_url_istp', '["I","S","T","P"]', null, '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1); INSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime, updateTime, isDelete, appId, userId) VALUES (6, 'ISFP(艺术家)', '具有艺术感和敏感性,珍视个人空间和自由。', 'icon_url_isfp', '["I","S","F","P"]', null, '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1); INSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime, updateTime, isDelete, appId, userId) VALUES (7, 'INFP(治愈者)', '理想主义者,富有创造力,以同情心和理解他人著称。', 'icon_url_infp', '["I","N","F","P"]', null, '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1); INSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime, updateTime, isDelete, appId, userId) VALUES (8, 'INTP(学者)', '思维清晰,探索精神,独立思考且理性。', 'icon_url_intp', '["I","N","T","P"]', null, '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1); INSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime, updateTime, isDelete, appId, userId) VALUES (9, 'ESTP(拓荒者)', '敢于冒险,乐于冒险,思维敏捷,行动果断。', 'icon_url_estp', '["E","S","T","P"]', null, '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1); INSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime, updateTime, isDelete, appId, userId) VALUES (10, 'ESFP(表演者)', '热情开朗,善于社交,热爱生活,乐于助人。', 'icon_url_esfp', '["E","S","F","P"]', null, '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1); INSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime, updateTime, isDelete, appId, userId) VALUES (11, 'ENFP(倡导者)', '富有想象力,充满热情,善于激发他人的活力和潜力。', 'icon_url_enfp', '["E","N","F","P"]', null, '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1); INSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime, updateTime, isDelete, appId, userId) VALUES (12, 'ENTP(发明家)', '充满创造力,善于辩论,挑战传统,喜欢探索新领域。', 'icon_url_entp', '["E","N","T","P"]', null, '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1); INSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime, updateTime, isDelete, appId, userId) VALUES (13, 'ESTJ(主管)', '务实果断,善于组织和管理,重视效率和目标。', 'icon_url_estj', '["E","S","T","J"]', null, '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1); INSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime, updateTime, isDelete, appId, userId) VALUES (14, 'ESFJ(尽责者)', '友善热心,以协调、耐心和关怀为特点,善于团队合作。', 'icon_url_esfj', '["E","S","F","J"]', null, '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1); INSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime, updateTime, isDelete, appId, userId) VALUES (15, 'ENFJ(教导着)', '热情关爱,善于帮助他人,具有领导力和社交能力。', 'icon_url_enfj', '["E","N","F","J"]', null, '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1); INSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime, updateTime, isDelete, appId, userId) VALUES (16, 'ENTJ(统帅)', '果断自信,具有领导才能,善于规划和执行目标。', 'icon_url_entj', '["E","N","T","J"]', null, '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1); INSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime, updateTime, isDelete, appId, userId) VALUES (17, '首都知识大师', '你真棒棒哦,首都知识非常出色!', null, null, 9, '2024-04-25 15:05:44', '2024-05-09 12:28:21', 0, 2, 1); INSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime, updateTime, isDelete, appId, userId) VALUES (18, '地理小能手!', '你对于世界各国的首都了解得相当不错,但还有一些小地方需要加强哦!', null, null, 7, '2024-04-25 15:05:44', '2024-05-09 12:28:21', 0, 2, 1); INSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime, updateTime, isDelete, appId, userId) VALUES (19, '继续加油!', '还需努力哦', null, null, 0, '2024-04-25 15:05:44', '2024-05-09 12:28:21', 0, 2, 1); -- 用户回答表初始数据 INSERT INTO user_answer (id, appId, appType, choices, resultId, resultName, resultDesc, resultPicture, resultScore, scoringStrategy, userId, createTime, updateTime, isDelete) VALUES (1, 1, 1, '["A","A","A","B","A","A","A","B","B","A"]', 1, 'ISTJ(物流师)', '忠诚可靠,被公认为务实,注重细节。', 'icon_url_istj', null, 0, 1, '2024-05-09 15:08:22', '2024-05-09 15:10:13', 0); INSERT INTO user_answer (id, appId, appType, choices, resultId, resultName, resultDesc, resultPicture, resultScore, scoringStrategy, userId, createTime, updateTime, isDelete) VALUES (2, 2, 0, '["D","C","B","D","A","C","C","B","C","A"]', 17, '首都知识大师', '你真棒棒哦,首都知识非常出色!', null, 10, 0, 1, '2024-05-09 15:08:36', '2024-05-09 15:10:13', 0); ================================================ FILE: yudada-backend/sql/post_es_mapping.json ================================================ { "aliases": { "post": {} }, "mappings": { "properties": { "title": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "content": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "tags": { "type": "keyword" }, "thumbNum": { "type": "long" }, "favourNum": { "type": "long" }, "userId": { "type": "keyword" }, "createTime": { "type": "date" }, "updateTime": { "type": "date" }, "isDelete": { "type": "keyword" } } } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/MainApplication.java ================================================ package com.yupi.yudada; 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.context.annotation.EnableAspectJAutoProxy; import org.springframework.scheduling.annotation.EnableScheduling; /** * 主类(项目启动入口) * * @author 程序员鱼皮 * @from 编程导航知识星球 */ // todo 如需开启 Redis,须移除 exclude 中的内容 @SpringBootApplication @MapperScan("com.yupi.yudada.mapper") @EnableScheduling @EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true) public class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class, args); } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/annotation/AuthCheck.java ================================================ package com.yupi.yudada.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 权限校验 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AuthCheck { /** * 必须有某个角色 * * @return */ String mustRole() default ""; } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/aop/AuthInterceptor.java ================================================ package com.yupi.yudada.aop; import com.yupi.yudada.annotation.AuthCheck; import com.yupi.yudada.common.ErrorCode; import com.yupi.yudada.exception.BusinessException; import com.yupi.yudada.model.entity.User; import com.yupi.yudada.model.enums.UserRoleEnum; import com.yupi.yudada.service.UserService; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; /** * 权限校验 AOP * * @author 程序员鱼皮 * @from 编程导航知识星球 */ @Aspect @Component public class AuthInterceptor { @Resource private UserService userService; /** * 执行拦截 * * @param joinPoint * @param authCheck * @return */ @Around("@annotation(authCheck)") public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable { String mustRole = authCheck.mustRole(); RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes(); HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); // 当前登录用户 User loginUser = userService.getLoginUser(request); UserRoleEnum mustRoleEnum = UserRoleEnum.getEnumByValue(mustRole); // 不需要权限,放行 if (mustRoleEnum == null) { return joinPoint.proceed(); } // 必须有该权限才通过 UserRoleEnum userRoleEnum = UserRoleEnum.getEnumByValue(loginUser.getUserRole()); if (userRoleEnum == null) { throw new BusinessException(ErrorCode.NO_AUTH_ERROR); } // 如果被封号,直接拒绝 if (UserRoleEnum.BAN.equals(userRoleEnum)) { throw new BusinessException(ErrorCode.NO_AUTH_ERROR); } // 必须有管理员权限 if (UserRoleEnum.ADMIN.equals(mustRoleEnum)) { // 用户没有管理员权限,拒绝 if (!UserRoleEnum.ADMIN.equals(userRoleEnum)) { throw new BusinessException(ErrorCode.NO_AUTH_ERROR); } } // 通过权限校验,放行 return joinPoint.proceed(); } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/aop/LogInterceptor.java ================================================ package com.yupi.yudada.aop; import java.util.UUID; import javax.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; import org.springframework.util.StopWatch; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; /** * 请求响应日志 AOP * * @author 程序员鱼皮 * @from 编程导航知识星球 **/ @Aspect @Component @Slf4j public class LogInterceptor { /** * 执行拦截 */ @Around("execution(* com.yupi.yudada.controller.*.*(..))") public Object doInterceptor(ProceedingJoinPoint point) throws Throwable { // 计时 StopWatch stopWatch = new StopWatch(); stopWatch.start(); // 获取请求路径 RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes(); HttpServletRequest httpServletRequest = ((ServletRequestAttributes) requestAttributes).getRequest(); // 生成请求唯一 id String requestId = UUID.randomUUID().toString(); String url = httpServletRequest.getRequestURI(); // 获取请求参数 Object[] args = point.getArgs(); String reqParam = "[" + StringUtils.join(args, ", ") + "]"; // 输出请求日志 log.info("request start,id: {}, path: {}, ip: {}, params: {}", requestId, url, httpServletRequest.getRemoteHost(), reqParam); // 执行原方法 Object result = point.proceed(); // 输出响应日志 stopWatch.stop(); long totalTimeMillis = stopWatch.getTotalTimeMillis(); log.info("request end, id: {}, cost: {}ms", requestId, totalTimeMillis); return result; } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/common/BaseResponse.java ================================================ package com.yupi.yudada.common; import java.io.Serializable; import lombok.Data; /** * 通用返回类 * * @param * @author 程序员鱼皮 * @from 编程导航知识星球 */ @Data public class BaseResponse implements Serializable { private int code; private T data; private String message; public BaseResponse(int code, T data, String message) { this.code = code; this.data = data; this.message = message; } public BaseResponse(int code, T data) { this(code, data, ""); } public BaseResponse(ErrorCode errorCode) { this(errorCode.getCode(), null, errorCode.getMessage()); } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/common/DeleteRequest.java ================================================ package com.yupi.yudada.common; import java.io.Serializable; import lombok.Data; /** * 删除请求 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ @Data public class DeleteRequest implements Serializable { /** * id */ private Long id; private static final long serialVersionUID = 1L; } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/common/ErrorCode.java ================================================ package com.yupi.yudada.common; /** * 自定义错误码 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ public enum ErrorCode { SUCCESS(0, "ok"), PARAMS_ERROR(40000, "请求参数错误"), NOT_LOGIN_ERROR(40100, "未登录"), NO_AUTH_ERROR(40101, "无权限"), NOT_FOUND_ERROR(40400, "请求数据不存在"), 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; } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/common/PageRequest.java ================================================ package com.yupi.yudada.common; import com.yupi.yudada.constant.CommonConstant; import lombok.Data; /** * 分页请求 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ @Data public class PageRequest { /** * 当前页号 */ private int current = 1; /** * 页面大小 */ private int pageSize = 10; /** * 排序字段 */ private String sortField; /** * 排序顺序(默认升序) */ private String sortOrder = CommonConstant.SORT_ORDER_ASC; } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/common/ResultUtils.java ================================================ package com.yupi.yudada.common; /** * 返回工具类 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ public class ResultUtils { /** * 成功 * * @param data * @param * @return */ public static BaseResponse success(T data) { return new BaseResponse<>(0, data, "ok"); } /** * 失败 * * @param errorCode * @return */ public static BaseResponse error(ErrorCode errorCode) { return new BaseResponse<>(errorCode); } /** * 失败 * * @param code * @param message * @return */ public static BaseResponse error(int code, String message) { return new BaseResponse(code, null, message); } /** * 失败 * * @param errorCode * @return */ public static BaseResponse error(ErrorCode errorCode, String message) { return new BaseResponse(errorCode.getCode(), null, message); } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/common/ReviewRequest.java ================================================ package com.yupi.yudada.common; import lombok.Data; import java.io.Serializable; /** * 审核请求 */ @Data public class ReviewRequest implements Serializable { /** * id */ private Long id; /** * 状态:0-待审核, 1-通过, 2-拒绝 */ private Integer reviewStatus; /** * 审核信息 */ private String reviewMessage; private static final long serialVersionUID = 1L; } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/config/AiConfig.java ================================================ package com.yupi.yudada.config; import com.zhipu.oapi.ClientV4; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @ConfigurationProperties(prefix = "ai") @Data public class AiConfig { /** * apiKey,需要从开放平台获取 */ private String apiKey; @Bean public ClientV4 getClientV4() { return new ClientV4.Builder(apiKey).build(); } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/config/CorsConfig.java ================================================ package com.yupi.yudada.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * 全局跨域配置 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { // 覆盖所有请求 registry.addMapping("/**") // 允许发送 Cookie .allowCredentials(true) // 放行哪些域名(必须用 patterns,否则 * 会和 allowCredentials 冲突) .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .exposedHeaders("*"); } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/config/CosClientConfig.java ================================================ package com.yupi.yudada.config; import com.qcloud.cos.COSClient; import com.qcloud.cos.ClientConfig; import com.qcloud.cos.auth.BasicCOSCredentials; import com.qcloud.cos.auth.COSCredentials; import com.qcloud.cos.region.Region; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 腾讯云对象存储客户端 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ @Configuration @ConfigurationProperties(prefix = "cos.client") @Data public class CosClientConfig { /** * accessKey */ private String accessKey; /** * secretKey */ private String secretKey; /** * 区域 */ private String region; /** * 桶名 */ private String bucket; @Bean public COSClient cosClient() { // 初始化用户身份信息(secretId, secretKey) COSCredentials cred = new BasicCOSCredentials(accessKey, secretKey); // 设置bucket的区域, COS地域的简称请参照 https://www.qcloud.com/document/product/436/6224 ClientConfig clientConfig = new ClientConfig(new Region(region)); // 生成cos客户端 return new COSClient(cred, clientConfig); } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/config/JsonConfig.java ================================================ package com.yupi.yudada.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 程序员鱼皮 * @from 编程导航知识星球 */ @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; } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/config/MyBatisPlusConfig.java ================================================ package com.yupi.yudada.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * MyBatis Plus 配置 * * @author https://github.com/liyupi */ @Configuration @MapperScan("com.yupi.yudada.mapper") public class MyBatisPlusConfig { /** * 拦截器配置 * * @return */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/config/RedissonConfig.java ================================================ package com.yupi.yudada.config; import com.zhipu.oapi.ClientV4; 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 String host; private Integer port; private Integer database; private String password; @Bean public RedissonClient redissonClient() { Config config = new Config(); config.useSingleServer() .setAddress("redis://" + host + ":" + port) .setDatabase(database) .setPassword(password); return Redisson.create(config); } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/config/VipSchedulerConfig.java ================================================ package com.yupi.yudada.config; import io.reactivex.Scheduler; import io.reactivex.schedulers.Schedulers; import lombok.Data; import org.jetbrains.annotations.NotNull; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; @Configuration @Data public class VipSchedulerConfig { @Bean public Scheduler vipScheduler() { ThreadFactory threadFactory = new ThreadFactory() { private final AtomicInteger threadNumber = new AtomicInteger(1); @Override public Thread newThread(@NotNull Runnable r) { Thread thread = new Thread(r, "VIPThreadPool-" + threadNumber.getAndIncrement()); // 非守护线程 thread.setDaemon(false); return thread; } }; ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10, threadFactory); return Schedulers.from(scheduledExecutorService); } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/constant/CommonConstant.java ================================================ package com.yupi.yudada.constant; /** * 通用常量 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ public interface CommonConstant { /** * 升序 */ String SORT_ORDER_ASC = "ascend"; /** * 降序 */ String SORT_ORDER_DESC = " descend"; } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/constant/FileConstant.java ================================================ package com.yupi.yudada.constant; /** * 文件常量 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ public interface FileConstant { /** * COS 访问地址 * todo 需替换配置 */ String COS_HOST = "https://yupi.icu"; } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/constant/UserConstant.java ================================================ package com.yupi.yudada.constant; /** * 用户常量 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ public interface UserConstant { /** * 用户登录态键 */ String USER_LOGIN_STATE = "user_login"; // region 权限 /** * 默认角色 */ String DEFAULT_ROLE = "user"; /** * 管理员角色 */ String ADMIN_ROLE = "admin"; /** * 被封号 */ String BAN_ROLE = "ban"; // endregion } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/controller/AppController.java ================================================ package com.yupi.yudada.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.yupi.yudada.annotation.AuthCheck; import com.yupi.yudada.common.*; import com.yupi.yudada.constant.UserConstant; import com.yupi.yudada.exception.BusinessException; import com.yupi.yudada.exception.ThrowUtils; import com.yupi.yudada.model.dto.app.AppAddRequest; import com.yupi.yudada.model.dto.app.AppEditRequest; import com.yupi.yudada.model.dto.app.AppQueryRequest; import com.yupi.yudada.model.dto.app.AppUpdateRequest; import com.yupi.yudada.model.entity.App; import com.yupi.yudada.model.entity.User; import com.yupi.yudada.model.enums.ReviewStatusEnum; import com.yupi.yudada.model.vo.AppVO; import com.yupi.yudada.service.AppService; import com.yupi.yudada.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.util.Date; /** * 应用接口 * * @author 程序员鱼皮 * @from 编程导航学习圈 */ @RestController @RequestMapping("/app") @Slf4j public class AppController { @Resource private AppService appService; @Resource private UserService userService; // region 增删改查 /** * 创建应用 * * @param appAddRequest * @param request * @return */ @PostMapping("/add") public BaseResponse addApp(@RequestBody AppAddRequest appAddRequest, HttpServletRequest request) { ThrowUtils.throwIf(appAddRequest == null, ErrorCode.PARAMS_ERROR); // 在此处将实体类和 DTO 进行转换 App app = new App(); BeanUtils.copyProperties(appAddRequest, app); // 数据校验 appService.validApp(app, true); // 填充默认值 User loginUser = userService.getLoginUser(request); app.setUserId(loginUser.getId()); app.setReviewStatus(ReviewStatusEnum.REVIEWING.getValue()); // 写入数据库 boolean result = appService.save(app); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); // 返回新写入的数据 id long newAppId = app.getId(); return ResultUtils.success(newAppId); } /** * 删除应用 * * @param deleteRequest * @param request * @return */ @PostMapping("/delete") public BaseResponse deleteApp(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) { if (deleteRequest == null || deleteRequest.getId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } User user = userService.getLoginUser(request); long id = deleteRequest.getId(); // 判断是否存在 App oldApp = appService.getById(id); ThrowUtils.throwIf(oldApp == null, ErrorCode.NOT_FOUND_ERROR); // 仅本人或管理员可删除 if (!oldApp.getUserId().equals(user.getId()) && !userService.isAdmin(request)) { throw new BusinessException(ErrorCode.NO_AUTH_ERROR); } // 操作数据库 boolean result = appService.removeById(id); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); return ResultUtils.success(true); } /** * 更新应用(仅管理员可用) * * @param appUpdateRequest * @return */ @PostMapping("/update") @AuthCheck(mustRole = UserConstant.ADMIN_ROLE) public BaseResponse updateApp(@RequestBody AppUpdateRequest appUpdateRequest) { if (appUpdateRequest == null || appUpdateRequest.getId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } // 在此处将实体类和 DTO 进行转换 App app = new App(); BeanUtils.copyProperties(appUpdateRequest, app); // 数据校验 appService.validApp(app, false); // 判断是否存在 long id = appUpdateRequest.getId(); App oldApp = appService.getById(id); ThrowUtils.throwIf(oldApp == null, ErrorCode.NOT_FOUND_ERROR); // 操作数据库 boolean result = appService.updateById(app); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); return ResultUtils.success(true); } /** * 根据 id 获取应用(封装类) * * @param id * @return */ @GetMapping("/get/vo") public BaseResponse getAppVOById(long id, HttpServletRequest request) { ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR); // 查询数据库 App app = appService.getById(id); ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR); // 获取封装类 return ResultUtils.success(appService.getAppVO(app, request)); } /** * 分页获取应用列表(仅管理员可用) * * @param appQueryRequest * @return */ @PostMapping("/list/page") @AuthCheck(mustRole = UserConstant.ADMIN_ROLE) public BaseResponse> listAppByPage(@RequestBody AppQueryRequest appQueryRequest) { long current = appQueryRequest.getCurrent(); long size = appQueryRequest.getPageSize(); // 查询数据库 Page appPage = appService.page(new Page<>(current, size), appService.getQueryWrapper(appQueryRequest)); return ResultUtils.success(appPage); } /** * 分页获取应用列表(封装类) * * @param appQueryRequest * @param request * @return */ @PostMapping("/list/page/vo") public BaseResponse> listAppVOByPage(@RequestBody AppQueryRequest appQueryRequest, HttpServletRequest request) { long current = appQueryRequest.getCurrent(); long size = appQueryRequest.getPageSize(); // 限制爬虫 ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR); // 只能看到已过审的应用 appQueryRequest.setReviewStatus(ReviewStatusEnum.PASS.getValue()); // 查询数据库 Page appPage = appService.page(new Page<>(current, size), appService.getQueryWrapper(appQueryRequest)); // 获取封装类 return ResultUtils.success(appService.getAppVOPage(appPage, request)); } /** * 分页获取当前登录用户创建的应用列表 * * @param appQueryRequest * @param request * @return */ @PostMapping("/my/list/page/vo") public BaseResponse> listMyAppVOByPage(@RequestBody AppQueryRequest appQueryRequest, HttpServletRequest request) { ThrowUtils.throwIf(appQueryRequest == null, ErrorCode.PARAMS_ERROR); // 补充查询条件,只查询当前登录用户的数据 User loginUser = userService.getLoginUser(request); appQueryRequest.setUserId(loginUser.getId()); long current = appQueryRequest.getCurrent(); long size = appQueryRequest.getPageSize(); // 限制爬虫 ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR); // 查询数据库 Page appPage = appService.page(new Page<>(current, size), appService.getQueryWrapper(appQueryRequest)); // 获取封装类 return ResultUtils.success(appService.getAppVOPage(appPage, request)); } /** * 编辑应用(给用户使用) * * @param appEditRequest * @param request * @return */ @PostMapping("/edit") public BaseResponse editApp(@RequestBody AppEditRequest appEditRequest, HttpServletRequest request) { if (appEditRequest == null || appEditRequest.getId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } // 在此处将实体类和 DTO 进行转换 App app = new App(); BeanUtils.copyProperties(appEditRequest, app); // 数据校验 appService.validApp(app, false); User loginUser = userService.getLoginUser(request); // 判断是否存在 long id = appEditRequest.getId(); App oldApp = appService.getById(id); ThrowUtils.throwIf(oldApp == null, ErrorCode.NOT_FOUND_ERROR); // 仅本人或管理员可编辑 if (!oldApp.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) { throw new BusinessException(ErrorCode.NO_AUTH_ERROR); } // 重置审核状态 app.setReviewStatus(ReviewStatusEnum.REVIEWING.getValue()); // 操作数据库 boolean result = appService.updateById(app); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); return ResultUtils.success(true); } // endregion /** * 应用审核 * * @param reviewRequest * @param request * @return */ @PostMapping("/review") @AuthCheck(mustRole = UserConstant.ADMIN_ROLE) public BaseResponse doAppReview(@RequestBody ReviewRequest reviewRequest, HttpServletRequest request) { ThrowUtils.throwIf(reviewRequest == null, ErrorCode.PARAMS_ERROR); Long id = reviewRequest.getId(); Integer reviewStatus = reviewRequest.getReviewStatus(); // 校验 ReviewStatusEnum reviewStatusEnum = ReviewStatusEnum.getEnumByValue(reviewStatus); if (id == null || reviewStatusEnum == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } // 判断是否存在 App oldApp = appService.getById(id); ThrowUtils.throwIf(oldApp == null, ErrorCode.NOT_FOUND_ERROR); // 已是该状态 if (oldApp.getReviewStatus().equals(reviewStatus)) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "请勿重复审核"); } // 更新审核状态 User loginUser = userService.getLoginUser(request); App app = new App(); app.setId(id); app.setReviewStatus(reviewStatus); app.setReviewMessage(reviewRequest.getReviewMessage()); app.setReviewerId(loginUser.getId()); app.setReviewTime(new Date()); boolean result = appService.updateById(app); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); return ResultUtils.success(true); } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/controller/AppStatisticController.java ================================================ package com.yupi.yudada.controller; import cn.hutool.core.io.FileUtil; import com.yupi.yudada.common.BaseResponse; import com.yupi.yudada.common.ErrorCode; import com.yupi.yudada.common.ResultUtils; import com.yupi.yudada.constant.FileConstant; import com.yupi.yudada.exception.BusinessException; import com.yupi.yudada.exception.ThrowUtils; import com.yupi.yudada.manager.CosManager; import com.yupi.yudada.mapper.UserAnswerMapper; import com.yupi.yudada.model.dto.file.UploadFileRequest; import com.yupi.yudada.model.dto.statistic.AppAnswerCountDTO; import com.yupi.yudada.model.dto.statistic.AppAnswerResultCountDTO; import com.yupi.yudada.model.entity.User; import com.yupi.yudada.model.enums.FileUploadBizEnum; import com.yupi.yudada.service.UserService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomStringUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.io.File; import java.util.Arrays; import java.util.List; /** * App 统计分析接口 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ @RestController @RequestMapping("/app/statistic") @Slf4j public class AppStatisticController { @Resource private UserAnswerMapper userAnswerMapper; /** * 热门应用及回答数统计(top 10) * * @return */ @GetMapping("/answer_count") public BaseResponse> getAppAnswerCount() { return ResultUtils.success(userAnswerMapper.doAppAnswerCount()); } /** * 某应用回答结果分布统计 * * @param appId * @return */ @GetMapping("/answer_result_count") public BaseResponse> getAppAnswerResultCount(Long appId) { ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR); return ResultUtils.success(userAnswerMapper.doAppAnswerResultCount(appId)); } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/controller/FileController.java ================================================ package com.yupi.yudada.controller; import cn.hutool.core.io.FileUtil; import com.yupi.yudada.common.BaseResponse; import com.yupi.yudada.common.ErrorCode; import com.yupi.yudada.common.ResultUtils; import com.yupi.yudada.constant.FileConstant; import com.yupi.yudada.exception.BusinessException; import com.yupi.yudada.manager.CosManager; import com.yupi.yudada.model.dto.file.UploadFileRequest; import com.yupi.yudada.model.entity.User; import com.yupi.yudada.model.enums.FileUploadBizEnum; import com.yupi.yudada.service.UserService; import java.io.File; import java.util.Arrays; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomStringUtils; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; /** * 文件接口 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ @RestController @RequestMapping("/file") @Slf4j public class FileController { @Resource private UserService userService; @Resource private CosManager cosManager; /** * 文件上传 * * @param multipartFile * @param uploadFileRequest * @param request * @return */ @PostMapping("/upload") public BaseResponse uploadFile(@RequestPart("file") MultipartFile multipartFile, UploadFileRequest uploadFileRequest, HttpServletRequest request) { String biz = uploadFileRequest.getBiz(); FileUploadBizEnum fileUploadBizEnum = FileUploadBizEnum.getEnumByValue(biz); if (fileUploadBizEnum == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } validFile(multipartFile, fileUploadBizEnum); User loginUser = userService.getLoginUser(request); // 文件目录:根据业务、用户来划分 String uuid = RandomStringUtils.randomAlphanumeric(8); String filename = uuid + "-" + multipartFile.getOriginalFilename(); String filepath = String.format("/%s/%s/%s", fileUploadBizEnum.getValue(), loginUser.getId(), filename); File file = null; try { // 上传文件 file = File.createTempFile(filepath, null); multipartFile.transferTo(file); cosManager.putObject(filepath, file); // 返回可访问地址 return ResultUtils.success(FileConstant.COS_HOST + filepath); } catch (Exception e) { log.error("file upload error, filepath = " + filepath, e); throw new BusinessException(ErrorCode.SYSTEM_ERROR, "上传失败"); } finally { if (file != null) { // 删除临时文件 boolean delete = file.delete(); if (!delete) { log.error("file delete error, filepath = {}", filepath); } } } } /** * 校验文件 * * @param multipartFile * @param fileUploadBizEnum 业务类型 */ private void validFile(MultipartFile multipartFile, FileUploadBizEnum fileUploadBizEnum) { // 文件大小 long fileSize = multipartFile.getSize(); // 文件后缀 String fileSuffix = FileUtil.getSuffix(multipartFile.getOriginalFilename()); final long ONE_M = 1024 * 1024L; if (FileUploadBizEnum.USER_AVATAR.equals(fileUploadBizEnum)) { if (fileSize > ONE_M) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件大小不能超过 1M"); } if (!Arrays.asList("jpeg", "jpg", "svg", "png", "webp").contains(fileSuffix)) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件类型错误"); } } } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/controller/PostController.java ================================================ package com.yupi.yudada.controller; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.yupi.yudada.annotation.AuthCheck; import com.yupi.yudada.common.BaseResponse; import com.yupi.yudada.common.DeleteRequest; import com.yupi.yudada.common.ErrorCode; import com.yupi.yudada.common.ResultUtils; import com.yupi.yudada.constant.UserConstant; import com.yupi.yudada.exception.BusinessException; import com.yupi.yudada.exception.ThrowUtils; import com.yupi.yudada.model.dto.post.PostAddRequest; import com.yupi.yudada.model.dto.post.PostEditRequest; import com.yupi.yudada.model.dto.post.PostQueryRequest; import com.yupi.yudada.model.dto.post.PostUpdateRequest; import com.yupi.yudada.model.entity.Post; import com.yupi.yudada.model.entity.User; import com.yupi.yudada.model.vo.PostVO; import com.yupi.yudada.service.PostService; import com.yupi.yudada.service.UserService; import java.util.List; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * 帖子接口 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ @RestController @RequestMapping("/post") @Slf4j public class PostController { @Resource private PostService postService; @Resource private UserService userService; // region 增删改查 /** * 创建 * * @param postAddRequest * @param request * @return */ @PostMapping("/add") public BaseResponse addPost(@RequestBody PostAddRequest postAddRequest, HttpServletRequest request) { if (postAddRequest == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } Post post = new Post(); BeanUtils.copyProperties(postAddRequest, post); List tags = postAddRequest.getTags(); if (tags != null) { post.setTags(JSONUtil.toJsonStr(tags)); } postService.validPost(post, true); User loginUser = userService.getLoginUser(request); post.setUserId(loginUser.getId()); post.setFavourNum(0); post.setThumbNum(0); boolean result = postService.save(post); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); long newPostId = post.getId(); return ResultUtils.success(newPostId); } /** * 删除 * * @param deleteRequest * @param request * @return */ @PostMapping("/delete") public BaseResponse deletePost(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) { if (deleteRequest == null || deleteRequest.getId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } User user = userService.getLoginUser(request); long id = deleteRequest.getId(); // 判断是否存在 Post oldPost = postService.getById(id); ThrowUtils.throwIf(oldPost == null, ErrorCode.NOT_FOUND_ERROR); // 仅本人或管理员可删除 if (!oldPost.getUserId().equals(user.getId()) && !userService.isAdmin(request)) { throw new BusinessException(ErrorCode.NO_AUTH_ERROR); } boolean b = postService.removeById(id); return ResultUtils.success(b); } /** * 更新(仅管理员) * * @param postUpdateRequest * @return */ @PostMapping("/update") @AuthCheck(mustRole = UserConstant.ADMIN_ROLE) public BaseResponse updatePost(@RequestBody PostUpdateRequest postUpdateRequest) { if (postUpdateRequest == null || postUpdateRequest.getId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } Post post = new Post(); BeanUtils.copyProperties(postUpdateRequest, post); List tags = postUpdateRequest.getTags(); if (tags != null) { post.setTags(JSONUtil.toJsonStr(tags)); } // 参数校验 postService.validPost(post, false); long id = postUpdateRequest.getId(); // 判断是否存在 Post oldPost = postService.getById(id); ThrowUtils.throwIf(oldPost == null, ErrorCode.NOT_FOUND_ERROR); boolean result = postService.updateById(post); return ResultUtils.success(result); } /** * 根据 id 获取 * * @param id * @return */ @GetMapping("/get/vo") public BaseResponse getPostVOById(long id, HttpServletRequest request) { if (id <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } Post post = postService.getById(id); if (post == null) { throw new BusinessException(ErrorCode.NOT_FOUND_ERROR); } return ResultUtils.success(postService.getPostVO(post, request)); } /** * 分页获取列表(仅管理员) * * @param postQueryRequest * @return */ @PostMapping("/list/page") @AuthCheck(mustRole = UserConstant.ADMIN_ROLE) public BaseResponse> listPostByPage(@RequestBody PostQueryRequest postQueryRequest) { long current = postQueryRequest.getCurrent(); long size = postQueryRequest.getPageSize(); Page postPage = postService.page(new Page<>(current, size), postService.getQueryWrapper(postQueryRequest)); return ResultUtils.success(postPage); } /** * 分页获取列表(封装类) * * @param postQueryRequest * @param request * @return */ @PostMapping("/list/page/vo") public BaseResponse> listPostVOByPage(@RequestBody PostQueryRequest postQueryRequest, HttpServletRequest request) { long current = postQueryRequest.getCurrent(); long size = postQueryRequest.getPageSize(); // 限制爬虫 ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR); Page postPage = postService.page(new Page<>(current, size), postService.getQueryWrapper(postQueryRequest)); return ResultUtils.success(postService.getPostVOPage(postPage, request)); } /** * 分页获取当前用户创建的资源列表 * * @param postQueryRequest * @param request * @return */ @PostMapping("/my/list/page/vo") public BaseResponse> listMyPostVOByPage(@RequestBody PostQueryRequest postQueryRequest, HttpServletRequest request) { if (postQueryRequest == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } User loginUser = userService.getLoginUser(request); postQueryRequest.setUserId(loginUser.getId()); long current = postQueryRequest.getCurrent(); long size = postQueryRequest.getPageSize(); // 限制爬虫 ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR); Page postPage = postService.page(new Page<>(current, size), postService.getQueryWrapper(postQueryRequest)); return ResultUtils.success(postService.getPostVOPage(postPage, request)); } // endregion /** * 编辑(用户) * * @param postEditRequest * @param request * @return */ @PostMapping("/edit") public BaseResponse editPost(@RequestBody PostEditRequest postEditRequest, HttpServletRequest request) { if (postEditRequest == null || postEditRequest.getId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } Post post = new Post(); BeanUtils.copyProperties(postEditRequest, post); List tags = postEditRequest.getTags(); if (tags != null) { post.setTags(JSONUtil.toJsonStr(tags)); } // 参数校验 postService.validPost(post, false); User loginUser = userService.getLoginUser(request); long id = postEditRequest.getId(); // 判断是否存在 Post oldPost = postService.getById(id); ThrowUtils.throwIf(oldPost == null, ErrorCode.NOT_FOUND_ERROR); // 仅本人或管理员可编辑 if (!oldPost.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) { throw new BusinessException(ErrorCode.NO_AUTH_ERROR); } boolean result = postService.updateById(post); return ResultUtils.success(result); } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/controller/PostFavourController.java ================================================ package com.yupi.yudada.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.yupi.yudada.common.BaseResponse; import com.yupi.yudada.common.ErrorCode; import com.yupi.yudada.common.ResultUtils; import com.yupi.yudada.exception.BusinessException; import com.yupi.yudada.exception.ThrowUtils; import com.yupi.yudada.model.dto.post.PostQueryRequest; import com.yupi.yudada.model.dto.postfavour.PostFavourAddRequest; import com.yupi.yudada.model.dto.postfavour.PostFavourQueryRequest; import com.yupi.yudada.model.entity.Post; import com.yupi.yudada.model.entity.User; import com.yupi.yudada.model.vo.PostVO; import com.yupi.yudada.service.PostFavourService; import com.yupi.yudada.service.PostService; import com.yupi.yudada.service.UserService; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; 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; /** * 帖子收藏接口 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ @RestController @RequestMapping("/post_favour") @Slf4j public class PostFavourController { @Resource private PostFavourService postFavourService; @Resource private PostService postService; @Resource private UserService userService; /** * 收藏 / 取消收藏 * * @param postFavourAddRequest * @param request * @return resultNum 收藏变化数 */ @PostMapping("/") public BaseResponse doPostFavour(@RequestBody PostFavourAddRequest postFavourAddRequest, HttpServletRequest request) { if (postFavourAddRequest == null || postFavourAddRequest.getPostId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } // 登录才能操作 final User loginUser = userService.getLoginUser(request); long postId = postFavourAddRequest.getPostId(); int result = postFavourService.doPostFavour(postId, loginUser); return ResultUtils.success(result); } /** * 获取我收藏的帖子列表 * * @param postQueryRequest * @param request */ @PostMapping("/my/list/page") public BaseResponse> listMyFavourPostByPage(@RequestBody PostQueryRequest postQueryRequest, HttpServletRequest request) { if (postQueryRequest == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } User loginUser = userService.getLoginUser(request); long current = postQueryRequest.getCurrent(); long size = postQueryRequest.getPageSize(); // 限制爬虫 ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR); Page postPage = postFavourService.listFavourPostByPage(new Page<>(current, size), postService.getQueryWrapper(postQueryRequest), loginUser.getId()); return ResultUtils.success(postService.getPostVOPage(postPage, request)); } /** * 获取用户收藏的帖子列表 * * @param postFavourQueryRequest * @param request */ @PostMapping("/list/page") public BaseResponse> listFavourPostByPage(@RequestBody PostFavourQueryRequest postFavourQueryRequest, HttpServletRequest request) { if (postFavourQueryRequest == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } long current = postFavourQueryRequest.getCurrent(); long size = postFavourQueryRequest.getPageSize(); Long userId = postFavourQueryRequest.getUserId(); // 限制爬虫 ThrowUtils.throwIf(size > 20 || userId == null, ErrorCode.PARAMS_ERROR); Page postPage = postFavourService.listFavourPostByPage(new Page<>(current, size), postService.getQueryWrapper(postFavourQueryRequest.getPostQueryRequest()), userId); return ResultUtils.success(postService.getPostVOPage(postPage, request)); } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/controller/PostThumbController.java ================================================ package com.yupi.yudada.controller; import com.yupi.yudada.common.BaseResponse; import com.yupi.yudada.common.ErrorCode; import com.yupi.yudada.common.ResultUtils; import com.yupi.yudada.exception.BusinessException; import com.yupi.yudada.model.dto.postthumb.PostThumbAddRequest; import com.yupi.yudada.model.entity.User; import com.yupi.yudada.service.PostThumbService; import com.yupi.yudada.service.UserService; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; 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; /** * 帖子点赞接口 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ @RestController @RequestMapping("/post_thumb") @Slf4j public class PostThumbController { @Resource private PostThumbService postThumbService; @Resource private UserService userService; /** * 点赞 / 取消点赞 * * @param postThumbAddRequest * @param request * @return resultNum 本次点赞变化数 */ @PostMapping("/") public BaseResponse doThumb(@RequestBody PostThumbAddRequest postThumbAddRequest, HttpServletRequest request) { if (postThumbAddRequest == null || postThumbAddRequest.getPostId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } // 登录才能点赞 final User loginUser = userService.getLoginUser(request); long postId = postThumbAddRequest.getPostId(); int result = postThumbService.doPostThumb(postId, loginUser); return ResultUtils.success(result); } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/controller/QuestionController.java ================================================ package com.yupi.yudada.controller; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.yupi.yudada.annotation.AuthCheck; import com.yupi.yudada.common.BaseResponse; import com.yupi.yudada.common.DeleteRequest; import com.yupi.yudada.common.ErrorCode; import com.yupi.yudada.common.ResultUtils; import com.yupi.yudada.constant.UserConstant; import com.yupi.yudada.exception.BusinessException; import com.yupi.yudada.exception.ThrowUtils; import com.yupi.yudada.manager.AiManager; import com.yupi.yudada.model.dto.question.*; import com.yupi.yudada.model.entity.App; import com.yupi.yudada.model.entity.Question; import com.yupi.yudada.model.entity.User; import com.yupi.yudada.model.enums.AppTypeEnum; import com.yupi.yudada.model.vo.QuestionVO; import com.yupi.yudada.service.AppService; import com.yupi.yudada.service.QuestionService; import com.yupi.yudada.service.UserService; import com.zhipu.oapi.service.v4.model.ModelData; import io.reactivex.Flowable; import io.reactivex.Scheduler; import io.reactivex.schedulers.Schedulers; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; /** * 题目接口 * * @author 程序员鱼皮 * @from 编程导航学习圈 */ @RestController @RequestMapping("/question") @Slf4j public class QuestionController { @Resource private QuestionService questionService; @Resource private UserService userService; @Resource private AppService appService; @Resource private AiManager aiManager; @Resource private Scheduler vipScheduler; // region 增删改查 /** * 创建题目 * * @param questionAddRequest * @param request * @return */ @PostMapping("/add") public BaseResponse addQuestion(@RequestBody QuestionAddRequest questionAddRequest, HttpServletRequest request) { ThrowUtils.throwIf(questionAddRequest == null, ErrorCode.PARAMS_ERROR); // 在此处将实体类和 DTO 进行转换 Question question = new Question(); BeanUtils.copyProperties(questionAddRequest, question); List questionContentDTO = questionAddRequest.getQuestionContent(); question.setQuestionContent(JSONUtil.toJsonStr(questionContentDTO)); // 数据校验 questionService.validQuestion(question, true); // 填充默认值 User loginUser = userService.getLoginUser(request); question.setUserId(loginUser.getId()); // 写入数据库 boolean result = questionService.save(question); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); // 返回新写入的数据 id long newQuestionId = question.getId(); return ResultUtils.success(newQuestionId); } /** * 删除题目 * * @param deleteRequest * @param request * @return */ @PostMapping("/delete") public BaseResponse deleteQuestion(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) { if (deleteRequest == null || deleteRequest.getId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } User user = userService.getLoginUser(request); long id = deleteRequest.getId(); // 判断是否存在 Question oldQuestion = questionService.getById(id); ThrowUtils.throwIf(oldQuestion == null, ErrorCode.NOT_FOUND_ERROR); // 仅本人或管理员可删除 if (!oldQuestion.getUserId().equals(user.getId()) && !userService.isAdmin(request)) { throw new BusinessException(ErrorCode.NO_AUTH_ERROR); } // 操作数据库 boolean result = questionService.removeById(id); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); return ResultUtils.success(true); } /** * 更新题目(仅管理员可用) * * @param questionUpdateRequest * @return */ @PostMapping("/update") @AuthCheck(mustRole = UserConstant.ADMIN_ROLE) public BaseResponse updateQuestion(@RequestBody QuestionUpdateRequest questionUpdateRequest) { if (questionUpdateRequest == null || questionUpdateRequest.getId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } // 在此处将实体类和 DTO 进行转换 Question question = new Question(); BeanUtils.copyProperties(questionUpdateRequest, question); List questionContentDTO = questionUpdateRequest.getQuestionContent(); question.setQuestionContent(JSONUtil.toJsonStr(questionContentDTO)); // 数据校验 questionService.validQuestion(question, false); // 判断是否存在 long id = questionUpdateRequest.getId(); Question oldQuestion = questionService.getById(id); ThrowUtils.throwIf(oldQuestion == null, ErrorCode.NOT_FOUND_ERROR); // 操作数据库 boolean result = questionService.updateById(question); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); return ResultUtils.success(true); } /** * 根据 id 获取题目(封装类) * * @param id * @return */ @GetMapping("/get/vo") public BaseResponse getQuestionVOById(long id, HttpServletRequest request) { ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR); // 查询数据库 Question question = questionService.getById(id); ThrowUtils.throwIf(question == null, ErrorCode.NOT_FOUND_ERROR); // 获取封装类 return ResultUtils.success(questionService.getQuestionVO(question, request)); } /** * 分页获取题目列表(仅管理员可用) * * @param questionQueryRequest * @return */ @PostMapping("/list/page") @AuthCheck(mustRole = UserConstant.ADMIN_ROLE) public BaseResponse> listQuestionByPage(@RequestBody QuestionQueryRequest questionQueryRequest) { long current = questionQueryRequest.getCurrent(); long size = questionQueryRequest.getPageSize(); // 查询数据库 Page questionPage = questionService.page(new Page<>(current, size), questionService.getQueryWrapper(questionQueryRequest)); return ResultUtils.success(questionPage); } /** * 分页获取题目列表(封装类) * * @param questionQueryRequest * @param request * @return */ @PostMapping("/list/page/vo") public BaseResponse> listQuestionVOByPage(@RequestBody QuestionQueryRequest questionQueryRequest, HttpServletRequest request) { long current = questionQueryRequest.getCurrent(); long size = questionQueryRequest.getPageSize(); // 限制爬虫 ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR); // 查询数据库 Page questionPage = questionService.page(new Page<>(current, size), questionService.getQueryWrapper(questionQueryRequest)); // 获取封装类 return ResultUtils.success(questionService.getQuestionVOPage(questionPage, request)); } /** * 分页获取当前登录用户创建的题目列表 * * @param questionQueryRequest * @param request * @return */ @PostMapping("/my/list/page/vo") public BaseResponse> listMyQuestionVOByPage(@RequestBody QuestionQueryRequest questionQueryRequest, HttpServletRequest request) { ThrowUtils.throwIf(questionQueryRequest == null, ErrorCode.PARAMS_ERROR); // 补充查询条件,只查询当前登录用户的数据 User loginUser = userService.getLoginUser(request); questionQueryRequest.setUserId(loginUser.getId()); long current = questionQueryRequest.getCurrent(); long size = questionQueryRequest.getPageSize(); // 限制爬虫 ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR); // 查询数据库 Page questionPage = questionService.page(new Page<>(current, size), questionService.getQueryWrapper(questionQueryRequest)); // 获取封装类 return ResultUtils.success(questionService.getQuestionVOPage(questionPage, request)); } /** * 编辑题目(给用户使用) * * @param questionEditRequest * @param request * @return */ @PostMapping("/edit") public BaseResponse editQuestion(@RequestBody QuestionEditRequest questionEditRequest, HttpServletRequest request) { if (questionEditRequest == null || questionEditRequest.getId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } // 在此处将实体类和 DTO 进行转换 Question question = new Question(); BeanUtils.copyProperties(questionEditRequest, question); List questionContentDTO = questionEditRequest.getQuestionContent(); question.setQuestionContent(JSONUtil.toJsonStr(questionContentDTO)); // 数据校验 questionService.validQuestion(question, false); User loginUser = userService.getLoginUser(request); // 判断是否存在 long id = questionEditRequest.getId(); Question oldQuestion = questionService.getById(id); ThrowUtils.throwIf(oldQuestion == null, ErrorCode.NOT_FOUND_ERROR); // 仅本人或管理员可编辑 if (!oldQuestion.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) { throw new BusinessException(ErrorCode.NO_AUTH_ERROR); } // 操作数据库 boolean result = questionService.updateById(question); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); return ResultUtils.success(true); } // endregion // region AI 生成题目功能 private static final String GENERATE_QUESTION_SYSTEM_MESSAGE = "你是一位严谨的出题专家,我会给你如下信息:\n" + "```\n" + "应用名称,\n" + "【【【应用描述】】】,\n" + "应用类别,\n" + "要生成的题目数,\n" + "每个题目的选项数\n" + "```\n" + "\n" + "请你根据上述信息,按照以下步骤来出题:\n" + "1. 要求:题目和选项尽可能地短,题目不要包含序号,每题的选项数以我提供的为主,题目不能重复\n" + "2. 严格按照下面的 json 格式输出题目和选项\n" + "```\n" + "[{\"options\":[{\"value\":\"选项内容\",\"key\":\"A\"},{\"value\":\"\",\"key\":\"B\"}],\"title\":\"题目标题\"}]\n" + "```\n" + "title 是题目,options 是选项,每个选项的 key 按照英文字母序(比如 A、B、C、D)以此类推,value 是选项内容\n" + "3. 检查题目是否包含序号,若包含序号则去除序号\n" + "4. 返回的题目列表格式必须为 JSON 数组"; /** * 生成题目的用户消息 * * @param app * @param questionNumber * @param optionNumber * @return */ private String getGenerateQuestionUserMessage(App app, int questionNumber, int optionNumber) { StringBuilder userMessage = new StringBuilder(); userMessage.append(app.getAppName()).append("\n"); userMessage.append(app.getAppDesc()).append("\n"); userMessage.append(AppTypeEnum.getEnumByValue(app.getAppType()).getText() + "类").append("\n"); userMessage.append(questionNumber).append("\n"); userMessage.append(optionNumber); return userMessage.toString(); } @PostMapping("/ai_generate") public BaseResponse> aiGenerateQuestion( @RequestBody AiGenerateQuestionRequest aiGenerateQuestionRequest) { ThrowUtils.throwIf(aiGenerateQuestionRequest == null, ErrorCode.PARAMS_ERROR); // 获取参数 Long appId = aiGenerateQuestionRequest.getAppId(); int questionNumber = aiGenerateQuestionRequest.getQuestionNumber(); int optionNumber = aiGenerateQuestionRequest.getOptionNumber(); // 获取应用信息 App app = appService.getById(appId); ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR); // 封装 Prompt String userMessage = getGenerateQuestionUserMessage(app, questionNumber, optionNumber); // AI 生成 String result = aiManager.doSyncRequest(GENERATE_QUESTION_SYSTEM_MESSAGE, userMessage, null); // 截取需要的 JSON 信息 int start = result.indexOf("["); int end = result.lastIndexOf("]"); String json = result.substring(start, end + 1); List questionContentDTOList = JSONUtil.toList(json, QuestionContentDTO.class); return ResultUtils.success(questionContentDTOList); } @GetMapping("/ai_generate/sse") public SseEmitter aiGenerateQuestionSSE(AiGenerateQuestionRequest aiGenerateQuestionRequest, HttpServletRequest request) { ThrowUtils.throwIf(aiGenerateQuestionRequest == null, ErrorCode.PARAMS_ERROR); // 获取参数 Long appId = aiGenerateQuestionRequest.getAppId(); int questionNumber = aiGenerateQuestionRequest.getQuestionNumber(); int optionNumber = aiGenerateQuestionRequest.getOptionNumber(); // 获取应用信息 App app = appService.getById(appId); ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR); // 封装 Prompt String userMessage = getGenerateQuestionUserMessage(app, questionNumber, optionNumber); // 建立 SSE 连接对象,0 表示永不超时 SseEmitter sseEmitter = new SseEmitter(0L); // AI 生成,SSE 流式返回 Flowable modelDataFlowable = aiManager.doStreamRequest(GENERATE_QUESTION_SYSTEM_MESSAGE, userMessage, null); // 左括号计数器,除了默认值外,当回归为 0 时,表示左括号等于右括号,可以截取 AtomicInteger counter = new AtomicInteger(0); // 拼接完整题目 StringBuilder stringBuilder = new StringBuilder(); // 获取登录用户 User loginUser = userService.getLoginUser(request); // 默认全局线程池 Scheduler scheduler = Schedulers.io(); if ("vip".equals(loginUser.getUserRole())) { scheduler = vipScheduler; } modelDataFlowable .observeOn(scheduler) .map(modelData -> modelData.getChoices().get(0).getDelta().getContent()) .map(message -> message.replaceAll("\\s", "")) .filter(StrUtil::isNotBlank) .flatMap(message -> { List characterList = new ArrayList<>(); for (char c : message.toCharArray()) { characterList.add(c); } return Flowable.fromIterable(characterList); }) .doOnNext(c -> { // 如果是 '{',计数器 + 1 if (c == '{') { counter.addAndGet(1); } if (counter.get() > 0) { stringBuilder.append(c); } if (c == '}') { counter.addAndGet(-1); if (counter.get() == 0) { // 可以拼接题目,并且通过 SSE 返回给前端 sseEmitter.send(JSONUtil.toJsonStr(stringBuilder.toString())); // 重置,准备拼接下一道题 stringBuilder.setLength(0); } } }) .doOnError((e) -> log.error("sse error", e)) .doOnComplete(sseEmitter::complete) .subscribe(); return sseEmitter; } // 仅测试隔离线程池使用 @Deprecated @GetMapping("/ai_generate/sse/test") public SseEmitter aiGenerateQuestionSSETest(AiGenerateQuestionRequest aiGenerateQuestionRequest, boolean isVip) { ThrowUtils.throwIf(aiGenerateQuestionRequest == null, ErrorCode.PARAMS_ERROR); // 获取参数 Long appId = aiGenerateQuestionRequest.getAppId(); int questionNumber = aiGenerateQuestionRequest.getQuestionNumber(); int optionNumber = aiGenerateQuestionRequest.getOptionNumber(); // 获取应用信息 App app = appService.getById(appId); ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR); // 封装 Prompt String userMessage = getGenerateQuestionUserMessage(app, questionNumber, optionNumber); // 建立 SSE 连接对象,0 表示永不超时 SseEmitter sseEmitter = new SseEmitter(0L); // AI 生成,SSE 流式返回 Flowable modelDataFlowable = aiManager.doStreamRequest(GENERATE_QUESTION_SYSTEM_MESSAGE, userMessage, null); // 左括号计数器,除了默认值外,当回归为 0 时,表示左括号等于右括号,可以截取 AtomicInteger counter = new AtomicInteger(0); // 拼接完整题目 StringBuilder stringBuilder = new StringBuilder(); // 默认全局线程池 Scheduler scheduler = Schedulers.single(); if (isVip) { scheduler = vipScheduler; } modelDataFlowable .observeOn(scheduler) .map(modelData -> modelData.getChoices().get(0).getDelta().getContent()) .map(message -> message.replaceAll("\\s", "")) .filter(StrUtil::isNotBlank) .flatMap(message -> { List characterList = new ArrayList<>(); for (char c : message.toCharArray()) { characterList.add(c); } return Flowable.fromIterable(characterList); }) .doOnNext(c -> { // 如果是 '{',计数器 + 1 if (c == '{') { counter.addAndGet(1); } if (counter.get() > 0) { stringBuilder.append(c); } if (c == '}') { counter.addAndGet(-1); if (counter.get() == 0) { // 输出当前线程的名称 System.out.println(Thread.currentThread().getName()); // 模拟普通用户阻塞 if (!isVip) { Thread.sleep(10000L); } // 可以拼接题目,并且通过 SSE 返回给前端 sseEmitter.send(JSONUtil.toJsonStr(stringBuilder.toString())); // 重置,准备拼接下一道题 stringBuilder.setLength(0); } } }) .doOnError((e) -> log.error("sse error", e)) .doOnComplete(sseEmitter::complete) .subscribe(); return sseEmitter; } // endregion } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/controller/ScoringResultController.java ================================================ package com.yupi.yudada.controller; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.yupi.yudada.annotation.AuthCheck; import com.yupi.yudada.common.BaseResponse; import com.yupi.yudada.common.DeleteRequest; import com.yupi.yudada.common.ErrorCode; import com.yupi.yudada.common.ResultUtils; import com.yupi.yudada.constant.UserConstant; import com.yupi.yudada.exception.BusinessException; import com.yupi.yudada.exception.ThrowUtils; import com.yupi.yudada.model.dto.scoringResult.ScoringResultAddRequest; import com.yupi.yudada.model.dto.scoringResult.ScoringResultEditRequest; import com.yupi.yudada.model.dto.scoringResult.ScoringResultQueryRequest; import com.yupi.yudada.model.dto.scoringResult.ScoringResultUpdateRequest; import com.yupi.yudada.model.entity.ScoringResult; import com.yupi.yudada.model.entity.User; import com.yupi.yudada.model.vo.ScoringResultVO; import com.yupi.yudada.service.ScoringResultService; import com.yupi.yudada.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.util.List; /** * 评分结果接口 * * @author 程序员鱼皮 * @from 编程导航学习圈 */ @RestController @RequestMapping("/scoringResult") @Slf4j public class ScoringResultController { @Resource private ScoringResultService scoringResultService; @Resource private UserService userService; // region 增删改查 /** * 创建评分结果 * * @param scoringResultAddRequest * @param request * @return */ @PostMapping("/add") public BaseResponse addScoringResult(@RequestBody ScoringResultAddRequest scoringResultAddRequest, HttpServletRequest request) { ThrowUtils.throwIf(scoringResultAddRequest == null, ErrorCode.PARAMS_ERROR); // 在此处将实体类和 DTO 进行转换 ScoringResult scoringResult = new ScoringResult(); BeanUtils.copyProperties(scoringResultAddRequest, scoringResult); List resultProp = scoringResultAddRequest.getResultProp(); scoringResult.setResultProp(JSONUtil.toJsonStr(resultProp)); // 数据校验 scoringResultService.validScoringResult(scoringResult, true); // 填充默认值 User loginUser = userService.getLoginUser(request); scoringResult.setUserId(loginUser.getId()); // 写入数据库 boolean result = scoringResultService.save(scoringResult); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); // 返回新写入的数据 id long newScoringResultId = scoringResult.getId(); return ResultUtils.success(newScoringResultId); } /** * 删除评分结果 * * @param deleteRequest * @param request * @return */ @PostMapping("/delete") public BaseResponse deleteScoringResult(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) { if (deleteRequest == null || deleteRequest.getId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } User user = userService.getLoginUser(request); long id = deleteRequest.getId(); // 判断是否存在 ScoringResult oldScoringResult = scoringResultService.getById(id); ThrowUtils.throwIf(oldScoringResult == null, ErrorCode.NOT_FOUND_ERROR); // 仅本人或管理员可删除 if (!oldScoringResult.getUserId().equals(user.getId()) && !userService.isAdmin(request)) { throw new BusinessException(ErrorCode.NO_AUTH_ERROR); } // 操作数据库 boolean result = scoringResultService.removeById(id); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); return ResultUtils.success(true); } /** * 更新评分结果(仅管理员可用) * * @param scoringResultUpdateRequest * @return */ @PostMapping("/update") @AuthCheck(mustRole = UserConstant.ADMIN_ROLE) public BaseResponse updateScoringResult(@RequestBody ScoringResultUpdateRequest scoringResultUpdateRequest) { if (scoringResultUpdateRequest == null || scoringResultUpdateRequest.getId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } // 在此处将实体类和 DTO 进行转换 ScoringResult scoringResult = new ScoringResult(); BeanUtils.copyProperties(scoringResultUpdateRequest, scoringResult); List resultProp = scoringResultUpdateRequest.getResultProp(); scoringResult.setResultProp(JSONUtil.toJsonStr(resultProp)); // 数据校验 scoringResultService.validScoringResult(scoringResult, false); // 判断是否存在 long id = scoringResultUpdateRequest.getId(); ScoringResult oldScoringResult = scoringResultService.getById(id); ThrowUtils.throwIf(oldScoringResult == null, ErrorCode.NOT_FOUND_ERROR); // 操作数据库 boolean result = scoringResultService.updateById(scoringResult); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); return ResultUtils.success(true); } /** * 根据 id 获取评分结果(封装类) * * @param id * @return */ @GetMapping("/get/vo") public BaseResponse getScoringResultVOById(long id, HttpServletRequest request) { ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR); // 查询数据库 ScoringResult scoringResult = scoringResultService.getById(id); ThrowUtils.throwIf(scoringResult == null, ErrorCode.NOT_FOUND_ERROR); // 获取封装类 return ResultUtils.success(scoringResultService.getScoringResultVO(scoringResult, request)); } /** * 分页获取评分结果列表(仅管理员可用) * * @param scoringResultQueryRequest * @return */ @PostMapping("/list/page") @AuthCheck(mustRole = UserConstant.ADMIN_ROLE) public BaseResponse> listScoringResultByPage(@RequestBody ScoringResultQueryRequest scoringResultQueryRequest) { long current = scoringResultQueryRequest.getCurrent(); long size = scoringResultQueryRequest.getPageSize(); // 查询数据库 Page scoringResultPage = scoringResultService.page(new Page<>(current, size), scoringResultService.getQueryWrapper(scoringResultQueryRequest)); return ResultUtils.success(scoringResultPage); } /** * 分页获取评分结果列表(封装类) * * @param scoringResultQueryRequest * @param request * @return */ @PostMapping("/list/page/vo") public BaseResponse> listScoringResultVOByPage(@RequestBody ScoringResultQueryRequest scoringResultQueryRequest, HttpServletRequest request) { long current = scoringResultQueryRequest.getCurrent(); long size = scoringResultQueryRequest.getPageSize(); // 限制爬虫 ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR); // 查询数据库 Page scoringResultPage = scoringResultService.page(new Page<>(current, size), scoringResultService.getQueryWrapper(scoringResultQueryRequest)); // 获取封装类 return ResultUtils.success(scoringResultService.getScoringResultVOPage(scoringResultPage, request)); } /** * 分页获取当前登录用户创建的评分结果列表 * * @param scoringResultQueryRequest * @param request * @return */ @PostMapping("/my/list/page/vo") public BaseResponse> listMyScoringResultVOByPage(@RequestBody ScoringResultQueryRequest scoringResultQueryRequest, HttpServletRequest request) { ThrowUtils.throwIf(scoringResultQueryRequest == null, ErrorCode.PARAMS_ERROR); // 补充查询条件,只查询当前登录用户的数据 User loginUser = userService.getLoginUser(request); scoringResultQueryRequest.setUserId(loginUser.getId()); long current = scoringResultQueryRequest.getCurrent(); long size = scoringResultQueryRequest.getPageSize(); // 限制爬虫 ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR); // 查询数据库 Page scoringResultPage = scoringResultService.page(new Page<>(current, size), scoringResultService.getQueryWrapper(scoringResultQueryRequest)); // 获取封装类 return ResultUtils.success(scoringResultService.getScoringResultVOPage(scoringResultPage, request)); } /** * 编辑评分结果(给用户使用) * * @param scoringResultEditRequest * @param request * @return */ @PostMapping("/edit") public BaseResponse editScoringResult(@RequestBody ScoringResultEditRequest scoringResultEditRequest, HttpServletRequest request) { if (scoringResultEditRequest == null || scoringResultEditRequest.getId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } // 在此处将实体类和 DTO 进行转换 ScoringResult scoringResult = new ScoringResult(); BeanUtils.copyProperties(scoringResultEditRequest, scoringResult); List resultProp = scoringResultEditRequest.getResultProp(); scoringResult.setResultProp(JSONUtil.toJsonStr(resultProp)); // 数据校验 scoringResultService.validScoringResult(scoringResult, false); User loginUser = userService.getLoginUser(request); // 判断是否存在 long id = scoringResultEditRequest.getId(); ScoringResult oldScoringResult = scoringResultService.getById(id); ThrowUtils.throwIf(oldScoringResult == null, ErrorCode.NOT_FOUND_ERROR); // 仅本人或管理员可编辑 if (!oldScoringResult.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) { throw new BusinessException(ErrorCode.NO_AUTH_ERROR); } // 操作数据库 boolean result = scoringResultService.updateById(scoringResult); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); return ResultUtils.success(true); } // endregion } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/controller/UserAnswerController.java ================================================ package com.yupi.yudada.controller; import cn.hutool.core.util.IdUtil; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.yupi.yudada.annotation.AuthCheck; import com.yupi.yudada.common.BaseResponse; import com.yupi.yudada.common.DeleteRequest; import com.yupi.yudada.common.ErrorCode; import com.yupi.yudada.common.ResultUtils; import com.yupi.yudada.constant.UserConstant; import com.yupi.yudada.exception.BusinessException; import com.yupi.yudada.exception.ThrowUtils; import com.yupi.yudada.model.dto.userAnswer.UserAnswerAddRequest; import com.yupi.yudada.model.dto.userAnswer.UserAnswerEditRequest; import com.yupi.yudada.model.dto.userAnswer.UserAnswerQueryRequest; import com.yupi.yudada.model.dto.userAnswer.UserAnswerUpdateRequest; import com.yupi.yudada.model.entity.App; import com.yupi.yudada.model.entity.User; import com.yupi.yudada.model.entity.UserAnswer; import com.yupi.yudada.model.enums.ReviewStatusEnum; import com.yupi.yudada.model.vo.UserAnswerVO; import com.yupi.yudada.scoring.ScoringStrategyExecutor; import com.yupi.yudada.service.AppService; import com.yupi.yudada.service.UserAnswerService; import com.yupi.yudada.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.dao.DuplicateKeyException; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.util.List; /** * 用户答案接口 * * @author 程序员鱼皮 * @from 编程导航学习圈 */ @RestController @RequestMapping("/userAnswer") @Slf4j public class UserAnswerController { @Resource private UserAnswerService userAnswerService; @Resource private AppService appService; @Resource private UserService userService; @Resource private ScoringStrategyExecutor scoringStrategyExecutor; // region 增删改查 /** * 创建用户答案 * * @param userAnswerAddRequest * @param request * @return */ @PostMapping("/add") public BaseResponse addUserAnswer(@RequestBody UserAnswerAddRequest userAnswerAddRequest, HttpServletRequest request) { ThrowUtils.throwIf(userAnswerAddRequest == null, ErrorCode.PARAMS_ERROR); // 在此处将实体类和 DTO 进行转换 UserAnswer userAnswer = new UserAnswer(); BeanUtils.copyProperties(userAnswerAddRequest, userAnswer); List choices = userAnswerAddRequest.getChoices(); userAnswer.setChoices(JSONUtil.toJsonStr(choices)); // 数据校验 userAnswerService.validUserAnswer(userAnswer, true); // 判断 app 是否存在 Long appId = userAnswerAddRequest.getAppId(); App app = appService.getById(appId); ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR); if (!ReviewStatusEnum.PASS.equals(ReviewStatusEnum.getEnumByValue(app.getReviewStatus()))) { throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "应用未通过审核,无法答题"); } // 填充默认值 User loginUser = userService.getLoginUser(request); userAnswer.setUserId(loginUser.getId()); // 写入数据库 try { boolean result = userAnswerService.save(userAnswer); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); } catch (DuplicateKeyException e) { // ignore error } // 返回新写入的数据 id long newUserAnswerId = userAnswer.getId(); // 调用评分模块 try { UserAnswer userAnswerWithResult = scoringStrategyExecutor.doScore(choices, app); userAnswerWithResult.setId(newUserAnswerId); userAnswerWithResult.setAppId(null); userAnswerService.updateById(userAnswerWithResult); } catch (Exception e) { e.printStackTrace(); throw new BusinessException(ErrorCode.OPERATION_ERROR, "评分错误"); } return ResultUtils.success(newUserAnswerId); } /** * 删除用户答案 * * @param deleteRequest * @param request * @return */ @PostMapping("/delete") public BaseResponse deleteUserAnswer(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) { if (deleteRequest == null || deleteRequest.getId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } User user = userService.getLoginUser(request); long id = deleteRequest.getId(); // 判断是否存在 UserAnswer oldUserAnswer = userAnswerService.getById(id); ThrowUtils.throwIf(oldUserAnswer == null, ErrorCode.NOT_FOUND_ERROR); // 仅本人或管理员可删除 if (!oldUserAnswer.getUserId().equals(user.getId()) && !userService.isAdmin(request)) { throw new BusinessException(ErrorCode.NO_AUTH_ERROR); } // 操作数据库 boolean result = userAnswerService.removeById(id); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); return ResultUtils.success(true); } /** * 更新用户答案(仅管理员可用) * * @param userAnswerUpdateRequest * @return */ @PostMapping("/update") @AuthCheck(mustRole = UserConstant.ADMIN_ROLE) public BaseResponse updateUserAnswer(@RequestBody UserAnswerUpdateRequest userAnswerUpdateRequest) { if (userAnswerUpdateRequest == null || userAnswerUpdateRequest.getId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } // 在此处将实体类和 DTO 进行转换 UserAnswer userAnswer = new UserAnswer(); BeanUtils.copyProperties(userAnswerUpdateRequest, userAnswer); List choices = userAnswerUpdateRequest.getChoices(); userAnswer.setChoices(JSONUtil.toJsonStr(choices)); // 数据校验 userAnswerService.validUserAnswer(userAnswer, false); // 判断是否存在 long id = userAnswerUpdateRequest.getId(); UserAnswer oldUserAnswer = userAnswerService.getById(id); ThrowUtils.throwIf(oldUserAnswer == null, ErrorCode.NOT_FOUND_ERROR); // 操作数据库 boolean result = userAnswerService.updateById(userAnswer); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); return ResultUtils.success(true); } /** * 根据 id 获取用户答案(封装类) * * @param id * @return */ @GetMapping("/get/vo") public BaseResponse getUserAnswerVOById(long id, HttpServletRequest request) { ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR); // 查询数据库 UserAnswer userAnswer = userAnswerService.getById(id); ThrowUtils.throwIf(userAnswer == null, ErrorCode.NOT_FOUND_ERROR); // 获取封装类 return ResultUtils.success(userAnswerService.getUserAnswerVO(userAnswer, request)); } /** * 分页获取用户答案列表(仅管理员可用) * * @param userAnswerQueryRequest * @return */ @PostMapping("/list/page") @AuthCheck(mustRole = UserConstant.ADMIN_ROLE) public BaseResponse> listUserAnswerByPage(@RequestBody UserAnswerQueryRequest userAnswerQueryRequest) { long current = userAnswerQueryRequest.getCurrent(); long size = userAnswerQueryRequest.getPageSize(); // 查询数据库 Page userAnswerPage = userAnswerService.page(new Page<>(current, size), userAnswerService.getQueryWrapper(userAnswerQueryRequest)); return ResultUtils.success(userAnswerPage); } /** * 分页获取用户答案列表(封装类) * * @param userAnswerQueryRequest * @param request * @return */ @PostMapping("/list/page/vo") public BaseResponse> listUserAnswerVOByPage(@RequestBody UserAnswerQueryRequest userAnswerQueryRequest, HttpServletRequest request) { long current = userAnswerQueryRequest.getCurrent(); long size = userAnswerQueryRequest.getPageSize(); // 限制爬虫 ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR); // 查询数据库 Page userAnswerPage = userAnswerService.page(new Page<>(current, size), userAnswerService.getQueryWrapper(userAnswerQueryRequest)); // 获取封装类 return ResultUtils.success(userAnswerService.getUserAnswerVOPage(userAnswerPage, request)); } /** * 分页获取当前登录用户创建的用户答案列表 * * @param userAnswerQueryRequest * @param request * @return */ @PostMapping("/my/list/page/vo") public BaseResponse> listMyUserAnswerVOByPage(@RequestBody UserAnswerQueryRequest userAnswerQueryRequest, HttpServletRequest request) { ThrowUtils.throwIf(userAnswerQueryRequest == null, ErrorCode.PARAMS_ERROR); // 补充查询条件,只查询当前登录用户的数据 User loginUser = userService.getLoginUser(request); userAnswerQueryRequest.setUserId(loginUser.getId()); long current = userAnswerQueryRequest.getCurrent(); long size = userAnswerQueryRequest.getPageSize(); // 限制爬虫 ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR); // 查询数据库 Page userAnswerPage = userAnswerService.page(new Page<>(current, size), userAnswerService.getQueryWrapper(userAnswerQueryRequest)); // 获取封装类 return ResultUtils.success(userAnswerService.getUserAnswerVOPage(userAnswerPage, request)); } /** * 编辑用户答案(给用户使用) * * @param userAnswerEditRequest * @param request * @return */ @PostMapping("/edit") public BaseResponse editUserAnswer(@RequestBody UserAnswerEditRequest userAnswerEditRequest, HttpServletRequest request) { if (userAnswerEditRequest == null || userAnswerEditRequest.getId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } // 在此处将实体类和 DTO 进行转换 UserAnswer userAnswer = new UserAnswer(); BeanUtils.copyProperties(userAnswerEditRequest, userAnswer); List choices = userAnswerEditRequest.getChoices(); userAnswer.setChoices(JSONUtil.toJsonStr(choices)); // 数据校验 userAnswerService.validUserAnswer(userAnswer, false); User loginUser = userService.getLoginUser(request); // 判断是否存在 long id = userAnswerEditRequest.getId(); UserAnswer oldUserAnswer = userAnswerService.getById(id); ThrowUtils.throwIf(oldUserAnswer == null, ErrorCode.NOT_FOUND_ERROR); // 仅本人或管理员可编辑 if (!oldUserAnswer.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) { throw new BusinessException(ErrorCode.NO_AUTH_ERROR); } // 操作数据库 boolean result = userAnswerService.updateById(userAnswer); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); return ResultUtils.success(true); } // endregion @GetMapping("/generate/id") public BaseResponse generateUserAnswerId() { return ResultUtils.success(IdUtil.getSnowflakeNextId()); } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/controller/UserController.java ================================================ package com.yupi.yudada.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.yupi.yudada.annotation.AuthCheck; import com.yupi.yudada.common.BaseResponse; import com.yupi.yudada.common.DeleteRequest; import com.yupi.yudada.common.ErrorCode; import com.yupi.yudada.common.ResultUtils; import com.yupi.yudada.constant.UserConstant; import com.yupi.yudada.exception.BusinessException; import com.yupi.yudada.exception.ThrowUtils; import com.yupi.yudada.model.dto.user.UserAddRequest; import com.yupi.yudada.model.dto.user.UserLoginRequest; import com.yupi.yudada.model.dto.user.UserQueryRequest; import com.yupi.yudada.model.dto.user.UserRegisterRequest; import com.yupi.yudada.model.dto.user.UserUpdateMyRequest; import com.yupi.yudada.model.dto.user.UserUpdateRequest; import com.yupi.yudada.model.entity.User; import com.yupi.yudada.model.vo.LoginUserVO; import com.yupi.yudada.model.vo.UserVO; import com.yupi.yudada.service.UserService; import java.util.List; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; import org.springframework.util.DigestUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import static com.yupi.yudada.service.impl.UserServiceImpl.SALT; /** * 用户接口 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ @RestController @RequestMapping("/user") @Slf4j public class UserController { @Resource private UserService userService; // region 登录相关 /** * 用户注册 * * @param userRegisterRequest * @return */ @PostMapping("/register") public BaseResponse userRegister(@RequestBody UserRegisterRequest userRegisterRequest) { if (userRegisterRequest == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } String userAccount = userRegisterRequest.getUserAccount(); String userPassword = userRegisterRequest.getUserPassword(); String checkPassword = userRegisterRequest.getCheckPassword(); if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) { return null; } long result = userService.userRegister(userAccount, userPassword, checkPassword); return ResultUtils.success(result); } /** * 用户登录 * * @param userLoginRequest * @param request * @return */ @PostMapping("/login") public BaseResponse userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) { if (userLoginRequest == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } String userAccount = userLoginRequest.getUserAccount(); String userPassword = userLoginRequest.getUserPassword(); if (StringUtils.isAnyBlank(userAccount, userPassword)) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } LoginUserVO loginUserVO = userService.userLogin(userAccount, userPassword, request); return ResultUtils.success(loginUserVO); } /** * 用户注销 * * @param request * @return */ @PostMapping("/logout") public BaseResponse userLogout(HttpServletRequest request) { if (request == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } boolean result = userService.userLogout(request); return ResultUtils.success(result); } /** * 获取当前登录用户 * * @param request * @return */ @GetMapping("/get/login") public BaseResponse getLoginUser(HttpServletRequest request) { User user = userService.getLoginUser(request); return ResultUtils.success(userService.getLoginUserVO(user)); } // endregion // region 增删改查 /** * 创建用户 * * @param userAddRequest * @param request * @return */ @PostMapping("/add") @AuthCheck(mustRole = UserConstant.ADMIN_ROLE) public BaseResponse addUser(@RequestBody UserAddRequest userAddRequest, HttpServletRequest request) { if (userAddRequest == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } User user = new User(); BeanUtils.copyProperties(userAddRequest, user); // 默认密码 12345678 String defaultPassword = "12345678"; String encryptPassword = DigestUtils.md5DigestAsHex((SALT + defaultPassword).getBytes()); user.setUserPassword(encryptPassword); boolean result = userService.save(user); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); return ResultUtils.success(user.getId()); } /** * 删除用户 * * @param deleteRequest * @param request * @return */ @PostMapping("/delete") @AuthCheck(mustRole = UserConstant.ADMIN_ROLE) public BaseResponse deleteUser(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) { if (deleteRequest == null || deleteRequest.getId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } boolean b = userService.removeById(deleteRequest.getId()); return ResultUtils.success(b); } /** * 更新用户 * * @param userUpdateRequest * @param request * @return */ @PostMapping("/update") @AuthCheck(mustRole = UserConstant.ADMIN_ROLE) public BaseResponse updateUser(@RequestBody UserUpdateRequest userUpdateRequest, HttpServletRequest request) { if (userUpdateRequest == null || userUpdateRequest.getId() == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } User user = new User(); BeanUtils.copyProperties(userUpdateRequest, user); boolean result = userService.updateById(user); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); return ResultUtils.success(true); } /** * 根据 id 获取用户(仅管理员) * * @param id * @param request * @return */ @GetMapping("/get") @AuthCheck(mustRole = UserConstant.ADMIN_ROLE) public BaseResponse getUserById(long id, HttpServletRequest request) { if (id <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } User user = userService.getById(id); ThrowUtils.throwIf(user == null, ErrorCode.NOT_FOUND_ERROR); return ResultUtils.success(user); } /** * 根据 id 获取包装类 * * @param id * @param request * @return */ @GetMapping("/get/vo") public BaseResponse getUserVOById(long id, HttpServletRequest request) { BaseResponse response = getUserById(id, request); User user = response.getData(); return ResultUtils.success(userService.getUserVO(user)); } /** * 分页获取用户列表(仅管理员) * * @param userQueryRequest * @param request * @return */ @PostMapping("/list/page") @AuthCheck(mustRole = UserConstant.ADMIN_ROLE) public BaseResponse> listUserByPage(@RequestBody UserQueryRequest userQueryRequest, HttpServletRequest request) { long current = userQueryRequest.getCurrent(); long size = userQueryRequest.getPageSize(); Page userPage = userService.page(new Page<>(current, size), userService.getQueryWrapper(userQueryRequest)); return ResultUtils.success(userPage); } /** * 分页获取用户封装列表 * * @param userQueryRequest * @param request * @return */ @PostMapping("/list/page/vo") public BaseResponse> listUserVOByPage(@RequestBody UserQueryRequest userQueryRequest, HttpServletRequest request) { if (userQueryRequest == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } long current = userQueryRequest.getCurrent(); long size = userQueryRequest.getPageSize(); // 限制爬虫 ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR); Page userPage = userService.page(new Page<>(current, size), userService.getQueryWrapper(userQueryRequest)); Page userVOPage = new Page<>(current, size, userPage.getTotal()); List userVO = userService.getUserVO(userPage.getRecords()); userVOPage.setRecords(userVO); return ResultUtils.success(userVOPage); } // endregion /** * 更新个人信息 * * @param userUpdateMyRequest * @param request * @return */ @PostMapping("/update/my") public BaseResponse updateMyUser(@RequestBody UserUpdateMyRequest userUpdateMyRequest, HttpServletRequest request) { if (userUpdateMyRequest == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } User loginUser = userService.getLoginUser(request); User user = new User(); BeanUtils.copyProperties(userUpdateMyRequest, user); user.setId(loginUser.getId()); boolean result = userService.updateById(user); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); return ResultUtils.success(true); } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/exception/BusinessException.java ================================================ package com.yupi.yudada.exception; import com.yupi.yudada.common.ErrorCode; /** * 自定义异常类 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ 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; } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/exception/GlobalExceptionHandler.java ================================================ package com.yupi.yudada.exception; import com.yupi.yudada.common.BaseResponse; import com.yupi.yudada.common.ErrorCode; import com.yupi.yudada.common.ResultUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; /** * 全局异常处理器 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public BaseResponse businessExceptionHandler(BusinessException e) { log.error("BusinessException", e); return ResultUtils.error(e.getCode(), e.getMessage()); } @ExceptionHandler(RuntimeException.class) public BaseResponse runtimeExceptionHandler(RuntimeException e) { log.error("RuntimeException", e); return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统错误"); } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/exception/ThrowUtils.java ================================================ package com.yupi.yudada.exception; import com.yupi.yudada.common.ErrorCode; /** * 抛异常工具类 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ 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)); } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/generate/CodeGenerator.java ================================================ package com.yupi.yudada.generate; import cn.hutool.core.io.FileUtil; import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateException; import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.io.FileWriter; import java.io.Writer; /** * 代码生成器 * * @author 程序员鱼皮 * @from 编程导航学习圈 */ public class CodeGenerator { /** * 用法:修改生成参数和生成路径,注释掉不需要的生成逻辑,然后运行即可 * * @param args * @throws TemplateException * @throws IOException */ public static void main(String[] args) throws TemplateException, IOException { // 指定生成参数 String packageName = "com.yupi.yudada"; String dataName = "用户答案"; String dataKey = "userAnswer"; String upperDataKey = "UserAnswer"; // 封装生成参数 Map dataModel = new HashMap<>(); dataModel.put("packageName", packageName); dataModel.put("dataName", dataName); dataModel.put("dataKey", dataKey); dataModel.put("upperDataKey", upperDataKey); // 生成路径默认值 String projectPath = System.getProperty("user.dir"); // 参考路径,可以自己调整下面的 outputPath String inputPath = projectPath + File.separator + "src/main/resources/templates/模板名称.java.ftl"; String outputPath = String.format("%s/generator/包名/%s类后缀.java", projectPath, upperDataKey); // 1、生成 Controller // 指定生成路径 inputPath = projectPath + File.separator + "src/main/resources/templates/TemplateController.java.ftl"; outputPath = String.format("%s/generator/controller/%sController.java", projectPath, upperDataKey); // 生成 doGenerate(inputPath, outputPath, dataModel); System.out.println("生成 Controller 成功,文件路径:" + outputPath); // 2、生成 Service 接口和实现类 // 生成 Service 接口 inputPath = projectPath + File.separator + "src/main/resources/templates/TemplateService.java.ftl"; outputPath = String.format("%s/generator/service/%sService.java", projectPath, upperDataKey); doGenerate(inputPath, outputPath, dataModel); System.out.println("生成 Service 接口成功,文件路径:" + outputPath); // 生成 Service 实现类 inputPath = projectPath + File.separator + "src/main/resources/templates/TemplateServiceImpl.java.ftl"; outputPath = String.format("%s/generator/service/impl/%sServiceImpl.java", projectPath, upperDataKey); doGenerate(inputPath, outputPath, dataModel); System.out.println("生成 Service 实现类成功,文件路径:" + outputPath); // 3、生成数据模型封装类(包括 DTO 和 VO) // 生成 DTO inputPath = projectPath + File.separator + "src/main/resources/templates/model/TemplateAddRequest.java.ftl"; outputPath = String.format("%s/generator/model/dto/%sAddRequest.java", projectPath, upperDataKey); doGenerate(inputPath, outputPath, dataModel); inputPath = projectPath + File.separator + "src/main/resources/templates/model/TemplateQueryRequest.java.ftl"; outputPath = String.format("%s/generator/model/dto/%sQueryRequest.java", projectPath, upperDataKey); doGenerate(inputPath, outputPath, dataModel); inputPath = projectPath + File.separator + "src/main/resources/templates/model/TemplateEditRequest.java.ftl"; outputPath = String.format("%s/generator/model/dto/%sEditRequest.java", projectPath, upperDataKey); doGenerate(inputPath, outputPath, dataModel); inputPath = projectPath + File.separator + "src/main/resources/templates/model/TemplateUpdateRequest.java.ftl"; outputPath = String.format("%s/generator/model/dto/%sUpdateRequest.java", projectPath, upperDataKey); doGenerate(inputPath, outputPath, dataModel); System.out.println("生成 DTO 成功,文件路径:" + outputPath); // 生成 VO inputPath = projectPath + File.separator + "src/main/resources/templates/model/TemplateVO.java.ftl"; outputPath = String.format("%s/generator/model/vo/%sVO.java", projectPath, upperDataKey); doGenerate(inputPath, outputPath, dataModel); System.out.println("生成 VO 成功,文件路径:" + outputPath); } /** * 生成文件 * * @param inputPath 模板文件输入路径 * @param outputPath 输出路径 * @param model 数据模型 * @throws IOException * @throws TemplateException */ public static void doGenerate(String inputPath, String outputPath, Object model) throws IOException, TemplateException { // new 出 Configuration 对象,参数为 FreeMarker 版本号 Configuration configuration = new Configuration(Configuration.VERSION_2_3_31); // 指定模板文件所在的路径 File templateDir = new File(inputPath).getParentFile(); configuration.setDirectoryForTemplateLoading(templateDir); // 设置模板文件使用的字符集 configuration.setDefaultEncoding("utf-8"); // 创建模板对象,加载指定模板 String templateName = new File(inputPath).getName(); Template template = configuration.getTemplate(templateName); // 文件不存在则创建文件和父目录 if (!FileUtil.exist(outputPath)) { FileUtil.touch(outputPath); } // 生成 Writer out = new FileWriter(outputPath); template.process(model, out); // 生成文件后别忘了关闭哦 out.close(); } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/manager/AiManager.java ================================================ package com.yupi.yudada.manager; import com.yupi.yudada.common.ErrorCode; import com.yupi.yudada.exception.BusinessException; import com.zhipu.oapi.ClientV4; import com.zhipu.oapi.Constants; import com.zhipu.oapi.service.v4.model.*; import io.reactivex.Flowable; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.ArrayList; import java.util.List; /** * 通用 AI 调用能力 */ @Component public class AiManager { @Resource private ClientV4 clientV4; // 稳定的随机数 private static final float STABLE_TEMPERATURE = 0.05f; // 不稳定的随机数 private static final float UNSTABLE_TEMPERATURE = 0.99f; /** * 同步请求(答案不稳定) * * @param systemMessage * @param userMessage * @return */ public String doSyncUnstableRequest(String systemMessage, String userMessage) { return doRequest(systemMessage, userMessage, Boolean.FALSE, UNSTABLE_TEMPERATURE); } /** * 同步请求(答案较稳定) * * @param systemMessage * @param userMessage * @return */ public String doSyncStableRequest(String systemMessage, String userMessage) { return doRequest(systemMessage, userMessage, Boolean.FALSE, STABLE_TEMPERATURE); } /** * 同步请求 * * @param systemMessage * @param userMessage * @param temperature * @return */ public String doSyncRequest(String systemMessage, String userMessage, Float temperature) { return doRequest(systemMessage, userMessage, Boolean.FALSE, temperature); } /** * 通用请求(简化消息传递) * * @param systemMessage * @param userMessage * @param stream * @param temperature * @return */ public String doRequest(String systemMessage, String userMessage, Boolean stream, Float temperature) { List chatMessageList = new ArrayList<>(); ChatMessage systemChatMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), systemMessage); chatMessageList.add(systemChatMessage); ChatMessage userChatMessage = new ChatMessage(ChatMessageRole.USER.value(), userMessage); chatMessageList.add(userChatMessage); return doRequest(chatMessageList, stream, temperature); } /** * 通用请求 * * @param messages * @param stream * @param temperature * @return */ public String doRequest(List messages, Boolean stream, Float temperature) { // 构建请求 ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder() .model(Constants.ModelChatGLM4) .stream(stream) .temperature(temperature) .invokeMethod(Constants.invokeMethod) .messages(messages) .build(); try { ModelApiResponse invokeModelApiResp = clientV4.invokeModelApi(chatCompletionRequest); return invokeModelApiResp.getData().getChoices().get(0).toString(); } catch (Exception e) { e.printStackTrace(); throw new BusinessException(ErrorCode.SYSTEM_ERROR, e.getMessage()); } } /** * 通用流式请求(简化消息传递) * * @param systemMessage * @param userMessage * @param temperature * @return */ public Flowable doStreamRequest(String systemMessage, String userMessage, Float temperature) { List chatMessageList = new ArrayList<>(); ChatMessage systemChatMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), systemMessage); chatMessageList.add(systemChatMessage); ChatMessage userChatMessage = new ChatMessage(ChatMessageRole.USER.value(), userMessage); chatMessageList.add(userChatMessage); return doStreamRequest(chatMessageList, temperature); } /** * 通用流式请求 * * @param messages * @param temperature * @return */ public Flowable doStreamRequest(List messages, Float temperature) { // 构建请求 ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder() .model(Constants.ModelChatGLM4) .stream(Boolean.TRUE) .temperature(temperature) .invokeMethod(Constants.invokeMethod) .messages(messages) .build(); try { ModelApiResponse invokeModelApiResp = clientV4.invokeModelApi(chatCompletionRequest); return invokeModelApiResp.getFlowable(); } catch (Exception e) { e.printStackTrace(); throw new BusinessException(ErrorCode.SYSTEM_ERROR, e.getMessage()); } } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/manager/CosManager.java ================================================ package com.yupi.yudada.manager; import com.qcloud.cos.COSClient; import com.qcloud.cos.model.PutObjectRequest; import com.qcloud.cos.model.PutObjectResult; import com.yupi.yudada.config.CosClientConfig; import java.io.File; import javax.annotation.Resource; import org.springframework.stereotype.Component; /** * Cos 对象存储操作 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ @Component public class CosManager { @Resource private CosClientConfig cosClientConfig; @Resource private COSClient cosClient; /** * 上传对象 * * @param key 唯一键 * @param localFilePath 本地文件路径 * @return */ public PutObjectResult putObject(String key, String localFilePath) { PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key, new File(localFilePath)); return cosClient.putObject(putObjectRequest); } /** * 上传对象 * * @param key 唯一键 * @param file 文件 * @return */ public PutObjectResult putObject(String key, File file) { PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key, file); return cosClient.putObject(putObjectRequest); } } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/mapper/AppMapper.java ================================================ package com.yupi.yudada.mapper; import com.yupi.yudada.model.entity.App; import com.baomidou.mybatisplus.core.mapper.BaseMapper; /** * @author 李鱼皮 * @description 针对表【app(应用)】的数据库操作Mapper * @createDate 2024-05-09 20:41:03 * @Entity com.yupi.yudada.model.entity.App */ public interface AppMapper extends BaseMapper { } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/mapper/PostFavourMapper.java ================================================ package com.yupi.yudada.mapper; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Constants; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.yupi.yudada.model.entity.Post; import com.yupi.yudada.model.entity.PostFavour; import org.apache.ibatis.annotations.Param; /** * 帖子收藏数据库操作 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ public interface PostFavourMapper extends BaseMapper { /** * 分页查询收藏帖子列表 * * @param page * @param queryWrapper * @param favourUserId * @return */ Page listFavourPostByPage(IPage page, @Param(Constants.WRAPPER) Wrapper queryWrapper, long favourUserId); } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/mapper/PostMapper.java ================================================ package com.yupi.yudada.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.yupi.yudada.model.entity.Post; import java.util.Date; import java.util.List; /** * 帖子数据库操作 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ public interface PostMapper extends BaseMapper { /** * 查询帖子列表(包括已被删除的数据) */ List listPostWithDelete(Date minUpdateTime); } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/mapper/PostThumbMapper.java ================================================ package com.yupi.yudada.mapper; import com.yupi.yudada.model.entity.PostThumb; import com.baomidou.mybatisplus.core.mapper.BaseMapper; /** * 帖子点赞数据库操作 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ public interface PostThumbMapper extends BaseMapper { } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/mapper/QuestionMapper.java ================================================ package com.yupi.yudada.mapper; import com.yupi.yudada.model.entity.Question; import com.baomidou.mybatisplus.core.mapper.BaseMapper; /** * @author 李鱼皮 * @description 针对表【question(题目)】的数据库操作Mapper * @createDate 2024-05-09 20:41:03 * @Entity com.yupi.yudada.model.entity.Question */ public interface QuestionMapper extends BaseMapper { } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/mapper/ScoringResultMapper.java ================================================ package com.yupi.yudada.mapper; import com.yupi.yudada.model.entity.ScoringResult; import com.baomidou.mybatisplus.core.mapper.BaseMapper; /** * @author 李鱼皮 * @description 针对表【scoring_result(评分结果)】的数据库操作Mapper * @createDate 2024-05-09 20:41:03 * @Entity com.yupi.yudada.model.entity.ScoringResult */ public interface ScoringResultMapper extends BaseMapper { } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/mapper/UserAnswerMapper.java ================================================ package com.yupi.yudada.mapper; import com.yupi.yudada.model.dto.statistic.AppAnswerCountDTO; import com.yupi.yudada.model.dto.statistic.AppAnswerResultCountDTO; import com.yupi.yudada.model.entity.UserAnswer; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Select; import java.util.List; /** * @author 李鱼皮 * @description 针对表【user_answer(用户答题记录)】的数据库操作Mapper * @createDate 2024-05-09 20:41:03 * @Entity com.yupi.yudada.model.entity.UserAnswer */ public interface UserAnswerMapper extends BaseMapper { @Select("select appId, count(userId) as answerCount from user_answer\n" + " group by appId order by answerCount desc limit 10;") List doAppAnswerCount(); @Select("select resultName, count(resultName) as resultCount from user_answer\n" + " where appId = #{appId}\n" + " group by resultName order by resultCount desc;") List doAppAnswerResultCount(Long appId); } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/mapper/UserMapper.java ================================================ package com.yupi.yudada.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.yupi.yudada.model.entity.User; /** * 用户数据库操作 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ public interface UserMapper extends BaseMapper { } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/model/dto/app/AppAddRequest.java ================================================ package com.yupi.yudada.model.dto.app; import lombok.Data; import java.io.Serializable; /** * 创建应用请求 * * @author 程序员鱼皮 * @from 编程导航学习圈 */ @Data public class AppAddRequest implements Serializable { /** * 应用名 */ private String appName; /** * 应用描述 */ private String appDesc; /** * 应用图标 */ private String appIcon; /** * 应用类型(0-得分类,1-测评类) */ private Integer appType; /** * 评分策略(0-自定义,1-AI) */ private Integer scoringStrategy; private static final long serialVersionUID = 1L; } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/model/dto/app/AppEditRequest.java ================================================ package com.yupi.yudada.model.dto.app; import lombok.Data; import java.io.Serializable; /** * 编辑应用请求 * * @author 程序员鱼皮 * @from 编程导航学习圈 */ @Data public class AppEditRequest implements Serializable { /** * id */ private Long id; /** * 应用名 */ private String appName; /** * 应用描述 */ private String appDesc; /** * 应用图标 */ private String appIcon; /** * 应用类型(0-得分类,1-测评类) */ private Integer appType; /** * 评分策略(0-自定义,1-AI) */ private Integer scoringStrategy; private static final long serialVersionUID = 1L; } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/model/dto/app/AppQueryRequest.java ================================================ package com.yupi.yudada.model.dto.app; import com.yupi.yudada.common.PageRequest; import lombok.Data; import lombok.EqualsAndHashCode; import java.io.Serializable; /** * 查询应用请求 * * @author 程序员鱼皮 * @from 编程导航学习圈 */ @EqualsAndHashCode(callSuper = true) @Data public class AppQueryRequest extends PageRequest implements Serializable { /** * id */ private Long id; /** * 应用名 */ private String appName; /** * 应用描述 */ private String appDesc; /** * 应用图标 */ private String appIcon; /** * 应用类型(0-得分类,1-测评类) */ private Integer appType; /** * 评分策略(0-自定义,1-AI) */ private Integer scoringStrategy; /** * 审核状态:0-待审核, 1-通过, 2-拒绝 */ private Integer reviewStatus; /** * 审核信息 */ private String reviewMessage; /** * 审核人 id */ private Long reviewerId; /** * 创建用户 id */ private Long userId; /** * id */ private Long notId; /** * 搜索词 */ private String searchText; private static final long serialVersionUID = 1L; } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/model/dto/app/AppUpdateRequest.java ================================================ package com.yupi.yudada.model.dto.app; import lombok.Data; import java.io.Serializable; import java.util.Date; /** * 更新应用请求 * * @author 程序员鱼皮 * @from 编程导航学习圈 */ @Data public class AppUpdateRequest implements Serializable { /** * id */ private Long id; /** * 应用名 */ private String appName; /** * 应用描述 */ private String appDesc; /** * 应用图标 */ private String appIcon; /** * 应用类型(0-得分类,1-测评类) */ private Integer appType; /** * 评分策略(0-自定义,1-AI) */ private Integer scoringStrategy; /** * 审核状态:0-待审核, 1-通过, 2-拒绝 */ private Integer reviewStatus; /** * 审核信息 */ private String reviewMessage; /** * 审核人 id */ private Long reviewerId; /** * 审核时间 */ private Date reviewTime; private static final long serialVersionUID = 1L; } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/model/dto/file/UploadFileRequest.java ================================================ package com.yupi.yudada.model.dto.file; import java.io.Serializable; import lombok.Data; /** * 文件上传请求 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ @Data public class UploadFileRequest implements Serializable { /** * 业务 */ private String biz; private static final long serialVersionUID = 1L; } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/model/dto/post/PostAddRequest.java ================================================ package com.yupi.yudada.model.dto.post; import java.io.Serializable; import java.util.List; import lombok.Data; /** * 创建请求 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ @Data public class PostAddRequest implements Serializable { /** * 标题 */ private String title; /** * 内容 */ private String content; /** * 标签列表 */ private List tags; private static final long serialVersionUID = 1L; } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/model/dto/post/PostEditRequest.java ================================================ package com.yupi.yudada.model.dto.post; import java.io.Serializable; import java.util.List; import lombok.Data; /** * 编辑请求 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ @Data public class PostEditRequest implements Serializable { /** * id */ private Long id; /** * 标题 */ private String title; /** * 内容 */ private String content; /** * 标签列表 */ private List tags; private static final long serialVersionUID = 1L; } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/model/dto/post/PostQueryRequest.java ================================================ package com.yupi.yudada.model.dto.post; import com.yupi.yudada.common.PageRequest; import java.io.Serializable; import java.util.List; import lombok.Data; import lombok.EqualsAndHashCode; /** * 查询请求 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ @EqualsAndHashCode(callSuper = true) @Data public class PostQueryRequest extends PageRequest implements Serializable { /** * id */ private Long id; /** * id */ private Long notId; /** * 搜索词 */ private String searchText; /** * 标题 */ private String title; /** * 内容 */ private String content; /** * 标签列表 */ private List tags; /** * 至少有一个标签 */ private List orTags; /** * 创建用户 id */ private Long userId; /** * 收藏用户 id */ private Long favourUserId; private static final long serialVersionUID = 1L; } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/model/dto/post/PostUpdateRequest.java ================================================ package com.yupi.yudada.model.dto.post; import java.io.Serializable; import java.util.List; import lombok.Data; /** * 更新请求 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ @Data public class PostUpdateRequest implements Serializable { /** * id */ private Long id; /** * 标题 */ private String title; /** * 内容 */ private String content; /** * 标签列表 */ private List tags; private static final long serialVersionUID = 1L; } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/model/dto/postfavour/PostFavourAddRequest.java ================================================ package com.yupi.yudada.model.dto.postfavour; import java.io.Serializable; import lombok.Data; /** * 帖子收藏 / 取消收藏请求 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ @Data public class PostFavourAddRequest implements Serializable { /** * 帖子 id */ private Long postId; private static final long serialVersionUID = 1L; } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/model/dto/postfavour/PostFavourQueryRequest.java ================================================ package com.yupi.yudada.model.dto.postfavour; import com.yupi.yudada.common.PageRequest; import com.yupi.yudada.model.dto.post.PostQueryRequest; import java.io.Serializable; import lombok.Data; import lombok.EqualsAndHashCode; /** * 帖子收藏查询请求 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ @Data @EqualsAndHashCode(callSuper = true) public class PostFavourQueryRequest extends PageRequest implements Serializable { /** * 帖子查询请求 */ private PostQueryRequest postQueryRequest; /** * 用户 id */ private Long userId; private static final long serialVersionUID = 1L; } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/model/dto/postthumb/PostThumbAddRequest.java ================================================ package com.yupi.yudada.model.dto.postthumb; import java.io.Serializable; import lombok.Data; /** * 帖子点赞请求 * * @author 程序员鱼皮 * @from 编程导航知识星球 */ @Data public class PostThumbAddRequest implements Serializable { /** * 帖子 id */ private Long postId; private static final long serialVersionUID = 1L; } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/model/dto/question/AiGenerateQuestionRequest.java ================================================ package com.yupi.yudada.model.dto.question; import lombok.Data; import java.io.Serializable; /** * AI 生成题目请求 * * @author 程序员鱼皮 * @from 编程导航学习圈 */ @Data public class AiGenerateQuestionRequest implements Serializable { /** * 应用 id */ private Long appId; /** * 题目数 */ int questionNumber = 10; /** * 选项数 */ int optionNumber = 2; private static final long serialVersionUID = 1L; } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/model/dto/question/QuestionAddRequest.java ================================================ package com.yupi.yudada.model.dto.question; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import lombok.Data; import java.io.Serializable; import java.util.Date; import java.util.List; /** * 创建题目请求 * * @author 程序员鱼皮 * @from 编程导航学习圈 */ @Data public class QuestionAddRequest implements Serializable { /** * 题目内容(json格式) */ private List questionContent; /** * 应用 id */ private Long appId; private static final long serialVersionUID = 1L; } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/model/dto/question/QuestionAnswerDTO.java ================================================ package com.yupi.yudada.model.dto.question; import lombok.Data; /** * 题目答案封装类(用于 AI 评分) * * @author 程序员鱼皮 * @from 编程导航学习圈 */ @Data public class QuestionAnswerDTO { /** * 题目 */ private String title; /** * 用户答案 */ private String userAnswer; } ================================================ FILE: yudada-backend/src/main/java/com/yupi/yudada/model/dto/question/QuestionContentDTO.java ================================================ package com.yupi.yudada.model.dto.question; import lombok.Data; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import java.util.List; @Data @NoArgsConstructor @AllArgsConstructor public class QuestionContentDTO { /** * 题目标题 */ private String title; /** * 题目选项列表 */ private List