[
  {
    "path": ".gitignore",
    "content": "### Intellij template\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn.  Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### WebStorm template\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn.  Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n"
  },
  {
    "path": "README.md",
    "content": "# 鱼答答 - AI 答题应用平台\n\n> 作者：[程序员鱼皮](https://yuyuanweb.feishu.cn/wiki/Abldw5WkjidySxkKxU2cQdAtnah)\n>\n> ⭐️ 加入项目系列学习：[加入编程导航](https://yuyuanweb.feishu.cn/wiki/SDtMwjR1DituVpkz5MLc3fZLnzb) \n\n\n\n## 项目简介\n\n### 项目介绍\n\n深入业务场景的企业级实战项目，基于 Vue 3 + Spring Boot + Redis + ChatGLM AI + RxJava + SSE 的 **AI 答题应用平台。**\n\n用户可以基于 AI 快速制作并发布多种答题应用，支持检索和分享应用、在线答题并基于评分算法或 AI 得到回答总结；管理员可以审核应用、集中管理整站内容，并进行统计分析。\n\n> 视频介绍：https://www.bilibili.com/video/BV1m142197hg\n\n\n\n### 项目四大阶段\n\n该项目选题新颖、业务完整、技术亮点多，为了帮大家循序渐进地学习，鱼皮将项目设计为 4 个阶段，通俗易懂：\n\n1）第一阶段，开发本地的 `MBTI 性格测试小程序`。带大家熟悉答题应用的开发流程和原理，从 0 到 1 实战 Taro 跨端微信小程序开发，并分享小程序开发经验。\n\n![](https://pic.yupi.icu/1/image-20240604145837172.png)\n\n2）第二阶段，上升一个层次，开发 `答题应用平台`。用户可以通过上传题目和自定义评分规则，创建答题应用，供其他用户检索和使用。该阶段涉及 Vue 3 + Spring Boot 前后端全栈项目从 0 到 1 的开发。\n\n![](https://pic.yupi.icu/1/20240604145229177.png)\n\n3）第三阶段，让 AI 为平台赋能，开发 `AI 智能答题应用平台`。用户只需设定主题，就能通过 AI 快速生成题目、让 AI 分析用户答案，极大降低创建答题应用的成本、提高回答多样性。是从 0 到 1 的 AI 应用开发教程，封装 AI 通用模块并教你成为 Prompt 大师！\n\n![](https://pic.yupi.icu/1/20240604145229383.png)\n\n4）第四阶段，通过多种企业开发技术手段进行 `项目优化`。包括 RxJava + SSE 优化 AI 生成体验、通过缓存和分库分表优化性能、通过幂等设计和线程池隔离提高系统安全性、通过统计分析和应用分享功能来将应用 “产品化” 等等，涉及大量干货！\n\n在这个项目中，鱼皮还会带大家用 AI 工具 `CodeGeeX 智能编程助手` 提高开发效率，是不是已经迫不及待了呢？\n\n \n\n### 项目展示\n\n本项目涉及 10 多个页面，前面已经展示部分页面。\n\n应用详情页：\n\n![](https://pic.yupi.icu/1/20240604145229915.png)\n\n用户答题页面：\n\n![](https://pic.yupi.icu/1/20240604145230156.png)\n\n创建应用页：\n\n![](https://pic.yupi.icu/1/20240604145230361.png)\n\n创建题目页，涉及复杂动态嵌套表单的开发：\n\n![](https://pic.yupi.icu/1/20240604145230557.png)\n\n应用管理页面：\n\n![](https://pic.yupi.icu/1/20240604145230731.png)\n\n统计分析页面：\n\n![](https://pic.yupi.icu/1/20240604145230905.png)\n\n应用分享功能：\n\n![](https://pic.yupi.icu/1/20240604145231269.png)\n\n\n\n### 免费试看\n\n感兴趣的同学可以 **免费试看** 第一期项目回放：https://www.bilibili.com/video/BV1m142197hg\n\n\n\n## 项目特点\n\n鱼皮 **从 0 到 1 全程直播无剪辑** 地带大家开发完成项目，包括 **视频教程** 和 **文字教程**！从需求分析、技术选型、项目设计、项目初始化、Demo 编写、前后端开发实现、项目优化、部署上线等，每个环节我都 **从理论到实践** 给大家讲的明明白白、每个细节都不放过！\n\n细致入微的教程：\n\n![](https://pic.yupi.icu/1/20240604145231642.png)\n\n满满的项目正反馈：\n\n![](https://pic.yupi.icu/1/20240604145231908.png)\n\n\n\n### 为什么要带做这个项目？\n\n1）**业务真实新颖**：别人做答题应用，你做 AI 应用平台。需求实用价值更高，可以扩展出很多新奇有趣的热门应用。\n\n2）**技术主流新颖**：基于企业主流前后端技术实现，再结合当下最热门的 AI 技术，比传统项目更有亮点。\n\n3）**能学到东西**：不再是增删改查的项目，而是包含了大量的实际业务场景、系统设计优化、企业级解决方案。\n\n4）**教程资料少**：市面上虽然有 AI 应用平台，但几乎没有从 0 到 1 的实战教程，鱼皮将提供细致入微的讲解。\n\n5）**增加竞争力**：区别于各种管理平台项目，本项目涉及响应式编程、分库分表、设计模式、性能优化、多角度系统优化、产品优化的实战，给你的简历增加竞争力。\n\n\n\n### 项目收获\n\n鱼皮给大家讲的都是 **通用的项目开发方法和架构设计套路**，从这个项目中你可以学到：\n\n- 如何拆解复杂业务，从 0 开始设计实现系统？\n- 如何快速开发小程序、响应式网站和后端项目？\n- 如何自己制作一套 Vue 3 万用前端模板？\n- 如何巧用 JSON 实现复杂评分策略？\n- 如何巧妙利用设计模式来优化代码？\n- 如何利用 AI 工具 `CodeGeeX 智能编程助手` 提高开发效率？\n- 如何利用 SSE 技术实时推送通知？\n- 如何利用 Redis + Caffeine + 分布式锁实现稳定高效的缓存？\n- 如何通过 RxJava 反应式编程 + 分库分表提升服务性能？\n- 如何通过幂等设计、线程池隔离提升系统安全稳定性？\n\n此外，还能学会很多思考问题、对比方案、产品设计的方法，提升排查问题、自主解决 Bug、产品理解的能力，成为一个项目负责人。\n\n\n\n### 鱼皮系列项目优势\n\n鱼皮原创项目系列以 **实战** 为主，用 **全程直播** 的方式，**从 0 到 1** 带大家学习技术知识，并立即实践运用到项目中，做到学以致用。\n\n此外，还提供如下服务：\n\n- 详细的直播笔记（本项目有全套文字教程）\n- 完整的项目源码（分节的代码，更易学习）\n- 答疑解惑\n- 专属项目交流群\n- ⭐️ 现成的简历写法（直接写满简历）\n- ⭐️ 项目的扩展思路（拉开和其他人的差距）\n- ⭐️ 项目相关面试题、题解和真实面经（提前准备，面试不懵逼）\n- ⭐️ 前端 + Java 后端万用项目模板（快速创建项目）\n\n比起看网上的教程学习，鱼皮项目系列的优势：\n\n> 从学知识 => 实践项目 => 复习笔记 => 项目答疑 => 简历写法 => 面试题解的一条龙服务\n\n从需求分析、技术选型、项目设计、项目初始化、Demo 编写、前后端开发实现、项目优化、部署上线等，每个环节我都 **从理论到实践** 给大家讲的明明白白、每个细节都不放过！\n\n编程导航已有 **10 多套项目教程！** 每个项目的学习重点不同，几乎全都是前端 + 后端的 **全栈** 项目 。\n\n详细请见：https://yuyuanweb.feishu.cn/wiki/SePYwTc9tipQiCktw7Uc7kujnCd\n\n\n\n## 架构设计\n\n### 1、核心业务流程图\n\n![](https://pic.yupi.icu/1/20240604145232082.png)\n\n### 2、时序图\n\n![](https://pic.yupi.icu/1/20240604145232239.png)\n\n### 3、架构设计图\n\n![](https://pic.yupi.icu/1/20240604145232474.png)\n\n\n\n## 技术选型\n\n### 后端\n\n- Java Spring Boot 开发框架（万用后端模板）\n- 存储层：MySQL 数据库 + Redis 缓存 + 腾讯云 COS 对象存储\n- MyBatis-Plus 及 MyBatis X 自动生成\n- Redisson 分布式锁\n- Caffeine 本地缓存\n- ⭐️ 基于 ChatGLM 大模型的通用 AI 能力\n- ⭐️ RxJava 响应式框架 + 线程池隔离实战 \n- ⭐️ SSE 服务端推送\n- ⭐️ Shardingsphere 分库分表\n- ⭐️ 幂等设计 + 分布式 ID 雪花算法\n- ⭐️ 多种设计模式\n- ⭐️ 多角度项目优化：性能、稳定性、成本优化、产品优化等\n\n\n\n### 前端\n\n#### Web 网页开发\n\n- Vue 3 \n- Vue-CLI 脚手架\n- Axios 请求库\n- Arco Design 组件库\n- 前端工程化：ESLint + Prettier + TypeScript\n- 富文本编辑器\n- QRCode.js 二维码生成\n- ⭐️ Pinia 状态管理\n- ⭐️ OpenAPI 前端代码生成\n\n#### 小程序开发\n\n- React\n- Taro 跨端开发框架\n- Taro UI 组件库\n\n### 开发工具\n\n- 前端 IDE：JetBrains WebStorm\n- 后端 IDE：JetBrains IDEA\n- [CodeGeeX 智能编程助手](https://codegeex.cn/)\n\n\n\n## 项目大纲\n\n### 第一阶段：MBTI 性格测试小程序\n\n1. 项目介绍 | 项目背景和优势\n2. 项目介绍 | 核心业务流程\n3. 项目介绍 | 项目功能梳理\n4. 项目介绍 | 技术选型和架构设计\n5. MBTI 小程序 | 性格测试应用介绍\n6. MBTI 小程序 | 实现方案和评分原理\n7. MBTI 小程序 | Taro + React 小程序入门\n8. MBTI 小程序 | 小程序开发实战\n9. MBTI 小程序 | 小程序开发常用解决方案\n\n### 第二阶段：Web 答题应用平台\n\n1. 平台开发 | 需求分析\n2. 平台开发 | 库表设计\n3. 平台开发 | 后端初始化\n4. 平台开发 | 后端基础开发\n5. 平台开发 | 后端核心业务流程开发\n6. 平台开发 | 前端技术选型\n7. 平台开发 | 前端项目初始化\n8. 平台开发 | 前端 Vue 3万用模板开发\n9. 平台开发 | 前端基础页面开发（管理页面）\n10. 平台开发 | 前端应用主页开发\n11. 平台开发 | 前端应用详情页开发\n12. 平台开发 | 前端创建模块开发 - 创建应用\n13. 平台开发 | 前端创建模块开发 - 创建题目\n14. 平台开发 | 前端创建模块开发 - 创建评分\n15. 平台开发 | 前端答题模块开发 - 应用答题\n16. 平台开发 | 前端答题模块开发 - 答题结果\n17. 平台开发 | 前端答题模块开发 - 我的回答\n\n### 第三阶段：AI 智能答题应用平台\n\n1. 平台智能化 | 智谱 AI 大模型介绍\n2. 平台智能化 | 智谱 AI SDK 接入\n3. 平台智能化 | 通用 AI 模块封装\n4. 平台智能化 | AI 生成题目 - 方案设计（Prompt）\n5. 平台智能化 | AI 生成题目 - 后端开发\n6. 平台智能化 | AI 生成题目 - 前端开发\n7. 平台智能化 | AI 智能评分 - 方案设计（Prompt）\n8. 平台智能化 | AI 智能评分 - 后端开发\n9. 平台智能化 | AI 智能评分 - 前端开发\n10. 扩展知识 | Spring AI\n11. 扩展知识 | 智谱 AI + Spring AI 整合应用\n\n### 第四阶段：多角度项目优化\n\n1. 性能优化 | RxJava 响应式编程 - 核心概念\n2. 性能优化 | RxJava 响应式编程 - Demo 实操\n3. 性能优化 | AI 生成题目优化 - 需求分析\n4. 性能优化 | AI 生成题目优化 - 前后端实时通讯（SSE 技术）\n5. 性能优化 | AI 生成题目优化 - 后端开发\n6. 性能优化 | AI 生成题目优化 - 前端开发\n7. 性能优化 | AI 评分优化 - 需求分析\n8. 性能优化 | AI 评分优化 - 方案设计（缓存设计）\n9. 性能优化 | AI 评分优化 - 后端本地缓存开发\n10. 性能优化 | AI 评分优化 - Redisson 解决缓存击穿\n11. 性能优化 | 分库分表 - 核心概念\n12. 性能优化 | 分库分表 - 技术选型\n13. 性能优化 | 分库分表 - Sharding JDBC 实战\n14. 系统优化 | 幂等设计 - 主流方案\n15. 系统优化 | 幂等设计 - 分布式唯一 id 生成\n16. 系统优化 | 幂等设计 - 后端开发\n17. 系统优化 | 幂等设计 - 前端开发\n18. 系统优化 | 线程池隔离 - 方案设计\n19. 系统优化 | 线程池隔离 - 开发实现\n20. 系统优化 | 统计分析 - 方案选型\n21. 系统优化 | 统计分析 - 自定义 SQL\n22. 系统优化 | 统计分析 - 后端开发\n23. 系统优化 | 统计分析 - 前端可视化\n24. 系统优化 | 应用分享 - 移动端扫码分享\n25. 系统优化 | 应用分享 - 通用分享组件\n\n\n\n## 项目资料\n\n包括：\n\n- 学习计划、视频教程、文字教程、项目源码\n- 项目答疑、项目交流群、学员笔记\n- 简历写法、面试题解、扩展思路\n\n以上资料均可在编程导航网站获取：https://www.code-nav.cn/course/1790274408835506178\n\n点击 [加入编程导航](https://yuyuanweb.feishu.cn/wiki/SDtMwjR1DituVpkz5MLc3fZLnzb) 后，可以按照帖子 https://t.zsxq.com/eJxjY 的引导认证并解锁项目资料的权限。如图：\n\n![](https://pic.yupi.icu/1/20240604145232643.png)\n\n\n\n## 更多项目\n\n请见：[项目实战 - 鱼皮原创项目教程系列](https://yuyuanweb.feishu.cn/wiki/SePYwTc9tipQiCktw7Uc7kujnCd)\n\n\n\n## 加入学习\n\n欢迎 [点此加入编程导航](https://yuyuanweb.feishu.cn/wiki/SDtMwjR1DituVpkz5MLc3fZLnzb) ，学习大量优质原创项目，享受更多原创资料，开启你的编程起飞之旅~\n\n"
  },
  {
    "path": "mbti-test-mini/.editorconfig",
    "content": "# http://editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.md]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": "mbti-test-mini/.eslintrc",
    "content": "{\n  \"extends\": [\"taro/react\"],\n  \"rules\": {\n    \"react/jsx-uses-react\": \"off\",\n    \"react/react-in-jsx-scope\": \"off\",\n    \"jsx-quotes\": \"off\"\n  }\n}\n"
  },
  {
    "path": "mbti-test-mini/babel.config.js",
    "content": "// babel-preset-taro 更多选项和默认值：\n// https://github.com/NervJS/taro/blob/next/packages/babel-preset-taro/README.md\nmodule.exports = {\n  presets: [\n    ['taro', {\n      framework: 'react',\n      ts: true\n    }]\n  ]\n}\n"
  },
  {
    "path": "mbti-test-mini/config/dev.ts",
    "content": "module.exports = {\n  env: {\n    NODE_ENV: '\"development\"'\n  },\n  defineConstants: {\n  },\n  mini: {},\n  h5: {\n    esnextModules: ['taro-ui']\n  }\n}\n"
  },
  {
    "path": "mbti-test-mini/config/index.ts",
    "content": "const config = {\n  projectName: 'mbti-test-mini',\n  date: '2024-5-7',\n  designWidth: 750,\n  deviceRatio: {\n    640: 2.34 / 2,\n    750: 1,\n    828: 1.81 / 2\n  },\n  sourceRoot: 'src',\n  outputRoot: 'dist',\n  plugins: [],\n  defineConstants: {\n  },\n  copy: {\n    patterns: [\n    ],\n    options: {\n    }\n  },\n  framework: 'react',\n  compiler: 'webpack5',\n  cache: {\n    enable: false // Webpack 持久化缓存配置，建议开启。默认配置请参考：https://docs.taro.zone/docs/config-detail#cache\n  },\n  mini: {\n    postcss: {\n      pxtransform: {\n        enable: true,\n        config: {\n\n        }\n      },\n      url: {\n        enable: true,\n        config: {\n          limit: 1024 // 设定转换尺寸上限\n        }\n      },\n      cssModules: {\n        enable: false, // 默认为 false，如需使用 css modules 功能，则设为 true\n        config: {\n          namingPattern: 'module', // 转换模式，取值为 global/module\n          generateScopedName: '[name]__[local]___[hash:base64:5]'\n        }\n      }\n    }\n  },\n  h5: {\n    publicPath: '/',\n    staticDirectory: 'static',\n    postcss: {\n      autoprefixer: {\n        enable: true,\n        config: {\n        }\n      },\n      cssModules: {\n        enable: false, // 默认为 false，如需使用 css modules 功能，则设为 true\n        config: {\n          namingPattern: 'module', // 转换模式，取值为 global/module\n          generateScopedName: '[name]__[local]___[hash:base64:5]'\n        }\n      }\n    }\n  }\n}\n\nmodule.exports = function (merge) {\n  if (process.env.NODE_ENV === 'development') {\n    return merge({}, config, require('./dev'))\n  }\n  return merge({}, config, require('./prod'))\n}\n"
  },
  {
    "path": "mbti-test-mini/config/prod.ts",
    "content": "module.exports = {\n  env: {\n    NODE_ENV: '\"production\"'\n  },\n  defineConstants: {\n  },\n  mini: {},\n  h5: {\n    /**\n     * WebpackChain 插件配置\n     * @docs https://github.com/neutrinojs/webpack-chain\n     */\n    // webpackChain (chain) {\n    //   /**\n    //    * 如果 h5 端编译后体积过大，可以使用 webpack-bundle-analyzer 插件对打包体积进行分析。\n    //    * @docs https://github.com/webpack-contrib/webpack-bundle-analyzer\n    //    */\n    //   chain.plugin('analyzer')\n    //     .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, [])\n\n    //   /**\n    //    * 如果 h5 端首屏加载时间过长，可以使用 prerender-spa-plugin 插件预加载首页。\n    //    * @docs https://github.com/chrisvfritz/prerender-spa-plugin\n    //    */\n    //   const path = require('path')\n    //   const Prerender = require('prerender-spa-plugin')\n    //   const staticDir = path.join(__dirname, '..', 'dist')\n    //   chain\n    //     .plugin('prerender')\n    //     .use(new Prerender({\n    //       staticDir,\n    //       routes: [ '/pages/index/index' ],\n    //       postProcess: (context) => ({ ...context, outputPath: path.join(staticDir, 'index.html') })\n    //     }))\n    // }\n  }\n}\n"
  },
  {
    "path": "mbti-test-mini/package.json",
    "content": "{\n  \"name\": \"mbti-test-mini\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"description\": \"\",\n  \"templateInfo\": {\n    \"name\": \"taro-ui\",\n    \"typescript\": true,\n    \"css\": \"Sass\",\n    \"framework\": \"React\"\n  },\n  \"scripts\": {\n    \"build:weapp\": \"taro build --type weapp\",\n    \"build:swan\": \"taro build --type swan\",\n    \"build:alipay\": \"taro build --type alipay\",\n    \"build:tt\": \"taro build --type tt\",\n    \"build:h5\": \"taro build --type h5\",\n    \"build:rn\": \"taro build --type rn\",\n    \"build:qq\": \"taro build --type qq\",\n    \"build:jd\": \"taro build --type jd\",\n    \"build:quickapp\": \"taro build --type quickapp\",\n    \"dev:weapp\": \"npm run build:weapp -- --watch\",\n    \"dev:swan\": \"npm run build:swan -- --watch\",\n    \"dev:alipay\": \"npm run build:alipay -- --watch\",\n    \"dev:tt\": \"npm run build:tt -- --watch\",\n    \"dev:h5\": \"npm run build:h5 -- --watch\",\n    \"dev:rn\": \"npm run build:rn -- --watch\",\n    \"dev:qq\": \"npm run build:qq -- --watch\",\n    \"dev:jd\": \"npm run build:jd -- --watch\",\n    \"dev:quickapp\": \"npm run build:quickapp -- --watch\"\n  },\n  \"browserslist\": [\n    \"last 3 versions\",\n    \"Android >= 4.1\",\n    \"ios >= 8\"\n  ],\n  \"author\": \"\",\n  \"dependencies\": {\n    \"@babel/runtime\": \"^7.7.7\",\n    \"@tarojs/components\": \"3.6.28\",\n    \"@tarojs/helper\": \"3.6.28\",\n    \"@tarojs/plugin-platform-weapp\": \"3.6.28\",\n    \"@tarojs/plugin-platform-alipay\": \"3.6.28\",\n    \"@tarojs/plugin-platform-tt\": \"3.6.28\",\n    \"@tarojs/plugin-platform-swan\": \"3.6.28\",\n    \"@tarojs/plugin-platform-jd\": \"3.6.28\",\n    \"@tarojs/plugin-platform-qq\": \"3.6.28\",\n    \"@tarojs/plugin-platform-h5\": \"3.6.28\",\n    \"@tarojs/runtime\": \"3.6.28\",\n    \"@tarojs/shared\": \"3.6.28\",\n    \"@tarojs/taro\": \"3.6.28\",\n    \"@tarojs/plugin-framework-react\": \"3.6.28\",\n    \"lodash\": \"4.17.15\",\n    \"taro-ui\": \"^3.2.1\",\n    \"@tarojs/react\": \"3.6.28\",\n    \"react\": \"^18.0.0\",\n    \"react-dom\": \"^18.0.0\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.8.0\",\n    \"@tarojs/cli\": \"3.6.28\",\n    \"postcss\": \"^8.4.18\",\n    \"webpack\": \"^5.78.0\",\n    \"@tarojs/taro-loader\": \"3.6.28\",\n    \"@tarojs/webpack5-runner\": \"3.6.28\",\n    \"@pmmmwh/react-refresh-webpack-plugin\": \"^0.5.5\",\n    \"react-refresh\": \"^0.11.0\",\n    \"@types/webpack-env\": \"^1.13.6\",\n    \"babel-preset-taro\": \"3.6.28\",\n    \"eslint\": \"^8.12.0\",\n    \"eslint-config-taro\": \"3.6.28\",\n    \"stylelint\": \"9.3.0\",\n    \"@typescript-eslint/parser\": \"^5.20.0\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.20.0\",\n    \"typescript\": \"^4.1.0\",\n    \"@types/react\": \"^18.0.0\",\n    \"eslint-plugin-react\": \"^7.8.2\",\n    \"eslint-plugin-import\": \"^2.12.0\",\n    \"eslint-plugin-react-hooks\": \"^4.2.0\",\n    \"ts-node\": \"^10.9.1\",\n    \"@types/node\": \"^18.15.11\"\n  }\n}\n"
  },
  {
    "path": "mbti-test-mini/project.config.json",
    "content": "{\n  \"miniprogramRoot\": \"dist/\",\n  \"projectname\": \"mbti-test-mini\",\n  \"description\": \"\",\n  \"appid\": \"wx370d9458c983e21d\",\n  \"setting\": {\n    \"urlCheck\": true,\n    \"es6\": false,\n    \"enhance\": false,\n    \"compileHotReLoad\": false,\n    \"postcss\": false,\n    \"preloadBackgroundData\": false,\n    \"minified\": false,\n    \"newFeature\": true,\n    \"autoAudits\": false,\n    \"coverView\": true,\n    \"showShadowRootInWxmlPanel\": false,\n    \"scopeDataCheck\": false,\n    \"useCompilerModule\": false,\n    \"babelSetting\": {\n      \"ignore\": [],\n      \"disablePlugins\": [],\n      \"outputPath\": \"\"\n    }\n  },\n  \"compileType\": \"miniprogram\",\n  \"simulatorType\": \"wechat\",\n  \"simulatorPluginLibVersion\": {},\n  \"condition\": {},\n  \"libVersion\": \"3.4.3\",\n  \"srcMiniprogramRoot\": \"dist/\",\n  \"packOptions\": {\n    \"ignore\": [],\n    \"include\": []\n  },\n  \"editorSetting\": {\n    \"tabIndent\": \"insertSpaces\",\n    \"tabSize\": 2\n  }\n}"
  },
  {
    "path": "mbti-test-mini/project.private.config.json",
    "content": "{\n  \"description\": \"项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档：https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html\",\n  \"projectname\": \"mbti-test-mini\",\n  \"setting\": {\n    \"compileHotReLoad\": true\n  }\n}"
  },
  {
    "path": "mbti-test-mini/project.tt.json",
    "content": "{\n  \"miniprogramRoot\": \"./\",\n  \"projectname\": \"mbti-test-mini\",\n  \"description\": \"\",\n  \"appid\": \"touristappid\",\n  \"setting\": {\n    \"urlCheck\": true,\n    \"es6\": false,\n    \"postcss\": false,\n    \"minified\": false\n  },\n  \"compileType\": \"miniprogram\"\n}\n"
  },
  {
    "path": "mbti-test-mini/src/app.config.ts",
    "content": "export default defineAppConfig({\n  pages: [\"pages/index/index\", \"pages/result/index\", \"pages/doQuestion/index\"],\n  window: {\n    backgroundTextStyle: \"light\",\n    navigationBarBackgroundColor: \"#fff\",\n    navigationBarTitleText: \"鱼皮 MBTI 性格测试\",\n    navigationBarTextStyle: \"black\",\n  },\n});\n"
  },
  {
    "path": "mbti-test-mini/src/app.scss",
    "content": ".at-button--primary {\n  background: #806497;\n  border-color: #806497;\n}\n"
  },
  {
    "path": "mbti-test-mini/src/app.ts",
    "content": "import Taro, { useLaunch } from \"@tarojs/taro\";\nimport { PropsWithChildren } from \"react\";\nimport \"taro-ui/dist/style/index.scss\"; // 引入组件样式 - 方式一\nimport \"./app.scss\";\n\nfunction App({ children }: PropsWithChildren) {\n  useLaunch(async () => {\n    const res = await Taro.login();\n    console.log(res);\n    // todo 拿到 res.code 后，调用后端登录\n  });\n\n  return children;\n}\n\nexport default App;\n"
  },
  {
    "path": "mbti-test-mini/src/components/GlobalFooter/index.scss",
    "content": ".globalFooter {\n  position: fixed;\n  bottom: 16px;\n  left: 0;\n  right: 0;\n  text-align: center;\n}\n"
  },
  {
    "path": "mbti-test-mini/src/components/GlobalFooter/index.tsx",
    "content": "import { View } from \"@tarojs/components\";\nimport \"./index.scss\";\n\n/**\n * 全局底部栏组件\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\nexport default () => {\n  return (\n    <View className=\"globalFooter\">\n      作者：程序员鱼皮\n    </View>\n  );\n};\n"
  },
  {
    "path": "mbti-test-mini/src/data/question_results.json",
    "content": "[\n  {\n    \"resultProp\": [\n      \"I\",\n      \"S\",\n      \"T\",\n      \"J\"\n    ],\n    \"resultDesc\": \"忠诚可靠，被公认为务实，注重细节。\",\n    \"resultPicture\": \"icon_url_istj\",\n    \"resultName\": \"ISTJ（物流师）\"\n  },\n  {\n    \"resultProp\": [\n      \"I\",\n      \"S\",\n      \"F\",\n      \"J\"\n    ],\n    \"resultDesc\": \"善良贴心，以同情心和责任为特点。\",\n    \"resultPicture\": \"icon_url_isfj\",\n    \"resultName\": \"ISFJ（守护者）\"\n  },\n  {\n    \"resultProp\": [\n      \"I\",\n      \"N\",\n      \"F\",\n      \"J\"\n    ],\n    \"resultDesc\": \"理想主义者，有着深刻的洞察力，善于理解他人。\",\n    \"resultPicture\": \"icon_url_infj\",\n    \"resultName\": \"INFJ（占有者）\"\n  },\n  {\n    \"resultProp\": [\n      \"I\",\n      \"N\",\n      \"T\",\n      \"J\"\n    ],\n    \"resultDesc\": \"独立思考者，善于规划和实现目标，理性而果断。\",\n    \"resultPicture\": \"icon_url_intj\",\n    \"resultName\": \"INTJ（设计师）\"\n  },\n  {\n    \"resultProp\": [\n      \"I\",\n      \"S\",\n      \"T\",\n      \"P\"\n    ],\n    \"resultDesc\": \"冷静自持，善于解决问题，擅长实践技能。\",\n    \"resultPicture\": \"icon_url_istp\",\n    \"resultName\": \"ISTP（运动员）\"\n  },\n  {\n    \"resultProp\": [\n      \"I\",\n      \"S\",\n      \"F\",\n      \"P\"\n    ],\n    \"resultDesc\": \"具有艺术感和敏感性，珍视个人空间和自由。\",\n    \"resultPicture\": \"icon_url_isfp\",\n    \"resultName\": \"ISFP（艺术家）\"\n  },\n  {\n    \"resultProp\": [\n      \"I\",\n      \"N\",\n      \"F\",\n      \"P\"\n    ],\n    \"resultDesc\": \"理想主义者，富有创造力，以同情心和理解他人著称。\",\n    \"resultPicture\": \"icon_url_infp\",\n    \"resultName\": \"INFP（治愈者）\"\n  },\n  {\n    \"resultProp\": [\n      \"I\",\n      \"N\",\n      \"T\",\n      \"P\"\n    ],\n    \"resultDesc\": \"思维清晰，探索精神，独立思考且理性。\",\n    \"resultPicture\": \"icon_url_intp\",\n    \"resultName\": \"INTP（学者）\"\n  },\n  {\n    \"resultProp\": [\n      \"E\",\n      \"S\",\n      \"T\",\n      \"P\"\n    ],\n    \"resultDesc\": \"敢于冒险，乐于冒险，思维敏捷，行动果断。\",\n    \"resultPicture\": \"icon_url_estp\",\n    \"resultName\": \"ESTP（拓荒者）\"\n  },\n  {\n    \"resultProp\": [\n      \"E\",\n      \"S\",\n      \"F\",\n      \"P\"\n    ],\n    \"resultDesc\": \"热情开朗，善于社交，热爱生活，乐于助人。\",\n    \"resultPicture\": \"icon_url_esfp\",\n    \"resultName\": \"ESFP（表演者）\"\n  },\n  {\n    \"resultProp\": [\n      \"E\",\n      \"N\",\n      \"F\",\n      \"P\"\n    ],\n    \"resultDesc\": \"富有想象力，充满热情，善于激发他人的活力和潜力。\",\n    \"resultPicture\": \"icon_url_enfp\",\n    \"resultName\": \"ENFP（倡导者）\"\n  },\n  {\n    \"resultProp\": [\n      \"E\",\n      \"N\",\n      \"T\",\n      \"P\"\n    ],\n    \"resultDesc\": \"充满创造力，善于辩论，挑战传统，喜欢探索新领域。\",\n    \"resultPicture\": \"icon_url_entp\",\n    \"resultName\": \"ENTP（发明家）\"\n  },\n  {\n    \"resultProp\": [\n      \"E\",\n      \"S\",\n      \"T\",\n      \"J\"\n    ],\n    \"resultDesc\": \"务实果断，善于组织和管理，重视效率和目标。\",\n    \"resultPicture\": \"icon_url_estj\",\n    \"resultName\": \"ESTJ（主管）\"\n  },\n  {\n    \"resultProp\": [\n      \"E\",\n      \"S\",\n      \"F\",\n      \"J\"\n    ],\n    \"resultDesc\": \"友善热心，以协调、耐心和关怀为特点，善于团队合作。\",\n    \"resultPicture\": \"icon_url_esfj\",\n    \"resultName\": \"ESFJ（尽责者）\"\n  },\n  {\n    \"resultProp\": [\n      \"E\",\n      \"N\",\n      \"F\",\n      \"J\"\n    ],\n    \"resultDesc\": \"热情关爱，善于帮助他人，具有领导力和社交能力。\",\n    \"resultPicture\": \"icon_url_enfj\",\n    \"resultName\": \"ENFJ（教导着）\"\n  },\n  {\n    \"resultProp\": [\n      \"E\",\n      \"N\",\n      \"T\",\n      \"J\"\n    ],\n    \"resultDesc\": \"果断自信，具有领导才能，善于规划和执行目标。\",\n    \"resultPicture\": \"icon_url_entj\",\n    \"resultName\": \"ENTJ（统帅）\"\n  }\n]\n"
  },
  {
    "path": "mbti-test-mini/src/data/questions.json",
    "content": "[\n  {\n    \"options\": [\n      {\n        \"result\": \"I\",\n        \"value\": \"独自工作\",\n        \"key\": \"A\"\n      },\n      {\n        \"result\": \"E\",\n        \"value\": \"与他人合作\",\n        \"key\": \"B\"\n      }\n    ],\n    \"title\": \"你通常更喜欢\"\n  },\n  {\n    \"options\": [\n      {\n        \"result\": \"J\",\n        \"value\": \"喜欢有明确的计划\",\n        \"key\": \"A\"\n      },\n      {\n        \"result\": \"P\",\n        \"value\": \"更愿意随机应变\",\n        \"key\": \"B\"\n      }\n    ],\n    \"title\": \"当安排活动时\"\n  },\n  {\n    \"options\": [\n      {\n        \"result\": \"T\",\n        \"value\": \"认为应该严格遵守\",\n        \"key\": \"A\"\n      },\n      {\n        \"result\": \"F\",\n        \"value\": \"认为应灵活运用\",\n        \"key\": \"B\"\n      }\n    ],\n    \"title\": \"你如何看待规则\"\n  },\n  {\n    \"options\": [\n      {\n        \"result\": \"E\",\n        \"value\": \"经常是说话的人\",\n        \"key\": \"A\"\n      },\n      {\n        \"result\": \"I\",\n        \"value\": \"更倾向于倾听\",\n        \"key\": \"B\"\n      }\n    ],\n    \"title\": \"在社交场合中\"\n  },\n  {\n    \"options\": [\n      {\n        \"result\": \"J\",\n        \"value\": \"先研究再行动\",\n        \"key\": \"A\"\n      },\n      {\n        \"result\": \"P\",\n        \"value\": \"边做边学习\",\n        \"key\": \"B\"\n      }\n    ],\n    \"title\": \"面对新的挑战\"\n  },\n  {\n    \"options\": [\n      {\n        \"result\": \"S\",\n        \"value\": \"注重细节和事实\",\n        \"key\": \"A\"\n      },\n      {\n        \"result\": \"N\",\n        \"value\": \"注重概念和想象\",\n        \"key\": \"B\"\n      }\n    ],\n    \"title\": \"在日常生活中\"\n  },\n  {\n    \"options\": [\n      {\n        \"result\": \"T\",\n        \"value\": \"更多基于逻辑分析\",\n        \"key\": \"A\"\n      },\n      {\n        \"result\": \"F\",\n        \"value\": \"更多基于个人情感\",\n        \"key\": \"B\"\n      }\n    ],\n    \"title\": \"做决定时\"\n  },\n  {\n    \"options\": [\n      {\n        \"result\": \"S\",\n        \"value\": \"喜欢有结构和常规\",\n        \"key\": \"A\"\n      },\n      {\n        \"result\": \"N\",\n        \"value\": \"喜欢自由和灵活性\",\n        \"key\": \"B\"\n      }\n    ],\n    \"title\": \"对于日常安排\"\n  },\n  {\n    \"options\": [\n      {\n        \"result\": \"P\",\n        \"value\": \"首先考虑可能性\",\n        \"key\": \"A\"\n      },\n      {\n        \"result\": \"J\",\n        \"value\": \"首先考虑后果\",\n        \"key\": \"B\"\n      }\n    ],\n    \"title\": \"当遇到问题时\"\n  },\n  {\n    \"options\": [\n      {\n        \"result\": \"T\",\n        \"value\": \"时间是一种宝贵的资源\",\n        \"key\": \"A\"\n      },\n      {\n        \"result\": \"F\",\n        \"value\": \"时间是相对灵活的概念\",\n        \"key\": \"B\"\n      }\n    ],\n    \"title\": \"你如何看待时间\"\n  }\n]\n"
  },
  {
    "path": "mbti-test-mini/src/index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\">\n  <meta content=\"width=device-width,initial-scale=1,user-scalable=no\" name=\"viewport\">\n  <meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n  <meta name=\"apple-touch-fullscreen\" content=\"yes\">\n  <meta name=\"format-detection\" content=\"telephone=no,address=no\">\n  <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"white\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\" >\n  <title>mbti-test-mini</title>\n  <script><%= htmlWebpackPlugin.options.script %></script>\n</head>\n<body>\n  <div id=\"app\"></div>\n</body>\n</html>\n"
  },
  {
    "path": "mbti-test-mini/src/pages/doQuestion/index.config.ts",
    "content": "export default definePageConfig({\n  // navigationBarTitleText: ''\n})\n"
  },
  {
    "path": "mbti-test-mini/src/pages/doQuestion/index.scss",
    "content": ".doQuestionPage {\n\n  .title {\n    margin-bottom: 48px;\n  }\n\n  .options-wrapper {\n    margin-bottom: 48px;\n  }\n\n  .controlBtn {\n    margin: 24px 48px;\n  }\n}\n"
  },
  {
    "path": "mbti-test-mini/src/pages/doQuestion/index.tsx",
    "content": "import {View} from \"@tarojs/components\";\nimport Taro from \"@tarojs/taro\";\nimport {AtButton, AtRadio} from \"taro-ui\";\nimport {useEffect, useState} from \"react\";\nimport GlobalFooter from \"../../components/GlobalFooter\";\nimport questions from \"../../data/questions.json\";\nimport \"./index.scss\";\n\n/**\n * 做题页面\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\nexport default () => {\n  // 当前题目序号（从 1 开始）\n  const [current, setCurrent] = useState<number>(1);\n  // 当前题目\n  const [currentQuestion, setCurrentQuestion] = useState<Question>(questions[0]);\n  const questionOptions = currentQuestion.options.map((option) => {\n    return {label: `${option.key}. ${option.value}`, value: option.key};\n  });\n  // 当前答案\n  const [currentAnswer, setCurrentAnswer] = useState<string>();\n  // 回答列表\n  const [answerList] = useState<string[]>([]);\n\n  // 序号变化时，切换当前题目和当前回答\n  useEffect(() => {\n    setCurrentQuestion(questions[current - 1]);\n    setCurrentAnswer(answerList[current - 1]);\n  }, [current]);\n\n  return (\n    <View className=\"doQuestionPage\">\n      <View className=\"at-article__h2 title\">\n        {current}、{currentQuestion.title}\n      </View>\n      <View className=\"options-wrapper\">\n        <AtRadio\n          options={questionOptions}\n          value={currentAnswer}\n          onClick={(value) => {\n            setCurrentAnswer(value);\n            // 记录回答\n            answerList[current - 1] = value;\n          }}\n        />\n      </View>\n      {current < questions.length && (\n        <AtButton\n          type=\"primary\"\n          circle\n          className=\"controlBtn\"\n          disabled={!currentAnswer}\n          onClick={() => setCurrent(current + 1)}\n        >\n          下一题\n        </AtButton>\n      )}\n      {current == questions.length && (\n        <AtButton\n          type=\"primary\"\n          circle\n          className=\"controlBtn\"\n          disabled={!currentAnswer}\n          onClick={() => {\n            // 传递答案\n            Taro.setStorageSync(\"answerList\", answerList);\n            // 跳转到结果页面\n            Taro.navigateTo({\n              url: \"/pages/result/index\",\n            });\n          }}\n        >\n          查看结果\n        </AtButton>\n      )}\n      {current > 1 && (\n        <AtButton\n          circle\n          className=\"controlBtn\"\n          onClick={() => setCurrent(current - 1)}\n        >\n          上一题\n        </AtButton>\n      )}\n      <GlobalFooter/>\n    </View>\n  );\n};\n"
  },
  {
    "path": "mbti-test-mini/src/pages/index/index.config.ts",
    "content": "export default definePageConfig({\n  // navigationBarTitleText: ''\n})\n"
  },
  {
    "path": "mbti-test-mini/src/pages/index/index.scss",
    "content": ".indexPage {\n  background: #A2C7D7;\n\n  .title {\n    color: white;\n    padding-top: 48px;\n    text-align: center;\n  }\n\n  .subTitle {\n    color: white;\n    margin-bottom: 48px;\n  }\n\n  .enterBtn {\n    width: 60vw;\n  }\n}\n"
  },
  {
    "path": "mbti-test-mini/src/pages/index/index.tsx",
    "content": "import {View, Image} from \"@tarojs/components\";\nimport {AtButton} from \"taro-ui\";\nimport Taro from \"@tarojs/taro\";\nimport headerBg from \"../../assets/headerBg.jpg\";\nimport GlobalFooter from \"../../components/GlobalFooter\";\nimport \"./index.scss\";\n\n/**\n * 主页\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\nexport default () => {\n  return (\n    <View className=\"indexPage\">\n      <View className=\"at-article__h1 title\">MBTI 性格测试</View>\n      <View className=\"at-article__h2 subTitle\">\n        只需 2 分钟，就能非常准确地描述出你是谁，以及你的性格特点\n      </View>\n      <AtButton\n        type=\"primary\"\n        circle\n        className=\"enterBtn\"\n        onClick={() => {\n          Taro.navigateTo({\n            url: \"/pages/doQuestion/index\",\n          });\n        }}\n      >\n        开始测试\n      </AtButton>\n      <Image className=\"headerBg\" src={headerBg}/>\n      <GlobalFooter/>\n    </View>\n  );\n};\n"
  },
  {
    "path": "mbti-test-mini/src/pages/result/index.config.ts",
    "content": "export default definePageConfig({\n  navigationBarTitleText: '查看结果'\n})\n"
  },
  {
    "path": "mbti-test-mini/src/pages/result/index.scss",
    "content": ".resultPage {\n  background: #A2C7D7;\n\n  .title {\n    color: white;\n    padding-top: 48px;\n    text-align: center;\n  }\n\n  .subTitle {\n    color: white;\n    margin-bottom: 48px;\n  }\n\n  .enterBtn {\n    width: 60vw;\n  }\n}\n"
  },
  {
    "path": "mbti-test-mini/src/pages/result/index.tsx",
    "content": "import {View, Image} from \"@tarojs/components\";\nimport {AtButton} from \"taro-ui\";\nimport Taro from \"@tarojs/taro\";\nimport headerBg from \"../../assets/headerBg.jpg\";\nimport GlobalFooter from \"../../components/GlobalFooter\";\nimport {getBestQuestionResult} from \"../../utils/bizUtils\";\nimport questions from \"../../data/questions.json\";\nimport questionResults from \"../../data/question_results.json\";\nimport \"./index.scss\";\n\n/**\n * 测试结果页面\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\nexport default () => {\n  // 获取答案\n  const answerList = Taro.getStorageSync(\"answerList\");\n  if (!answerList || answerList.length < 1) {\n    Taro.showToast({\n      title: \"答案为空\",\n      icon: \"error\",\n      duration: 3000,\n    });\n  }\n  // 获取测试结果\n  const result = getBestQuestionResult(answerList, questions, questionResults);\n\n  return (\n    <View className=\"resultPage\">\n      <View className=\"at-article__h1 title\">{result.resultName}</View>\n      <View className=\"at-article__h2 subTitle\">{result.resultDesc}</View>\n      <AtButton\n        type=\"primary\"\n        circle\n        className=\"enterBtn\"\n        onClick={() => {\n          Taro.reLaunch({\n            url: \"/pages/index/index\",\n          });\n        }}\n      >\n        返回主页\n      </AtButton>\n      <Image className=\"headerBg\" src={headerBg}/>\n      <GlobalFooter/>\n    </View>\n  );\n};\n"
  },
  {
    "path": "mbti-test-mini/src/utils/bizUtils.ts",
    "content": "/**\n * 获取最佳题目评分结果\n * @param answerList\n * @param questions\n * @param question_results\n */\n\nexport function getBestQuestionResult(answerList, questions, question_results) {\n  // 初始化一个对象，用于存储每个选项的计数\n  const optionCount = {};\n\n  // 用户选择 A, B, C\n  // 对应 result：I, I, J\n  // optionCount[I] = 2; optionCount[J] = 1\n\n  // 遍历题目列表\n  for (const question of questions) {\n    // 遍历答案列表\n    for (const answer of answerList) {\n      // 遍历题目中的选项\n      for (const option of question.options) {\n        // 如果答案和选项的key匹配\n        if (option.key === answer) {\n          // 获取选项的result属性\n          const result = option.result;\n\n          // 如果result属性不在optionCount中，初始化为0\n          if (!optionCount[result]) {\n            optionCount[result] = 0;\n          }\n\n          // 在optionCount中增加计数\n          optionCount[result]++;\n        }\n      }\n    }\n  }\n\n  // 初始化最高分数和最高分数对应的评分结果\n  let maxScore = 0;\n  let maxScoreResult = question_results[0];\n\n  // 遍历评分结果列表\n  for (const result of question_results) {\n    // 计算当前评分结果的分数\n    const score = result.resultProp.reduce((count, prop) => {\n      return count + (optionCount[prop] || 0);\n    }, 0);\n\n    // 如果分数高于当前最高分数，更新最高分数和最高分数对应的评分结果\n    if (score > maxScore) {\n      maxScore = score;\n      maxScoreResult = result;\n    }\n  }\n\n  // 返回最高分数和最高分数对应的评分结果\n  return maxScoreResult;\n}\n\n// 示例数据\nconst answerList = [\"B\",\"B\",\"B\",\"A\"];\nconst questions = [\n  {\n    title: \"你通常更喜欢\",\n    options: [\n      {\n        result: \"I\",\n        value: \"独自工作\",\n        key: \"A\",\n      },\n      {\n        result: \"E\",\n        value: \"与他人合作\",\n        key: \"B\",\n      },\n    ],\n  },\n  {\n    options: [\n      {\n        result: \"S\",\n        value: \"喜欢有结构和常规\",\n        key: \"A\",\n      },\n      {\n        result: \"N\",\n        value: \"喜欢自由和灵活性\",\n        key: \"B\",\n      },\n    ],\n    title: \"对于日常安排\",\n  },\n  {\n    options: [\n      {\n        result: \"P\",\n        value: \"首先考虑可能性\",\n        key: \"A\",\n      },\n      {\n        result: \"J\",\n        value: \"首先考虑后果\",\n        key: \"B\",\n      },\n    ],\n    title: \"当遇到问题时\",\n  },\n  {\n    options: [\n      {\n        result: \"T\",\n        value: \"时间是一种宝贵的资源\",\n        key: \"A\",\n      },\n      {\n        result: \"F\",\n        value: \"时间是相对灵活的概念\",\n        key: \"B\",\n      },\n    ],\n    title: \"你如何看待时间\",\n  },\n];\nconst question_results = [\n  {\n    resultProp: [\"I\", \"S\", \"T\", \"J\"],\n    resultDesc: \"忠诚可靠，被公认为务实，注重细节。\",\n    resultPicture: \"icon_url_istj\",\n    resultName: \"ISTJ（物流师）\",\n  },\n  {\n    resultProp: [\"I\", \"S\", \"F\", \"J\"],\n    resultDesc: \"善良贴心，以同情心和责任为特点。\",\n    resultPicture: \"icon_url_isfj\",\n    resultName: \"ISFJ（守护者）\",\n  },\n];\n\nconsole.log(getBestQuestionResult(answerList, questions, question_results));\n"
  },
  {
    "path": "mbti-test-mini/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2017\",\n    \"module\": \"commonjs\",\n    \"removeComments\": false,\n    \"preserveConstEnums\": true,\n    \"moduleResolution\": \"node\",\n    \"experimentalDecorators\": true,\n    \"noImplicitAny\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"outDir\": \"lib\",\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"strictNullChecks\": true,\n    \"sourceMap\": true,\n    \"baseUrl\": \".\",\n    \"rootDir\": \".\",\n    \"jsx\": \"react-jsx\",\n    \"allowJs\": true,\n    \"resolveJsonModule\": true,\n    \"typeRoots\": [\n      \"node_modules/@types\"\n    ]\n  },\n  \"include\": [\"./src\", \"./types\"],\n  \"compileOnSave\": false\n}\n"
  },
  {
    "path": "mbti-test-mini/types/custom.d.ts",
    "content": "interface QuestionOption<T extends string = string> {\n  result: T;\n  value: string;\n  key: T;\n}\n\ninterface Question<T extends string = string> {\n  title: string;\n  options: QuestionOption<T>[];\n}\n"
  },
  {
    "path": "mbti-test-mini/types/global.d.ts",
    "content": "/// <reference types=\"@tarojs/taro\" />\n\ndeclare module '*.png';\ndeclare module '*.gif';\ndeclare module '*.jpg';\ndeclare module '*.jpeg';\ndeclare module '*.svg';\ndeclare module '*.css';\ndeclare module '*.less';\ndeclare module '*.scss';\ndeclare module '*.sass';\ndeclare module '*.styl';\n\ndeclare namespace NodeJS {\n  interface ProcessEnv {\n    TARO_ENV: 'weapp' | 'swan' | 'alipay' | 'h5' | 'rn' | 'tt' | 'quickapp' | 'qq' | 'jd'\n  }\n}\n\n\n"
  },
  {
    "path": "yudada-backend/.gitignore",
    "content": "### @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a> ###\n### @from <a href=\"https://yupi.icu\">编程导航知识星球</a> ###\n\nHELP.md\ntarget/\n!.mvn/wrapper/maven-wrapper.jar\n!**/src/main/**/target/\n!**/src/test/**/target/\n\n### STS ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\n\n### IntelliJ IDEA ###\n.idea\n*.iws\n*.iml\n*.ipr\n\n### NetBeans ###\n/nbproject/private/\n/nbbuild/\n/dist/\n/nbdist/\n/.nb-gradle/\nbuild/\n!**/src/main/**/build/\n!**/src/test/**/build/\n\n### VS Code ###\n.vscode/\n### Java template\n# Compiled class file\n*.class\n\n# Log file\n*.log\n\n# BlueJ files\n*.ctxt\n\n# Mobile Tools for Java (J2ME)\n.mtj.tmp/\n\n# Package Files #\n*.jar\n*.war\n*.nar\n*.ear\n*.zip\n*.tar.gz\n*.rar\n\n# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml\nhs_err_pid*\n\n### Maven template\ntarget/\npom.xml.tag\npom.xml.releaseBackup\npom.xml.versionsBackup\npom.xml.next\nrelease.properties\ndependency-reduced-pom.xml\nbuildNumber.properties\n.mvn/timing.properties\n# https://github.com/takari/maven-wrapper#usage-without-binary-jar\n.mvn/wrapper/maven-wrapper.jar\n\n### JetBrains template\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn.  Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n"
  },
  {
    "path": "yudada-backend/.mvn/wrapper/maven-wrapper.properties",
    "content": "distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.1/apache-maven-3.8.1-bin.zip\nwrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\n"
  },
  {
    "path": "yudada-backend/Dockerfile",
    "content": "# Docker 镜像构建\n# @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n# @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n# 选择基础镜像\nFROM maven:3.8.1-jdk-8-slim as builder\n\n# 解决容器时期与真实时间相差 8 小时的问题\nRUN ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo Asia/Shanghai > /etc/timezone\n\n# 复制代码到容器内\nWORKDIR /app\nCOPY pom.xml .\nCOPY src ./src\n\n# 打包构建\nRUN mvn package -DskipTests\n\n# 容器启动时运行 jar 包\nCMD [\"java\",\"-jar\",\"/app/target/yudada-backend-0.0.1-SNAPSHOT.jar\",\"--spring.profiles.active=prod\"]"
  },
  {
    "path": "yudada-backend/README.md",
    "content": "# SpringBoot 项目初始模板\n\n> 作者：[程序员鱼皮](https://github.com/liyupi)\n> 仅分享于 [编程导航知识星球](https://yupi.icu)\n\n基于 Java SpringBoot 的项目初始模板，整合了常用框架和主流业务的示例代码。\n\n只需 1 分钟即可完成内容网站的后端！！！大家还可以在此基础上快速开发自己的项目。\n\n[toc]\n\n## 模板特点\n\n### 主流框架 & 特性\n\n- Spring Boot 2.7.x（贼新）\n- Spring MVC\n- MyBatis + MyBatis Plus 数据访问（开启分页）\n- Spring Boot 调试工具和项目处理器\n- Spring AOP 切面编程\n- Spring Scheduler 定时任务\n- Spring 事务注解\n\n### 数据存储\n\n- MySQL 数据库\n- Redis 内存数据库\n- Elasticsearch 搜索引擎\n- 腾讯云 COS 对象存储\n\n### 工具类\n\n- Easy Excel 表格处理\n- Hutool 工具库\n- Apache Commons Lang3 工具类\n- Lombok 注解\n\n### 业务特性\n\n- 业务代码生成器（支持自动生成 Service、Controller、数据模型代码）\n- Spring Session Redis 分布式登录\n- 全局请求响应拦截器（记录日志）\n- 全局异常处理器\n- 自定义错误码\n- 封装通用响应类\n- Swagger + Knife4j 接口文档\n- 自定义权限注解 + 全局校验\n- 全局跨域处理\n- 长整数丢失精度解决\n- 多环境配置\n\n\n## 业务功能\n\n- 提供示例 SQL（用户、帖子、帖子点赞、帖子收藏表）\n- 用户登录、注册、注销、更新、检索、权限管理\n- 帖子创建、删除、编辑、更新、数据库检索、ES 灵活检索\n- 帖子点赞、取消点赞\n- 帖子收藏、取消收藏、检索已收藏帖子\n- 帖子全量同步 ES、增量同步 ES 定时任务\n- 支持微信开放平台登录\n- 支持微信公众号订阅、收发消息、设置菜单\n- 支持分业务的文件上传\n\n### 单元测试\n\n- JUnit5 单元测试\n- 示例单元测试类\n\n### 架构设计\n\n- 合理分层\n\n\n## 快速上手\n\n> 所有需要修改的地方鱼皮都标记了 `todo`，便于大家找到修改的位置~\n\n### MySQL 数据库\n\n1）修改 `application.yml` 的数据库配置为你自己的：\n\n```yml\nspring:\n  datasource:\n    driver-class-name: com.mysql.cj.jdbc.Driver\n    url: jdbc:mysql://localhost:3306/yudada\n    username: root\n    password: 123456\n```\n\n2）执行 `sql/create_table.sql` 中的数据库语句，自动创建库表\n\n3）启动项目，访问 `http://localhost:8101/api/doc.html` 即可打开接口文档，不需要写前端就能在线调试接口了~\n\n![](doc/swagger.png)\n\n### Redis 分布式登录\n\n1）修改 `application.yml` 的 Redis 配置为你自己的：\n\n```yml\nspring:\n  redis:\n    database: 1\n    host: localhost\n    port: 6379\n    timeout: 5000\n    password: 123456\n```\n\n2）修改 `application.yml` 中的 session 存储方式：\n\n```yml\nspring:\n  session:\n    store-type: redis\n```\n\n3）移除 `MainApplication` 类开头 `@SpringBootApplication` 注解内的 exclude 参数：\n\n修改前：\n\n```java\n@SpringBootApplication(exclude = {RedisAutoConfiguration.class})\n```\n\n修改后：\n\n\n```java\n@SpringBootApplication\n```\n\n### Elasticsearch 搜索引擎\n\n1）修改 `application.yml` 的 Elasticsearch 配置为你自己的：\n\n```yml\nspring:\n  elasticsearch:\n    uris: http://localhost:9200\n    username: root\n    password: 123456\n```\n\n2）复制 `sql/post_es_mapping.json` 文件中的内容，通过调用 Elasticsearch 的接口或者 Kibana Dev Tools 来创建索引（相当于数据库建表）\n\n```\nPUT post_v1\n{\n 参数见 sql/post_es_mapping.json 文件\n}\n```\n\n这步不会操作的话需要补充下 Elasticsearch 的知识，或者自行百度一下~\n\n3）开启同步任务，将数据库的帖子同步到 Elasticsearch\n\n找到 job 目录下的 `FullSyncPostToEs` 和 `IncSyncPostToEs` 文件，取消掉 `@Component` 注解的注释，再次执行程序即可触发同步：\n\n```java\n// todo 取消注释开启任务\n//@Component\n```\n\n### 业务代码生成器\n\n支持自动生成 Service、Controller、数据模型代码，配合 MyBatisX 插件，可以快速开发增删改查等实用基础功能。\n\n找到 `generate.CodeGenerator` 类，修改生成参数和生成路径，并且支持注释掉不需要的生成逻辑，然后运行即可。\n\n```\n// 指定生成参数\nString packageName = \"com.yupi.yudada\";\nString dataName = \"用户评论\";\nString dataKey = \"userComment\";\nString upperDataKey = \"UserComment\";\n```\n\n生成代码后，可以移动到实际项目中，并且按照 `// todo` 注释的提示来针对自己的业务需求进行修改。\n"
  },
  {
    "path": "yudada-backend/mvnw",
    "content": "#!/bin/sh\n# ----------------------------------------------------------------------------\n# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#    https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n# ----------------------------------------------------------------------------\n\n# ----------------------------------------------------------------------------\n# Maven Start Up Batch script\n#\n# Required ENV vars:\n# ------------------\n#   JAVA_HOME - location of a JDK home dir\n#\n# Optional ENV vars\n# -----------------\n#   M2_HOME - location of maven2's installed home dir\n#   MAVEN_OPTS - parameters passed to the Java VM when running Maven\n#     e.g. to debug Maven itself, use\n#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n# ----------------------------------------------------------------------------\n\nif [ -z \"$MAVEN_SKIP_RC\" ] ; then\n\n  if [ -f /usr/local/etc/mavenrc ] ; then\n    . /usr/local/etc/mavenrc\n  fi\n\n  if [ -f /etc/mavenrc ] ; then\n    . /etc/mavenrc\n  fi\n\n  if [ -f \"$HOME/.mavenrc\" ] ; then\n    . \"$HOME/.mavenrc\"\n  fi\n\nfi\n\n# OS specific support.  $var _must_ be set to either true or false.\ncygwin=false;\ndarwin=false;\nmingw=false\ncase \"`uname`\" in\n  CYGWIN*) cygwin=true ;;\n  MINGW*) mingw=true;;\n  Darwin*) darwin=true\n    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home\n    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html\n    if [ -z \"$JAVA_HOME\" ]; then\n      if [ -x \"/usr/libexec/java_home\" ]; then\n        export JAVA_HOME=\"`/usr/libexec/java_home`\"\n      else\n        export JAVA_HOME=\"/Library/Java/Home\"\n      fi\n    fi\n    ;;\nesac\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  if [ -r /etc/gentoo-release ] ; then\n    JAVA_HOME=`java-config --jre-home`\n  fi\nfi\n\nif [ -z \"$M2_HOME\" ] ; then\n  ## resolve links - $0 may be a link to maven's home\n  PRG=\"$0\"\n\n  # need this for relative symlinks\n  while [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n      PRG=\"$link\"\n    else\n      PRG=\"`dirname \"$PRG\"`/$link\"\n    fi\n  done\n\n  saveddir=`pwd`\n\n  M2_HOME=`dirname \"$PRG\"`/..\n\n  # make it fully qualified\n  M2_HOME=`cd \"$M2_HOME\" && pwd`\n\n  cd \"$saveddir\"\n  # echo Using m2 at $M2_HOME\nfi\n\n# For Cygwin, ensure paths are in UNIX format before anything is touched\nif $cygwin ; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=`cygpath --unix \"$M2_HOME\"`\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=`cygpath --unix \"$JAVA_HOME\"`\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=`cygpath --path --unix \"$CLASSPATH\"`\nfi\n\n# For Mingw, ensure paths are in UNIX format before anything is touched\nif $mingw ; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=\"`(cd \"$M2_HOME\"; pwd)`\"\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=\"`(cd \"$JAVA_HOME\"; pwd)`\"\nfi\n\nif [ -z \"$JAVA_HOME\" ]; then\n  javaExecutable=\"`which javac`\"\n  if [ -n \"$javaExecutable\" ] && ! [ \"`expr \\\"$javaExecutable\\\" : '\\([^ ]*\\)'`\" = \"no\" ]; then\n    # readlink(1) is not available as standard on Solaris 10.\n    readLink=`which readlink`\n    if [ ! `expr \"$readLink\" : '\\([^ ]*\\)'` = \"no\" ]; then\n      if $darwin ; then\n        javaHome=\"`dirname \\\"$javaExecutable\\\"`\"\n        javaExecutable=\"`cd \\\"$javaHome\\\" && pwd -P`/javac\"\n      else\n        javaExecutable=\"`readlink -f \\\"$javaExecutable\\\"`\"\n      fi\n      javaHome=\"`dirname \\\"$javaExecutable\\\"`\"\n      javaHome=`expr \"$javaHome\" : '\\(.*\\)/bin'`\n      JAVA_HOME=\"$javaHome\"\n      export JAVA_HOME\n    fi\n  fi\nfi\n\nif [ -z \"$JAVACMD\" ] ; then\n  if [ -n \"$JAVA_HOME\"  ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n      # IBM's JDK on AIX uses strange locations for the executables\n      JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n      JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n  else\n    JAVACMD=\"`\\\\unset -f command; \\\\command -v java`\"\n  fi\nfi\n\nif [ ! -x \"$JAVACMD\" ] ; then\n  echo \"Error: JAVA_HOME is not defined correctly.\" >&2\n  echo \"  We cannot execute $JAVACMD\" >&2\n  exit 1\nfi\n\nif [ -z \"$JAVA_HOME\" ] ; then\n  echo \"Warning: JAVA_HOME environment variable is not set.\"\nfi\n\nCLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher\n\n# traverses directory structure from process work directory to filesystem root\n# first directory with .mvn subdirectory is considered project base directory\nfind_maven_basedir() {\n\n  if [ -z \"$1\" ]\n  then\n    echo \"Path not specified to find_maven_basedir\"\n    return 1\n  fi\n\n  basedir=\"$1\"\n  wdir=\"$1\"\n  while [ \"$wdir\" != '/' ] ; do\n    if [ -d \"$wdir\"/.mvn ] ; then\n      basedir=$wdir\n      break\n    fi\n    # workaround for JBEAP-8937 (on Solaris 10/Sparc)\n    if [ -d \"${wdir}\" ]; then\n      wdir=`cd \"$wdir/..\"; pwd`\n    fi\n    # end of workaround\n  done\n  echo \"${basedir}\"\n}\n\n# concatenates all lines of a file\nconcat_lines() {\n  if [ -f \"$1\" ]; then\n    echo \"$(tr -s '\\n' ' ' < \"$1\")\"\n  fi\n}\n\nBASE_DIR=`find_maven_basedir \"$(pwd)\"`\nif [ -z \"$BASE_DIR\" ]; then\n  exit 1;\nfi\n\n##########################################################################################\n# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n# This allows using the maven wrapper in projects that prohibit checking in binary data.\n##########################################################################################\nif [ -r \"$BASE_DIR/.mvn/wrapper/maven-wrapper.jar\" ]; then\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Found .mvn/wrapper/maven-wrapper.jar\"\n    fi\nelse\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ...\"\n    fi\n    if [ -n \"$MVNW_REPOURL\" ]; then\n      jarUrl=\"$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\"\n    else\n      jarUrl=\"https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\"\n    fi\n    while IFS=\"=\" read key value; do\n      case \"$key\" in (wrapperUrl) jarUrl=\"$value\"; break ;;\n      esac\n    done < \"$BASE_DIR/.mvn/wrapper/maven-wrapper.properties\"\n    if [ \"$MVNW_VERBOSE\" = true ]; then\n      echo \"Downloading from: $jarUrl\"\n    fi\n    wrapperJarPath=\"$BASE_DIR/.mvn/wrapper/maven-wrapper.jar\"\n    if $cygwin; then\n      wrapperJarPath=`cygpath --path --windows \"$wrapperJarPath\"`\n    fi\n\n    if command -v wget > /dev/null; then\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Found wget ... using wget\"\n        fi\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            wget \"$jarUrl\" -O \"$wrapperJarPath\" || rm -f \"$wrapperJarPath\"\n        else\n            wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD \"$jarUrl\" -O \"$wrapperJarPath\" || rm -f \"$wrapperJarPath\"\n        fi\n    elif command -v curl > /dev/null; then\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Found curl ... using curl\"\n        fi\n        if [ -z \"$MVNW_USERNAME\" ] || [ -z \"$MVNW_PASSWORD\" ]; then\n            curl -o \"$wrapperJarPath\" \"$jarUrl\" -f\n        else\n            curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o \"$wrapperJarPath\" \"$jarUrl\" -f\n        fi\n\n    else\n        if [ \"$MVNW_VERBOSE\" = true ]; then\n          echo \"Falling back to using Java to download\"\n        fi\n        javaClass=\"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java\"\n        # For Cygwin, switch paths to Windows format before running javac\n        if $cygwin; then\n          javaClass=`cygpath --path --windows \"$javaClass\"`\n        fi\n        if [ -e \"$javaClass\" ]; then\n            if [ ! -e \"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class\" ]; then\n                if [ \"$MVNW_VERBOSE\" = true ]; then\n                  echo \" - Compiling MavenWrapperDownloader.java ...\"\n                fi\n                # Compiling the Java class\n                (\"$JAVA_HOME/bin/javac\" \"$javaClass\")\n            fi\n            if [ -e \"$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class\" ]; then\n                # Running the downloader\n                if [ \"$MVNW_VERBOSE\" = true ]; then\n                  echo \" - Running MavenWrapperDownloader.java ...\"\n                fi\n                (\"$JAVA_HOME/bin/java\" -cp .mvn/wrapper MavenWrapperDownloader \"$MAVEN_PROJECTBASEDIR\")\n            fi\n        fi\n    fi\nfi\n##########################################################################################\n# End of extension\n##########################################################################################\n\nexport MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-\"$BASE_DIR\"}\nif [ \"$MVNW_VERBOSE\" = true ]; then\n  echo $MAVEN_PROJECTBASEDIR\nfi\nMAVEN_OPTS=\"$(concat_lines \"$MAVEN_PROJECTBASEDIR/.mvn/jvm.config\") $MAVEN_OPTS\"\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin; then\n  [ -n \"$M2_HOME\" ] &&\n    M2_HOME=`cygpath --path --windows \"$M2_HOME\"`\n  [ -n \"$JAVA_HOME\" ] &&\n    JAVA_HOME=`cygpath --path --windows \"$JAVA_HOME\"`\n  [ -n \"$CLASSPATH\" ] &&\n    CLASSPATH=`cygpath --path --windows \"$CLASSPATH\"`\n  [ -n \"$MAVEN_PROJECTBASEDIR\" ] &&\n    MAVEN_PROJECTBASEDIR=`cygpath --path --windows \"$MAVEN_PROJECTBASEDIR\"`\nfi\n\n# Provide a \"standardized\" way to retrieve the CLI args that will\n# work with both Windows and non-Windows executions.\nMAVEN_CMD_LINE_ARGS=\"$MAVEN_CONFIG $@\"\nexport MAVEN_CMD_LINE_ARGS\n\nWRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nexec \"$JAVACMD\" \\\n  $MAVEN_OPTS \\\n  $MAVEN_DEBUG_OPTS \\\n  -classpath \"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar\" \\\n  \"-Dmaven.home=${M2_HOME}\" \\\n  \"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}\" \\\n  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG \"$@\"\n"
  },
  {
    "path": "yudada-backend/mvnw.cmd",
    "content": "@REM ----------------------------------------------------------------------------\n@REM Licensed to the Apache Software Foundation (ASF) under one\n@REM or more contributor license agreements.  See the NOTICE file\n@REM distributed with this work for additional information\n@REM regarding copyright ownership.  The ASF licenses this file\n@REM to you under the Apache License, Version 2.0 (the\n@REM \"License\"); you may not use this file except in compliance\n@REM with the License.  You may obtain a copy of the License at\n@REM\n@REM    https://www.apache.org/licenses/LICENSE-2.0\n@REM\n@REM Unless required by applicable law or agreed to in writing,\n@REM software distributed under the License is distributed on an\n@REM \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n@REM KIND, either express or implied.  See the License for the\n@REM specific language governing permissions and limitations\n@REM under the License.\n@REM ----------------------------------------------------------------------------\n\n@REM ----------------------------------------------------------------------------\n@REM Maven Start Up Batch script\n@REM\n@REM Required ENV vars:\n@REM JAVA_HOME - location of a JDK home dir\n@REM\n@REM Optional ENV vars\n@REM M2_HOME - location of maven2's installed home dir\n@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands\n@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending\n@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven\n@REM     e.g. to debug Maven itself, use\n@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000\n@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files\n@REM ----------------------------------------------------------------------------\n\n@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'\n@echo off\n@REM set title of command window\ntitle %0\n@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'\n@if \"%MAVEN_BATCH_ECHO%\" == \"on\"  echo %MAVEN_BATCH_ECHO%\n\n@REM set %HOME% to equivalent of $HOME\nif \"%HOME%\" == \"\" (set \"HOME=%HOMEDRIVE%%HOMEPATH%\")\n\n@REM Execute a user defined script before this one\nif not \"%MAVEN_SKIP_RC%\" == \"\" goto skipRcPre\n@REM check for pre script, once with legacy .bat ending and once with .cmd ending\nif exist \"%USERPROFILE%\\mavenrc_pre.bat\" call \"%USERPROFILE%\\mavenrc_pre.bat\" %*\nif exist \"%USERPROFILE%\\mavenrc_pre.cmd\" call \"%USERPROFILE%\\mavenrc_pre.cmd\" %*\n:skipRcPre\n\n@setlocal\n\nset ERROR_CODE=0\n\n@REM To isolate internal variables from possible post scripts, we use another setlocal\n@setlocal\n\n@REM ==== START VALIDATION ====\nif not \"%JAVA_HOME%\" == \"\" goto OkJHome\n\necho.\necho Error: JAVA_HOME not found in your environment. >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n:OkJHome\nif exist \"%JAVA_HOME%\\bin\\java.exe\" goto init\n\necho.\necho Error: JAVA_HOME is set to an invalid directory. >&2\necho JAVA_HOME = \"%JAVA_HOME%\" >&2\necho Please set the JAVA_HOME variable in your environment to match the >&2\necho location of your Java installation. >&2\necho.\ngoto error\n\n@REM ==== END VALIDATION ====\n\n:init\n\n@REM Find the project base dir, i.e. the directory that contains the folder \".mvn\".\n@REM Fallback to current working directory if not found.\n\nset MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%\nIF NOT \"%MAVEN_PROJECTBASEDIR%\"==\"\" goto endDetectBaseDir\n\nset EXEC_DIR=%CD%\nset WDIR=%EXEC_DIR%\n:findBaseDir\nIF EXIST \"%WDIR%\"\\.mvn goto baseDirFound\ncd ..\nIF \"%WDIR%\"==\"%CD%\" goto baseDirNotFound\nset WDIR=%CD%\ngoto findBaseDir\n\n:baseDirFound\nset MAVEN_PROJECTBASEDIR=%WDIR%\ncd \"%EXEC_DIR%\"\ngoto endDetectBaseDir\n\n:baseDirNotFound\nset MAVEN_PROJECTBASEDIR=%EXEC_DIR%\ncd \"%EXEC_DIR%\"\n\n:endDetectBaseDir\n\nIF NOT EXIST \"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\" goto endReadAdditionalConfig\n\n@setlocal EnableExtensions EnableDelayedExpansion\nfor /F \"usebackq delims=\" %%a in (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\jvm.config\") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a\n@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%\n\n:endReadAdditionalConfig\n\nSET MAVEN_JAVA_EXE=\"%JAVA_HOME%\\bin\\java.exe\"\nset WRAPPER_JAR=\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.jar\"\nset WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain\n\nset DOWNLOAD_URL=\"https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\"\n\nFOR /F \"usebackq tokens=1,2 delims==\" %%A IN (\"%MAVEN_PROJECTBASEDIR%\\.mvn\\wrapper\\maven-wrapper.properties\") DO (\n    IF \"%%A\"==\"wrapperUrl\" SET DOWNLOAD_URL=%%B\n)\n\n@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central\n@REM This allows using the maven wrapper in projects that prohibit checking in binary data.\nif exist %WRAPPER_JAR% (\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Found %WRAPPER_JAR%\n    )\n) else (\n    if not \"%MVNW_REPOURL%\" == \"\" (\n        SET DOWNLOAD_URL=\"%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar\"\n    )\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Couldn't find %WRAPPER_JAR%, downloading it ...\n        echo Downloading from: %DOWNLOAD_URL%\n    )\n\n    powershell -Command \"&{\"^\n\t\t\"$webclient = new-object System.Net.WebClient;\"^\n\t\t\"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {\"^\n\t\t\"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');\"^\n\t\t\"}\"^\n\t\t\"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')\"^\n\t\t\"}\"\n    if \"%MVNW_VERBOSE%\" == \"true\" (\n        echo Finished downloading %WRAPPER_JAR%\n    )\n)\n@REM End of extension\n\n@REM Provide a \"standardized\" way to retrieve the CLI args that will\n@REM work with both Windows and non-Windows executions.\nset MAVEN_CMD_LINE_ARGS=%*\n\n%MAVEN_JAVA_EXE% ^\n  %JVM_CONFIG_MAVEN_PROPS% ^\n  %MAVEN_OPTS% ^\n  %MAVEN_DEBUG_OPTS% ^\n  -classpath %WRAPPER_JAR% ^\n  \"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%\" ^\n  %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*\nif ERRORLEVEL 1 goto error\ngoto end\n\n:error\nset ERROR_CODE=1\n\n:end\n@endlocal & set ERROR_CODE=%ERROR_CODE%\n\nif not \"%MAVEN_SKIP_RC%\"==\"\" goto skipRcPost\n@REM check for post script, once with legacy .bat ending and once with .cmd ending\nif exist \"%USERPROFILE%\\mavenrc_post.bat\" call \"%USERPROFILE%\\mavenrc_post.bat\"\nif exist \"%USERPROFILE%\\mavenrc_post.cmd\" call \"%USERPROFILE%\\mavenrc_post.cmd\"\n:skipRcPost\n\n@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'\nif \"%MAVEN_BATCH_PAUSE%\"==\"on\" pause\n\nif \"%MAVEN_TERMINATE_CMD%\"==\"on\" exit %ERROR_CODE%\n\ncmd /C exit /B %ERROR_CODE%\n"
  },
  {
    "path": "yudada-backend/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a> -->\n<!-- @from <a href=\"https://yupi.icu\">编程导航知识星球</a> -->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>2.7.2</version>\n        <relativePath/> <!-- lookup parent from repository -->\n    </parent>\n    <groupId>com.yupi</groupId>\n    <artifactId>yudada-backend</artifactId>\n    <version>0.0.1-SNAPSHOT</version>\n    <name>yudada-backend</name>\n    <properties>\n        <java.version>1.8</java.version>\n    </properties>\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-freemarker</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-aop</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.mybatis.spring.boot</groupId>\n            <artifactId>mybatis-spring-boot-starter</artifactId>\n            <version>2.2.2</version>\n        </dependency>\n        <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->\n        <dependency>\n            <groupId>com.baomidou</groupId>\n            <artifactId>mybatis-plus-boot-starter</artifactId>\n            <version>3.5.2</version>\n        </dependency>\n        <!-- redis -->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-data-redis</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.session</groupId>\n            <artifactId>spring-session-data-redis</artifactId>\n        </dependency>\n        <!-- https://github.com/redisson/redisson -->\n        <dependency>\n            <groupId>org.redisson</groupId>\n            <artifactId>redisson</artifactId>\n            <version>3.21.0</version>\n        </dependency>\n        <!-- https://github.com/ben-manes/caffeine -->\n        <dependency>\n            <groupId>com.github.ben-manes.caffeine</groupId>\n            <artifactId>caffeine</artifactId>\n            <version>2.9.2</version>\n        </dependency>\n        <!-- https://github.com/apache/shardingsphere -->\n        <dependency>\n            <groupId>org.apache.shardingsphere</groupId>\n            <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>\n            <version>5.2.0</version>\n        </dependency>\n        <!-- https://doc.xiaominfo.com/docs/quick-start#openapi2 -->\n        <dependency>\n            <groupId>com.github.xiaoymin</groupId>\n            <artifactId>knife4j-openapi2-spring-boot-starter</artifactId>\n            <version>4.4.0</version>\n        </dependency>\n        <!-- https://cloud.tencent.com/document/product/436/10199-->\n        <dependency>\n            <groupId>com.qcloud</groupId>\n            <artifactId>cos_api</artifactId>\n            <version>5.6.89</version>\n        </dependency>\n        <!-- https://open.bigmodel.cn/dev/api#sdk_install -->\n        <dependency>\n            <groupId>cn.bigmodel.openapi</groupId>\n            <artifactId>oapi-java-sdk</artifactId>\n            <version>release-V4-2.0.2</version>\n        </dependency>\n        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-lang3</artifactId>\n        </dependency>\n        <!-- https://github.com/alibaba/easyexcel -->\n        <dependency>\n            <groupId>com.alibaba</groupId>\n            <artifactId>easyexcel</artifactId>\n            <version>3.1.1</version>\n        </dependency>\n        <!-- https://hutool.cn/docs/index.html#/-->\n        <dependency>\n            <groupId>cn.hutool</groupId>\n            <artifactId>hutool-all</artifactId>\n            <version>5.8.8</version>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-devtools</artifactId>\n            <scope>runtime</scope>\n            <optional>true</optional>\n        </dependency>\n        <dependency>\n            <groupId>mysql</groupId>\n            <artifactId>mysql-connector-java</artifactId>\n            <scope>runtime</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-configuration-processor</artifactId>\n            <optional>true</optional>\n        </dependency>\n        <dependency>\n            <groupId>org.projectlombok</groupId>\n            <artifactId>lombok</artifactId>\n            <optional>true</optional>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n                <configuration>\n                    <excludes>\n                        <exclude>\n                            <groupId>org.projectlombok</groupId>\n                            <artifactId>lombok</artifactId>\n                        </exclude>\n                    </excludes>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>\n"
  },
  {
    "path": "yudada-backend/sql/create_table.sql",
    "content": "# 数据库初始化\n# @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n# @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n\n-- 创建库\ncreate database if not exists yudada;\n\n-- 切换库\nuse yudada;\n\n-- 用户表\ncreate table if not exists user\n(\n    id           bigint auto_increment comment 'id' primary key,\n    userAccount  varchar(256)                           not null comment '账号',\n    userPassword varchar(512)                           not null comment '密码',\n    unionId      varchar(256)                           null comment '微信开放平台id',\n    mpOpenId     varchar(256)                           null comment '公众号openId',\n    userName     varchar(256)                           null comment '用户昵称',\n    userAvatar   varchar(1024)                          null comment '用户头像',\n    userProfile  varchar(512)                           null comment '用户简介',\n    userRole     varchar(256) default 'user'            not null comment '用户角色：user/admin/ban',\n    createTime   datetime     default CURRENT_TIMESTAMP not null comment '创建时间',\n    updateTime   datetime     default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',\n    isDelete     tinyint      default 0                 not null comment '是否删除',\n    index idx_unionId (unionId)\n) comment '用户' collate = utf8mb4_unicode_ci;\n\n-- 应用表\ncreate table if not exists app\n(\n    id              bigint auto_increment comment 'id' primary key,\n    appName         varchar(128)                       not null comment '应用名',\n    appDesc         varchar(2048)                      null comment '应用描述',\n    appIcon         varchar(1024)                      null comment '应用图标',\n    appType         tinyint  default 0                 not null comment '应用类型（0-得分类，1-测评类）',\n    scoringStrategy tinyint  default 0                 not null comment '评分策略（0-自定义，1-AI）',\n    reviewStatus    int      default 0                 not null comment '审核状态：0-待审核, 1-通过, 2-拒绝',\n    reviewMessage   varchar(512)                       null comment '审核信息',\n    reviewerId      bigint                             null comment '审核人 id',\n    reviewTime      datetime                           null comment '审核时间',\n    userId          bigint                             not null comment '创建用户 id',\n    createTime      datetime default CURRENT_TIMESTAMP not null comment '创建时间',\n    updateTime      datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',\n    isDelete        tinyint  default 0                 not null comment '是否删除',\n    index idx_appName (appName)\n) comment '应用' collate = utf8mb4_unicode_ci;\n\n-- 题目表\ncreate table if not exists question\n(\n    id              bigint auto_increment comment 'id' primary key,\n    questionContent text                               null comment '题目内容（json格式）',\n    appId           bigint                             not null comment '应用 id',\n    userId          bigint                             not null comment '创建用户 id',\n    createTime      datetime default CURRENT_TIMESTAMP not null comment '创建时间',\n    updateTime      datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',\n    isDelete        tinyint  default 0                 not null comment '是否删除',\n    index idx_appId (appId)\n) comment '题目' collate = utf8mb4_unicode_ci;\n\n-- 评分结果表\ncreate table if not exists scoring_result\n(\n    id               bigint auto_increment comment 'id' primary key,\n    resultName       varchar(128)                       not null comment '结果名称，如物流师',\n    resultDesc       text                               null comment '结果描述',\n    resultPicture    varchar(1024)                      null comment '结果图片',\n    resultProp       varchar(128)                       null comment '结果属性集合 JSON，如 [I,S,T,J]',\n    resultScoreRange int                                null comment '结果得分范围，如 80，表示 80及以上的分数命中此结果',\n    appId            bigint                             not null comment '应用 id',\n    userId           bigint                             not null comment '创建用户 id',\n    createTime       datetime default CURRENT_TIMESTAMP not null comment '创建时间',\n    updateTime       datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',\n    isDelete         tinyint  default 0                 not null comment '是否删除',\n    index idx_appId (appId)\n) comment '评分结果' collate = utf8mb4_unicode_ci;\n\n-- 用户答题记录表\ncreate table if not exists user_answer\n(\n    id              bigint auto_increment primary key,\n    appId           bigint                             not null comment '应用 id',\n    appType         tinyint  default 0                 not null comment '应用类型（0-得分类，1-角色测评类）',\n    scoringStrategy tinyint  default 0                 not null comment '评分策略（0-自定义，1-AI）',\n    choices         text                               null comment '用户答案（JSON 数组）',\n    resultId        bigint                             null comment '评分结果 id',\n    resultName      varchar(128)                       null comment '结果名称，如物流师',\n    resultDesc      text                               null comment '结果描述',\n    resultPicture   varchar(1024)                      null comment '结果图标',\n    resultScore     int                                null comment '得分',\n    userId          bigint                             not null comment '用户 id',\n    createTime      datetime default CURRENT_TIMESTAMP not null comment '创建时间',\n    updateTime      datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',\n    isDelete        tinyint  default 0                 not null comment '是否删除',\n    index idx_appId (appId),\n    index idx_userId (userId)\n) comment '用户答题记录' collate = utf8mb4_unicode_ci;\n\n-- 用户答题记录表（分表 0）\ncreate table if not exists user_answer_0\n(\n    id              bigint auto_increment primary key,\n    appId           bigint                             not null comment '应用 id',\n    appType         tinyint  default 0                 not null comment '应用类型（0-得分类，1-角色测评类）',\n    scoringStrategy tinyint  default 0                 not null comment '评分策略（0-自定义，1-AI）',\n    choices         text                               null comment '用户答案（JSON 数组）',\n    resultId        bigint                             null comment '评分结果 id',\n    resultName      varchar(128)                       null comment '结果名称，如物流师',\n    resultDesc      text                               null comment '结果描述',\n    resultPicture   varchar(1024)                      null comment '结果图标',\n    resultScore     int                                null comment '得分',\n    userId          bigint                             not null comment '用户 id',\n    createTime      datetime default CURRENT_TIMESTAMP not null comment '创建时间',\n    updateTime      datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',\n    isDelete        tinyint  default 0                 not null comment '是否删除',\n    index idx_appId (appId),\n    index idx_userId (userId)\n) comment '用户答题记录分表 0' collate = utf8mb4_unicode_ci;\n\n-- 用户答题记录表（分表 1）\ncreate table if not exists user_answer_1\n(\n    id              bigint auto_increment primary key,\n    appId           bigint                             not null comment '应用 id',\n    appType         tinyint  default 0                 not null comment '应用类型（0-得分类，1-角色测评类）',\n    scoringStrategy tinyint  default 0                 not null comment '评分策略（0-自定义，1-AI）',\n    choices         text                               null comment '用户答案（JSON 数组）',\n    resultId        bigint                             null comment '评分结果 id',\n    resultName      varchar(128)                       null comment '结果名称，如物流师',\n    resultDesc      text                               null comment '结果描述',\n    resultPicture   varchar(1024)                      null comment '结果图标',\n    resultScore     int                                null comment '得分',\n    userId          bigint                             not null comment '用户 id',\n    createTime      datetime default CURRENT_TIMESTAMP not null comment '创建时间',\n    updateTime      datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',\n    isDelete        tinyint  default 0                 not null comment '是否删除',\n    index idx_appId (appId),\n    index idx_userId (userId)\n) comment '用户答题记录分表 1' collate = utf8mb4_unicode_ci;"
  },
  {
    "path": "yudada-backend/sql/init_data.sql",
    "content": "-- 切换库\nuse yudada;\n\n-- 用户表初始数据\nINSERT INTO user (id, userAccount, userPassword, unionId, mpOpenId, userName, userAvatar, userProfile, userRole,\n                  createTime, updateTime, isDelete)\nVALUES (1, 'yupi', 'b0dd3697a192885d7c055db46155b26a', null, null, '鱼皮',\n        'https://k.sinaimg.cn/n/sinakd20110/560/w1080h1080/20230930/915d-f3d7b580c33632b191e19afa0a858d31.jpg/w700d1q75cms.jpg',\n        '欢迎来编程导航学习', 'admin', '2024-05-09 11:13:13', '2024-05-09 15:07:48', 0);\n\n-- 应用表初始数据\nINSERT INTO app (id, appName, appDesc, appIcon, appType, scoringStrategy, reviewStatus, reviewMessage, reviewerId,\n                 reviewTime, userId, createTime, updateTime, isDelete)\nVALUES (1, '自定义MBTI性格测试', '测试性格', '11', 1, 0, 1, null, null, null, 1, '2024-04-24 15:58:05', '2024-05-09 15:09:53', 0);\nINSERT INTO app (id, appName, appDesc, appIcon, appType, scoringStrategy, reviewStatus, reviewMessage, reviewerId,\n                 reviewTime, userId, createTime, updateTime, isDelete)\nVALUES (2, '自定义得分测试', '测试得分', '22', 0, 0, 1, null, null, null, 1, '2024-04-25 11:39:30', '2024-05-09 15:09:53', 0);\nINSERT INTO app (id, appName, appDesc, appIcon, appType, scoringStrategy, reviewStatus, reviewMessage, reviewerId,\n                 reviewTime, userId, createTime, updateTime, isDelete)\nVALUES (3, 'AI MBTI 性格测试', '快来测测你的 MBTI', '11', 1, 1, 1, null, null, null, 1, '2024-04-26 16:38:12',\n        '2024-05-09 15:09:53', 0);\nINSERT INTO app (id, appName, appDesc, appIcon, appType, scoringStrategy, reviewStatus, reviewMessage, reviewerId,\n                 reviewTime, userId, createTime, updateTime, isDelete)\nVALUES (4, 'AI 得分测试', '看看你熟悉多少首都', '22', 0, 1, 1, null, null, null, 1, '2024-04-26 16:38:56', '2024-05-09 15:09:53', 0);\n\n-- 题目表初始数据\nINSERT INTO question (id, questionContent, appId, userId, createTime, updateTime, isDelete)\nVALUES (1,\n        '[{\"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. 你如何看待时间\"}]',\n        1, 1, '2024-04-24 16:41:53', '2024-05-09 12:28:58', 0);\nINSERT INTO question (id, questionContent, appId, userId, createTime, updateTime, isDelete)\nVALUES (2,\n        '[{\"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\":\"埃及的首都是?\"}]',\n        2, 1, '2024-04-25 15:03:07', '2024-05-09 12:28:58', 0);\nINSERT INTO question (id, questionContent, appId, userId, createTime, updateTime, isDelete)\nVALUES (3,\n        '[{\"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. 你如何看待时间\"}]',\n        3, 1, '2024-04-26 16:39:29', '2024-05-09 12:28:58', 0);\nINSERT INTO question (id, questionContent, appId, userId, createTime, updateTime, isDelete)\nVALUES (4,\n        '[{\"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\":\"埃及的首都是?\"}]',\n        4, 1, '2024-04-26 16:39:29', '2024-05-09 12:28:58', 0);\n\n\n-- 评分结果表初始数据\nINSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime,\n                            updateTime, isDelete, appId, userId)\nVALUES (1, 'ISTJ（物流师）', '忠诚可靠，被公认为务实，注重细节。', 'icon_url_istj', '[\"I\",\"S\",\"T\",\"J\"]', null,\n        '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1);\nINSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime,\n                            updateTime, isDelete, appId, userId)\nVALUES (2, 'ISFJ（守护者）', '善良贴心，以同情心和责任为特点。', 'icon_url_isfj', '[\"I\",\"S\",\"F\",\"J\"]', null,\n        '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1);\nINSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime,\n                            updateTime, isDelete, appId, userId)\nVALUES (3, 'INFJ（占有者）', '理想主义者，有着深刻的洞察力，善于理解他人。', 'icon_url_infj', '[\"I\",\"N\",\"F\",\"J\"]', null,\n        '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1);\nINSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime,\n                            updateTime, isDelete, appId, userId)\nVALUES (4, 'INTJ（设计师）', '独立思考者，善于规划和实现目标，理性而果断。', 'icon_url_intj', '[\"I\",\"N\",\"T\",\"J\"]', null,\n        '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1);\nINSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime,\n                            updateTime, isDelete, appId, userId)\nVALUES (5, 'ISTP（运动员）', '冷静自持，善于解决问题，擅长实践技能。', 'icon_url_istp', '[\"I\",\"S\",\"T\",\"P\"]', null,\n        '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1);\nINSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime,\n                            updateTime, isDelete, appId, userId)\nVALUES (6, 'ISFP（艺术家）', '具有艺术感和敏感性，珍视个人空间和自由。', 'icon_url_isfp', '[\"I\",\"S\",\"F\",\"P\"]', null,\n        '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1);\nINSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime,\n                            updateTime, isDelete, appId, userId)\nVALUES (7, 'INFP（治愈者）', '理想主义者，富有创造力，以同情心和理解他人著称。', 'icon_url_infp', '[\"I\",\"N\",\"F\",\"P\"]', null,\n        '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1);\nINSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime,\n                            updateTime, isDelete, appId, userId)\nVALUES (8, 'INTP（学者）', '思维清晰，探索精神，独立思考且理性。', 'icon_url_intp', '[\"I\",\"N\",\"T\",\"P\"]', null,\n        '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1);\nINSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime,\n                            updateTime, isDelete, appId, userId)\nVALUES (9, 'ESTP（拓荒者）', '敢于冒险，乐于冒险，思维敏捷，行动果断。', 'icon_url_estp', '[\"E\",\"S\",\"T\",\"P\"]', null,\n        '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1);\nINSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime,\n                            updateTime, isDelete, appId, userId)\nVALUES (10, 'ESFP（表演者）', '热情开朗，善于社交，热爱生活，乐于助人。', 'icon_url_esfp', '[\"E\",\"S\",\"F\",\"P\"]', null,\n        '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1);\nINSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime,\n                            updateTime, isDelete, appId, userId)\nVALUES (11, 'ENFP（倡导者）', '富有想象力，充满热情，善于激发他人的活力和潜力。', 'icon_url_enfp', '[\"E\",\"N\",\"F\",\"P\"]', null,\n        '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1);\nINSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime,\n                            updateTime, isDelete, appId, userId)\nVALUES (12, 'ENTP（发明家）', '充满创造力，善于辩论，挑战传统，喜欢探索新领域。', 'icon_url_entp', '[\"E\",\"N\",\"T\",\"P\"]', null,\n        '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1);\nINSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime,\n                            updateTime, isDelete, appId, userId)\nVALUES (13, 'ESTJ（主管）', '务实果断，善于组织和管理，重视效率和目标。', 'icon_url_estj', '[\"E\",\"S\",\"T\",\"J\"]', null,\n        '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1);\nINSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime,\n                            updateTime, isDelete, appId, userId)\nVALUES (14, 'ESFJ（尽责者）', '友善热心，以协调、耐心和关怀为特点，善于团队合作。', 'icon_url_esfj', '[\"E\",\"S\",\"F\",\"J\"]',\n        null, '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1);\nINSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime,\n                            updateTime, isDelete, appId, userId)\nVALUES (15, 'ENFJ（教导着）', '热情关爱，善于帮助他人，具有领导力和社交能力。', 'icon_url_enfj', '[\"E\",\"N\",\"F\",\"J\"]', null,\n        '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1);\nINSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime,\n                            updateTime, isDelete, appId, userId)\nVALUES (16, 'ENTJ（统帅）', '果断自信，具有领导才能，善于规划和执行目标。', 'icon_url_entj', '[\"E\",\"N\",\"T\",\"J\"]', null,\n        '2024-04-24 16:57:02', '2024-05-09 12:28:21', 0, 1, 1);\nINSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime,\n                            updateTime, isDelete, appId, userId)\nVALUES (17, '首都知识大师', '你真棒棒哦，首都知识非常出色！', null, null, 9, '2024-04-25 15:05:44', '2024-05-09 12:28:21',\n        0, 2, 1);\nINSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime,\n                            updateTime, isDelete, appId, userId)\nVALUES (18, '地理小能手！', '你对于世界各国的首都了解得相当不错，但还有一些小地方需要加强哦！', null, null, 7,\n        '2024-04-25 15:05:44', '2024-05-09 12:28:21', 0, 2, 1);\nINSERT INTO scoring_result (id, resultName, resultDesc, resultPicture, resultProp, resultScoreRange, createTime,\n                            updateTime, isDelete, appId, userId)\nVALUES (19, '继续加油！', '还需努力哦', null, null, 0, '2024-04-25 15:05:44', '2024-05-09 12:28:21', 0, 2, 1);\n\n-- 用户回答表初始数据\nINSERT INTO user_answer (id, appId, appType, choices, resultId, resultName, resultDesc, resultPicture, resultScore,\n                              scoringStrategy, userId, createTime, updateTime, isDelete)\nVALUES (1, 1, 1, '[\"A\",\"A\",\"A\",\"B\",\"A\",\"A\",\"A\",\"B\",\"B\",\"A\"]', 1, 'ISTJ（物流师）', '忠诚可靠，被公认为务实，注重细节。', 'icon_url_istj',\n        null, 0, 1, '2024-05-09 15:08:22', '2024-05-09 15:10:13', 0);\nINSERT INTO user_answer (id, appId, appType, choices, resultId, resultName, resultDesc, resultPicture, resultScore,\n                              scoringStrategy, userId, createTime, updateTime, isDelete)\nVALUES (2, 2, 0, '[\"D\",\"C\",\"B\",\"D\",\"A\",\"C\",\"C\",\"B\",\"C\",\"A\"]', 17, '首都知识大师', '你真棒棒哦，首都知识非常出色！', null, 10, 0, 1,\n        '2024-05-09 15:08:36', '2024-05-09 15:10:13', 0);\n"
  },
  {
    "path": "yudada-backend/sql/post_es_mapping.json",
    "content": "{\n  \"aliases\": {\n    \"post\": {}\n  },\n  \"mappings\": {\n    \"properties\": {\n      \"title\": {\n        \"type\": \"text\",\n        \"analyzer\": \"ik_max_word\",\n        \"search_analyzer\": \"ik_smart\",\n        \"fields\": {\n          \"keyword\": {\n            \"type\": \"keyword\",\n            \"ignore_above\": 256\n          }\n        }\n      },\n      \"content\": {\n        \"type\": \"text\",\n        \"analyzer\": \"ik_max_word\",\n        \"search_analyzer\": \"ik_smart\",\n        \"fields\": {\n          \"keyword\": {\n            \"type\": \"keyword\",\n            \"ignore_above\": 256\n          }\n        }\n      },\n      \"tags\": {\n        \"type\": \"keyword\"\n      },\n      \"thumbNum\": {\n        \"type\": \"long\"\n      },\n      \"favourNum\": {\n        \"type\": \"long\"\n      },\n      \"userId\": {\n        \"type\": \"keyword\"\n      },\n      \"createTime\": {\n        \"type\": \"date\"\n      },\n      \"updateTime\": {\n        \"type\": \"date\"\n      },\n      \"isDelete\": {\n        \"type\": \"keyword\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/MainApplication.java",
    "content": "package com.yupi.yudada;\n\nimport org.mybatis.spring.annotation.MapperScan;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;\nimport org.springframework.context.annotation.EnableAspectJAutoProxy;\nimport org.springframework.scheduling.annotation.EnableScheduling;\n\n/**\n * 主类（项目启动入口）\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n// todo 如需开启 Redis，须移除 exclude 中的内容\n@SpringBootApplication\n@MapperScan(\"com.yupi.yudada.mapper\")\n@EnableScheduling\n@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)\npublic class MainApplication {\n\n    public static void main(String[] args) {\n        SpringApplication.run(MainApplication.class, args);\n    }\n\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/annotation/AuthCheck.java",
    "content": "package com.yupi.yudada.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 权限校验\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Target(ElementType.METHOD)\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface AuthCheck {\n\n    /**\n     * 必须有某个角色\n     *\n     * @return\n     */\n    String mustRole() default \"\";\n\n}\n\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/aop/AuthInterceptor.java",
    "content": "package com.yupi.yudada.aop;\n\nimport com.yupi.yudada.annotation.AuthCheck;\nimport com.yupi.yudada.common.ErrorCode;\nimport com.yupi.yudada.exception.BusinessException;\nimport com.yupi.yudada.model.entity.User;\nimport com.yupi.yudada.model.enums.UserRoleEnum;\nimport com.yupi.yudada.service.UserService;\nimport org.aspectj.lang.ProceedingJoinPoint;\nimport org.aspectj.lang.annotation.Around;\nimport org.aspectj.lang.annotation.Aspect;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.context.request.RequestAttributes;\nimport org.springframework.web.context.request.RequestContextHolder;\nimport org.springframework.web.context.request.ServletRequestAttributes;\n\nimport javax.annotation.Resource;\nimport javax.servlet.http.HttpServletRequest;\n\n/**\n * 权限校验 AOP\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Aspect\n@Component\npublic class AuthInterceptor {\n\n    @Resource\n    private UserService userService;\n\n    /**\n     * 执行拦截\n     *\n     * @param joinPoint\n     * @param authCheck\n     * @return\n     */\n    @Around(\"@annotation(authCheck)\")\n    public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable {\n        String mustRole = authCheck.mustRole();\n        RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();\n        HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();\n        // 当前登录用户\n        User loginUser = userService.getLoginUser(request);\n        UserRoleEnum mustRoleEnum = UserRoleEnum.getEnumByValue(mustRole);\n        // 不需要权限，放行\n        if (mustRoleEnum == null) {\n            return joinPoint.proceed();\n        }\n        // 必须有该权限才通过\n        UserRoleEnum userRoleEnum = UserRoleEnum.getEnumByValue(loginUser.getUserRole());\n        if (userRoleEnum == null) {\n            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);\n        }\n        // 如果被封号，直接拒绝\n        if (UserRoleEnum.BAN.equals(userRoleEnum)) {\n            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);\n        }\n        // 必须有管理员权限\n        if (UserRoleEnum.ADMIN.equals(mustRoleEnum)) {\n            // 用户没有管理员权限，拒绝\n            if (!UserRoleEnum.ADMIN.equals(userRoleEnum)) {\n                throw new BusinessException(ErrorCode.NO_AUTH_ERROR);\n            }\n        }\n        // 通过权限校验，放行\n        return joinPoint.proceed();\n    }\n}\n\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/aop/LogInterceptor.java",
    "content": "package com.yupi.yudada.aop;\n\nimport java.util.UUID;\nimport javax.servlet.http.HttpServletRequest;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.aspectj.lang.ProceedingJoinPoint;\nimport org.aspectj.lang.annotation.Around;\nimport org.aspectj.lang.annotation.Aspect;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.StopWatch;\nimport org.springframework.web.context.request.RequestAttributes;\nimport org.springframework.web.context.request.RequestContextHolder;\nimport org.springframework.web.context.request.ServletRequestAttributes;\n\n/**\n * 请求响应日志 AOP\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n **/\n@Aspect\n@Component\n@Slf4j\npublic class LogInterceptor {\n\n    /**\n     * 执行拦截\n     */\n    @Around(\"execution(* com.yupi.yudada.controller.*.*(..))\")\n    public Object doInterceptor(ProceedingJoinPoint point) throws Throwable {\n        // 计时\n        StopWatch stopWatch = new StopWatch();\n        stopWatch.start();\n        // 获取请求路径\n        RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();\n        HttpServletRequest httpServletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();\n        // 生成请求唯一 id\n        String requestId = UUID.randomUUID().toString();\n        String url = httpServletRequest.getRequestURI();\n        // 获取请求参数\n        Object[] args = point.getArgs();\n        String reqParam = \"[\" + StringUtils.join(args, \", \") + \"]\";\n        // 输出请求日志\n        log.info(\"request start，id: {}, path: {}, ip: {}, params: {}\", requestId, url,\n                httpServletRequest.getRemoteHost(), reqParam);\n        // 执行原方法\n        Object result = point.proceed();\n        // 输出响应日志\n        stopWatch.stop();\n        long totalTimeMillis = stopWatch.getTotalTimeMillis();\n        log.info(\"request end, id: {}, cost: {}ms\", requestId, totalTimeMillis);\n        return result;\n    }\n}\n\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/common/BaseResponse.java",
    "content": "package com.yupi.yudada.common;\n\nimport java.io.Serializable;\nimport lombok.Data;\n\n/**\n * 通用返回类\n *\n * @param <T>\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Data\npublic class BaseResponse<T> implements Serializable {\n\n    private int code;\n\n    private T data;\n\n    private String message;\n\n    public BaseResponse(int code, T data, String message) {\n        this.code = code;\n        this.data = data;\n        this.message = message;\n    }\n\n    public BaseResponse(int code, T data) {\n        this(code, data, \"\");\n    }\n\n    public BaseResponse(ErrorCode errorCode) {\n        this(errorCode.getCode(), null, errorCode.getMessage());\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/common/DeleteRequest.java",
    "content": "package com.yupi.yudada.common;\n\nimport java.io.Serializable;\nimport lombok.Data;\n\n/**\n * 删除请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Data\npublic class DeleteRequest implements Serializable {\n\n    /**\n     * id\n     */\n    private Long id;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/common/ErrorCode.java",
    "content": "package com.yupi.yudada.common;\n\n/**\n * 自定义错误码\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\npublic enum ErrorCode {\n\n    SUCCESS(0, \"ok\"),\n    PARAMS_ERROR(40000, \"请求参数错误\"),\n    NOT_LOGIN_ERROR(40100, \"未登录\"),\n    NO_AUTH_ERROR(40101, \"无权限\"),\n    NOT_FOUND_ERROR(40400, \"请求数据不存在\"),\n    FORBIDDEN_ERROR(40300, \"禁止访问\"),\n    SYSTEM_ERROR(50000, \"系统内部异常\"),\n    OPERATION_ERROR(50001, \"操作失败\");\n\n    /**\n     * 状态码\n     */\n    private final int code;\n\n    /**\n     * 信息\n     */\n    private final String message;\n\n    ErrorCode(int code, String message) {\n        this.code = code;\n        this.message = message;\n    }\n\n    public int getCode() {\n        return code;\n    }\n\n    public String getMessage() {\n        return message;\n    }\n\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/common/PageRequest.java",
    "content": "package com.yupi.yudada.common;\n\nimport com.yupi.yudada.constant.CommonConstant;\nimport lombok.Data;\n\n/**\n * 分页请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Data\npublic class PageRequest {\n\n    /**\n     * 当前页号\n     */\n    private int current = 1;\n\n    /**\n     * 页面大小\n     */\n    private int pageSize = 10;\n\n    /**\n     * 排序字段\n     */\n    private String sortField;\n\n    /**\n     * 排序顺序（默认升序）\n     */\n    private String sortOrder = CommonConstant.SORT_ORDER_ASC;\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/common/ResultUtils.java",
    "content": "package com.yupi.yudada.common;\n\n/**\n * 返回工具类\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\npublic class ResultUtils {\n\n    /**\n     * 成功\n     *\n     * @param data\n     * @param <T>\n     * @return\n     */\n    public static <T> BaseResponse<T> success(T data) {\n        return new BaseResponse<>(0, data, \"ok\");\n    }\n\n    /**\n     * 失败\n     *\n     * @param errorCode\n     * @return\n     */\n    public static BaseResponse error(ErrorCode errorCode) {\n        return new BaseResponse<>(errorCode);\n    }\n\n    /**\n     * 失败\n     *\n     * @param code\n     * @param message\n     * @return\n     */\n    public static BaseResponse error(int code, String message) {\n        return new BaseResponse(code, null, message);\n    }\n\n    /**\n     * 失败\n     *\n     * @param errorCode\n     * @return\n     */\n    public static BaseResponse error(ErrorCode errorCode, String message) {\n        return new BaseResponse(errorCode.getCode(), null, message);\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/common/ReviewRequest.java",
    "content": "package com.yupi.yudada.common;\n\nimport lombok.Data;\n\nimport java.io.Serializable;\n\n/**\n * 审核请求\n */\n@Data\npublic class ReviewRequest implements Serializable {\n\n    /**\n     * id\n     */\n    private Long id;\n\n    /**\n     * 状态：0-待审核, 1-通过, 2-拒绝\n     */\n    private Integer reviewStatus;\n\n    /**\n     * 审核信息\n     */\n    private String reviewMessage;\n\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/config/AiConfig.java",
    "content": "package com.yupi.yudada.config;\n\nimport com.zhipu.oapi.ClientV4;\nimport lombok.Data;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n@Configuration\n@ConfigurationProperties(prefix = \"ai\")\n@Data\npublic class AiConfig {\n\n    /**\n     * apiKey，需要从开放平台获取\n     */\n\n    private String apiKey;\n\n    @Bean\n    public ClientV4 getClientV4() {\n        return new ClientV4.Builder(apiKey).build();\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/config/CorsConfig.java",
    "content": "package com.yupi.yudada.config;\n\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.CorsRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n\n/**\n * 全局跨域配置\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Configuration\npublic class CorsConfig implements WebMvcConfigurer {\n\n    @Override\n    public void addCorsMappings(CorsRegistry registry) {\n        // 覆盖所有请求\n        registry.addMapping(\"/**\")\n                // 允许发送 Cookie\n                .allowCredentials(true)\n                // 放行哪些域名（必须用 patterns，否则 * 会和 allowCredentials 冲突）\n                .allowedOriginPatterns(\"*\")\n                .allowedMethods(\"GET\", \"POST\", \"PUT\", \"DELETE\", \"OPTIONS\")\n                .allowedHeaders(\"*\")\n                .exposedHeaders(\"*\");\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/config/CosClientConfig.java",
    "content": "package com.yupi.yudada.config;\n\nimport com.qcloud.cos.COSClient;\nimport com.qcloud.cos.ClientConfig;\nimport com.qcloud.cos.auth.BasicCOSCredentials;\nimport com.qcloud.cos.auth.COSCredentials;\nimport com.qcloud.cos.region.Region;\nimport lombok.Data;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n/**\n * 腾讯云对象存储客户端\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Configuration\n@ConfigurationProperties(prefix = \"cos.client\")\n@Data\npublic class CosClientConfig {\n\n    /**\n     * accessKey\n     */\n    private String accessKey;\n\n    /**\n     * secretKey\n     */\n    private String secretKey;\n\n    /**\n     * 区域\n     */\n    private String region;\n\n    /**\n     * 桶名\n     */\n    private String bucket;\n\n    @Bean\n    public COSClient cosClient() {\n        // 初始化用户身份信息(secretId, secretKey)\n        COSCredentials cred = new BasicCOSCredentials(accessKey, secretKey);\n        // 设置bucket的区域, COS地域的简称请参照 https://www.qcloud.com/document/product/436/6224\n        ClientConfig clientConfig = new ClientConfig(new Region(region));\n        // 生成cos客户端\n        return new COSClient(cred, clientConfig);\n    }\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/config/JsonConfig.java",
    "content": "package com.yupi.yudada.config;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.module.SimpleModule;\nimport com.fasterxml.jackson.databind.ser.std.ToStringSerializer;\nimport org.springframework.boot.jackson.JsonComponent;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;\n\n/**\n * Spring MVC Json 配置\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@JsonComponent\npublic class JsonConfig {\n\n    /**\n     * 添加 Long 转 json 精度丢失的配置\n     */\n    @Bean\n    public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {\n        ObjectMapper objectMapper = builder.createXmlMapper(false).build();\n        SimpleModule module = new SimpleModule();\n        module.addSerializer(Long.class, ToStringSerializer.instance);\n        module.addSerializer(Long.TYPE, ToStringSerializer.instance);\n        objectMapper.registerModule(module);\n        return objectMapper;\n    }\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/config/MyBatisPlusConfig.java",
    "content": "package com.yupi.yudada.config;\n\nimport com.baomidou.mybatisplus.annotation.DbType;\nimport com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;\nimport com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;\nimport org.mybatis.spring.annotation.MapperScan;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n/**\n * MyBatis Plus 配置\n *\n * @author https://github.com/liyupi\n */\n@Configuration\n@MapperScan(\"com.yupi.yudada.mapper\")\npublic class MyBatisPlusConfig {\n\n    /**\n     * 拦截器配置\n     *\n     * @return\n     */\n    @Bean\n    public MybatisPlusInterceptor mybatisPlusInterceptor() {\n        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();\n        // 分页插件\n        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));\n        return interceptor;\n    }\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/config/RedissonConfig.java",
    "content": "package com.yupi.yudada.config;\n\nimport com.zhipu.oapi.ClientV4;\nimport lombok.Data;\nimport org.redisson.Redisson;\nimport org.redisson.api.RedissonClient;\nimport org.redisson.config.Config;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n@Configuration\n@ConfigurationProperties(prefix = \"spring.redis\")\n@Data\npublic class RedissonConfig {\n\n\n    private String host;\n\n    private Integer port;\n\n    private Integer database;\n\n    private String password;\n\n    @Bean\n    public RedissonClient redissonClient() {\n        Config config = new Config();\n        config.useSingleServer()\n                .setAddress(\"redis://\" + host + \":\" + port)\n                .setDatabase(database)\n                .setPassword(password);\n        return Redisson.create(config);\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/config/VipSchedulerConfig.java",
    "content": "package com.yupi.yudada.config;\n\nimport io.reactivex.Scheduler;\nimport io.reactivex.schedulers.Schedulers;\nimport lombok.Data;\nimport org.jetbrains.annotations.NotNull;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.ScheduledExecutorService;\nimport java.util.concurrent.ThreadFactory;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n@Configuration\n@Data\npublic class VipSchedulerConfig {\n\n    @Bean\n    public Scheduler vipScheduler() {\n        ThreadFactory threadFactory = new ThreadFactory() {\n            private final AtomicInteger threadNumber = new AtomicInteger(1);\n\n            @Override\n            public Thread newThread(@NotNull Runnable r) {\n                Thread thread = new Thread(r, \"VIPThreadPool-\" + threadNumber.getAndIncrement());\n                // 非守护线程\n                thread.setDaemon(false);\n                return thread;\n            }\n        };\n        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10, threadFactory);\n        return Schedulers.from(scheduledExecutorService);\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/constant/CommonConstant.java",
    "content": "package com.yupi.yudada.constant;\n\n/**\n * 通用常量\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\npublic interface CommonConstant {\n\n    /**\n     * 升序\n     */\n    String SORT_ORDER_ASC = \"ascend\";\n\n    /**\n     * 降序\n     */\n    String SORT_ORDER_DESC = \" descend\";\n    \n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/constant/FileConstant.java",
    "content": "package com.yupi.yudada.constant;\n\n/**\n * 文件常量\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\npublic interface FileConstant {\n\n    /**\n     * COS 访问地址\n     * todo 需替换配置\n     */\n    String COS_HOST = \"https://yupi.icu\";\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/constant/UserConstant.java",
    "content": "package com.yupi.yudada.constant;\n\n/**\n * 用户常量\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\npublic interface UserConstant {\n\n    /**\n     * 用户登录态键\n     */\n    String USER_LOGIN_STATE = \"user_login\";\n\n    //  region 权限\n\n    /**\n     * 默认角色\n     */\n    String DEFAULT_ROLE = \"user\";\n\n    /**\n     * 管理员角色\n     */\n    String ADMIN_ROLE = \"admin\";\n\n    /**\n     * 被封号\n     */\n    String BAN_ROLE = \"ban\";\n\n    // endregion\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/controller/AppController.java",
    "content": "package com.yupi.yudada.controller;\n\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.yupi.yudada.annotation.AuthCheck;\nimport com.yupi.yudada.common.*;\nimport com.yupi.yudada.constant.UserConstant;\nimport com.yupi.yudada.exception.BusinessException;\nimport com.yupi.yudada.exception.ThrowUtils;\nimport com.yupi.yudada.model.dto.app.AppAddRequest;\nimport com.yupi.yudada.model.dto.app.AppEditRequest;\nimport com.yupi.yudada.model.dto.app.AppQueryRequest;\nimport com.yupi.yudada.model.dto.app.AppUpdateRequest;\nimport com.yupi.yudada.model.entity.App;\nimport com.yupi.yudada.model.entity.User;\nimport com.yupi.yudada.model.enums.ReviewStatusEnum;\nimport com.yupi.yudada.model.vo.AppVO;\nimport com.yupi.yudada.service.AppService;\nimport com.yupi.yudada.service.UserService;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.web.bind.annotation.*;\n\nimport javax.annotation.Resource;\nimport javax.servlet.http.HttpServletRequest;\nimport java.util.Date;\n\n/**\n * 应用接口\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@RestController\n@RequestMapping(\"/app\")\n@Slf4j\npublic class AppController {\n\n    @Resource\n    private AppService appService;\n\n    @Resource\n    private UserService userService;\n\n    // region 增删改查\n\n    /**\n     * 创建应用\n     *\n     * @param appAddRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/add\")\n    public BaseResponse<Long> addApp(@RequestBody AppAddRequest appAddRequest, HttpServletRequest request) {\n        ThrowUtils.throwIf(appAddRequest == null, ErrorCode.PARAMS_ERROR);\n        // 在此处将实体类和 DTO 进行转换\n        App app = new App();\n        BeanUtils.copyProperties(appAddRequest, app);\n        // 数据校验\n        appService.validApp(app, true);\n        // 填充默认值\n        User loginUser = userService.getLoginUser(request);\n        app.setUserId(loginUser.getId());\n        app.setReviewStatus(ReviewStatusEnum.REVIEWING.getValue());\n        // 写入数据库\n        boolean result = appService.save(app);\n        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);\n        // 返回新写入的数据 id\n        long newAppId = app.getId();\n        return ResultUtils.success(newAppId);\n    }\n\n    /**\n     * 删除应用\n     *\n     * @param deleteRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/delete\")\n    public BaseResponse<Boolean> deleteApp(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {\n        if (deleteRequest == null || deleteRequest.getId() <= 0) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        User user = userService.getLoginUser(request);\n        long id = deleteRequest.getId();\n        // 判断是否存在\n        App oldApp = appService.getById(id);\n        ThrowUtils.throwIf(oldApp == null, ErrorCode.NOT_FOUND_ERROR);\n        // 仅本人或管理员可删除\n        if (!oldApp.getUserId().equals(user.getId()) && !userService.isAdmin(request)) {\n            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);\n        }\n        // 操作数据库\n        boolean result = appService.removeById(id);\n        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);\n        return ResultUtils.success(true);\n    }\n\n    /**\n     * 更新应用（仅管理员可用）\n     *\n     * @param appUpdateRequest\n     * @return\n     */\n    @PostMapping(\"/update\")\n    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)\n    public BaseResponse<Boolean> updateApp(@RequestBody AppUpdateRequest appUpdateRequest) {\n        if (appUpdateRequest == null || appUpdateRequest.getId() <= 0) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        // 在此处将实体类和 DTO 进行转换\n        App app = new App();\n        BeanUtils.copyProperties(appUpdateRequest, app);\n        // 数据校验\n        appService.validApp(app, false);\n        // 判断是否存在\n        long id = appUpdateRequest.getId();\n        App oldApp = appService.getById(id);\n        ThrowUtils.throwIf(oldApp == null, ErrorCode.NOT_FOUND_ERROR);\n        // 操作数据库\n        boolean result = appService.updateById(app);\n        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);\n        return ResultUtils.success(true);\n    }\n\n    /**\n     * 根据 id 获取应用（封装类）\n     *\n     * @param id\n     * @return\n     */\n    @GetMapping(\"/get/vo\")\n    public BaseResponse<AppVO> getAppVOById(long id, HttpServletRequest request) {\n        ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR);\n        // 查询数据库\n        App app = appService.getById(id);\n        ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR);\n        // 获取封装类\n        return ResultUtils.success(appService.getAppVO(app, request));\n    }\n\n    /**\n     * 分页获取应用列表（仅管理员可用）\n     *\n     * @param appQueryRequest\n     * @return\n     */\n    @PostMapping(\"/list/page\")\n    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)\n    public BaseResponse<Page<App>> listAppByPage(@RequestBody AppQueryRequest appQueryRequest) {\n        long current = appQueryRequest.getCurrent();\n        long size = appQueryRequest.getPageSize();\n        // 查询数据库\n        Page<App> appPage = appService.page(new Page<>(current, size),\n                appService.getQueryWrapper(appQueryRequest));\n        return ResultUtils.success(appPage);\n    }\n\n    /**\n     * 分页获取应用列表（封装类）\n     *\n     * @param appQueryRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/list/page/vo\")\n    public BaseResponse<Page<AppVO>> listAppVOByPage(@RequestBody AppQueryRequest appQueryRequest,\n                                                     HttpServletRequest request) {\n        long current = appQueryRequest.getCurrent();\n        long size = appQueryRequest.getPageSize();\n        // 限制爬虫\n        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);\n        // 只能看到已过审的应用\n        appQueryRequest.setReviewStatus(ReviewStatusEnum.PASS.getValue());\n        // 查询数据库\n        Page<App> appPage = appService.page(new Page<>(current, size),\n                appService.getQueryWrapper(appQueryRequest));\n        // 获取封装类\n        return ResultUtils.success(appService.getAppVOPage(appPage, request));\n    }\n\n    /**\n     * 分页获取当前登录用户创建的应用列表\n     *\n     * @param appQueryRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/my/list/page/vo\")\n    public BaseResponse<Page<AppVO>> listMyAppVOByPage(@RequestBody AppQueryRequest appQueryRequest,\n                                                       HttpServletRequest request) {\n        ThrowUtils.throwIf(appQueryRequest == null, ErrorCode.PARAMS_ERROR);\n        // 补充查询条件，只查询当前登录用户的数据\n        User loginUser = userService.getLoginUser(request);\n        appQueryRequest.setUserId(loginUser.getId());\n        long current = appQueryRequest.getCurrent();\n        long size = appQueryRequest.getPageSize();\n        // 限制爬虫\n        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);\n        // 查询数据库\n        Page<App> appPage = appService.page(new Page<>(current, size),\n                appService.getQueryWrapper(appQueryRequest));\n        // 获取封装类\n        return ResultUtils.success(appService.getAppVOPage(appPage, request));\n    }\n\n    /**\n     * 编辑应用（给用户使用）\n     *\n     * @param appEditRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/edit\")\n    public BaseResponse<Boolean> editApp(@RequestBody AppEditRequest appEditRequest, HttpServletRequest request) {\n        if (appEditRequest == null || appEditRequest.getId() <= 0) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        // 在此处将实体类和 DTO 进行转换\n        App app = new App();\n        BeanUtils.copyProperties(appEditRequest, app);\n        // 数据校验\n        appService.validApp(app, false);\n        User loginUser = userService.getLoginUser(request);\n        // 判断是否存在\n        long id = appEditRequest.getId();\n        App oldApp = appService.getById(id);\n        ThrowUtils.throwIf(oldApp == null, ErrorCode.NOT_FOUND_ERROR);\n        // 仅本人或管理员可编辑\n        if (!oldApp.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {\n            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);\n        }\n        // 重置审核状态\n        app.setReviewStatus(ReviewStatusEnum.REVIEWING.getValue());\n        // 操作数据库\n        boolean result = appService.updateById(app);\n        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);\n        return ResultUtils.success(true);\n    }\n\n    // endregion\n\n    /**\n     * 应用审核\n     *\n     * @param reviewRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/review\")\n    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)\n    public BaseResponse<Boolean> doAppReview(@RequestBody ReviewRequest reviewRequest, HttpServletRequest request) {\n        ThrowUtils.throwIf(reviewRequest == null, ErrorCode.PARAMS_ERROR);\n        Long id = reviewRequest.getId();\n        Integer reviewStatus = reviewRequest.getReviewStatus();\n        // 校验\n        ReviewStatusEnum reviewStatusEnum = ReviewStatusEnum.getEnumByValue(reviewStatus);\n        if (id == null || reviewStatusEnum == null) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        // 判断是否存在\n        App oldApp = appService.getById(id);\n        ThrowUtils.throwIf(oldApp == null, ErrorCode.NOT_FOUND_ERROR);\n        // 已是该状态\n        if (oldApp.getReviewStatus().equals(reviewStatus)) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR, \"请勿重复审核\");\n        }\n        // 更新审核状态\n        User loginUser = userService.getLoginUser(request);\n        App app = new App();\n        app.setId(id);\n        app.setReviewStatus(reviewStatus);\n        app.setReviewMessage(reviewRequest.getReviewMessage());\n        app.setReviewerId(loginUser.getId());\n        app.setReviewTime(new Date());\n        boolean result = appService.updateById(app);\n        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);\n        return ResultUtils.success(true);\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/controller/AppStatisticController.java",
    "content": "package com.yupi.yudada.controller;\n\nimport cn.hutool.core.io.FileUtil;\nimport com.yupi.yudada.common.BaseResponse;\nimport com.yupi.yudada.common.ErrorCode;\nimport com.yupi.yudada.common.ResultUtils;\nimport com.yupi.yudada.constant.FileConstant;\nimport com.yupi.yudada.exception.BusinessException;\nimport com.yupi.yudada.exception.ThrowUtils;\nimport com.yupi.yudada.manager.CosManager;\nimport com.yupi.yudada.mapper.UserAnswerMapper;\nimport com.yupi.yudada.model.dto.file.UploadFileRequest;\nimport com.yupi.yudada.model.dto.statistic.AppAnswerCountDTO;\nimport com.yupi.yudada.model.dto.statistic.AppAnswerResultCountDTO;\nimport com.yupi.yudada.model.entity.User;\nimport com.yupi.yudada.model.enums.FileUploadBizEnum;\nimport com.yupi.yudada.service.UserService;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.RandomStringUtils;\nimport org.springframework.web.bind.annotation.*;\nimport org.springframework.web.multipart.MultipartFile;\n\nimport javax.annotation.Resource;\nimport javax.servlet.http.HttpServletRequest;\nimport java.io.File;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * App 统计分析接口\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@RestController\n@RequestMapping(\"/app/statistic\")\n@Slf4j\npublic class AppStatisticController {\n\n    @Resource\n    private UserAnswerMapper userAnswerMapper;\n\n    /**\n     * 热门应用及回答数统计（top 10）\n     *\n     * @return\n     */\n    @GetMapping(\"/answer_count\")\n    public BaseResponse<List<AppAnswerCountDTO>> getAppAnswerCount() {\n        return ResultUtils.success(userAnswerMapper.doAppAnswerCount());\n    }\n\n    /**\n     * 某应用回答结果分布统计\n     *\n     * @param appId\n     * @return\n     */\n    @GetMapping(\"/answer_result_count\")\n    public BaseResponse<List<AppAnswerResultCountDTO>> getAppAnswerResultCount(Long appId) {\n        ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR);\n        return ResultUtils.success(userAnswerMapper.doAppAnswerResultCount(appId));\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/controller/FileController.java",
    "content": "package com.yupi.yudada.controller;\n\nimport cn.hutool.core.io.FileUtil;\nimport com.yupi.yudada.common.BaseResponse;\nimport com.yupi.yudada.common.ErrorCode;\nimport com.yupi.yudada.common.ResultUtils;\nimport com.yupi.yudada.constant.FileConstant;\nimport com.yupi.yudada.exception.BusinessException;\nimport com.yupi.yudada.manager.CosManager;\nimport com.yupi.yudada.model.dto.file.UploadFileRequest;\nimport com.yupi.yudada.model.entity.User;\nimport com.yupi.yudada.model.enums.FileUploadBizEnum;\nimport com.yupi.yudada.service.UserService;\nimport java.io.File;\nimport java.util.Arrays;\nimport javax.annotation.Resource;\nimport javax.servlet.http.HttpServletRequest;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.RandomStringUtils;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestPart;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.multipart.MultipartFile;\n\n/**\n * 文件接口\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@RestController\n@RequestMapping(\"/file\")\n@Slf4j\npublic class FileController {\n\n    @Resource\n    private UserService userService;\n\n    @Resource\n    private CosManager cosManager;\n\n    /**\n     * 文件上传\n     *\n     * @param multipartFile\n     * @param uploadFileRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/upload\")\n    public BaseResponse<String> uploadFile(@RequestPart(\"file\") MultipartFile multipartFile,\n            UploadFileRequest uploadFileRequest, HttpServletRequest request) {\n        String biz = uploadFileRequest.getBiz();\n        FileUploadBizEnum fileUploadBizEnum = FileUploadBizEnum.getEnumByValue(biz);\n        if (fileUploadBizEnum == null) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        validFile(multipartFile, fileUploadBizEnum);\n        User loginUser = userService.getLoginUser(request);\n        // 文件目录：根据业务、用户来划分\n        String uuid = RandomStringUtils.randomAlphanumeric(8);\n        String filename = uuid + \"-\" + multipartFile.getOriginalFilename();\n        String filepath = String.format(\"/%s/%s/%s\", fileUploadBizEnum.getValue(), loginUser.getId(), filename);\n        File file = null;\n        try {\n            // 上传文件\n            file = File.createTempFile(filepath, null);\n            multipartFile.transferTo(file);\n            cosManager.putObject(filepath, file);\n            // 返回可访问地址\n            return ResultUtils.success(FileConstant.COS_HOST + filepath);\n        } catch (Exception e) {\n            log.error(\"file upload error, filepath = \" + filepath, e);\n            throw new BusinessException(ErrorCode.SYSTEM_ERROR, \"上传失败\");\n        } finally {\n            if (file != null) {\n                // 删除临时文件\n                boolean delete = file.delete();\n                if (!delete) {\n                    log.error(\"file delete error, filepath = {}\", filepath);\n                }\n            }\n        }\n    }\n\n    /**\n     * 校验文件\n     *\n     * @param multipartFile\n     * @param fileUploadBizEnum 业务类型\n     */\n    private void validFile(MultipartFile multipartFile, FileUploadBizEnum fileUploadBizEnum) {\n        // 文件大小\n        long fileSize = multipartFile.getSize();\n        // 文件后缀\n        String fileSuffix = FileUtil.getSuffix(multipartFile.getOriginalFilename());\n        final long ONE_M = 1024 * 1024L;\n        if (FileUploadBizEnum.USER_AVATAR.equals(fileUploadBizEnum)) {\n            if (fileSize > ONE_M) {\n                throw new BusinessException(ErrorCode.PARAMS_ERROR, \"文件大小不能超过 1M\");\n            }\n            if (!Arrays.asList(\"jpeg\", \"jpg\", \"svg\", \"png\", \"webp\").contains(fileSuffix)) {\n                throw new BusinessException(ErrorCode.PARAMS_ERROR, \"文件类型错误\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/controller/PostController.java",
    "content": "package com.yupi.yudada.controller;\n\nimport cn.hutool.json.JSONUtil;\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.yupi.yudada.annotation.AuthCheck;\nimport com.yupi.yudada.common.BaseResponse;\nimport com.yupi.yudada.common.DeleteRequest;\nimport com.yupi.yudada.common.ErrorCode;\nimport com.yupi.yudada.common.ResultUtils;\nimport com.yupi.yudada.constant.UserConstant;\nimport com.yupi.yudada.exception.BusinessException;\nimport com.yupi.yudada.exception.ThrowUtils;\nimport com.yupi.yudada.model.dto.post.PostAddRequest;\nimport com.yupi.yudada.model.dto.post.PostEditRequest;\nimport com.yupi.yudada.model.dto.post.PostQueryRequest;\nimport com.yupi.yudada.model.dto.post.PostUpdateRequest;\nimport com.yupi.yudada.model.entity.Post;\nimport com.yupi.yudada.model.entity.User;\nimport com.yupi.yudada.model.vo.PostVO;\nimport com.yupi.yudada.service.PostService;\nimport com.yupi.yudada.service.UserService;\nimport java.util.List;\nimport javax.annotation.Resource;\nimport javax.servlet.http.HttpServletRequest;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * 帖子接口\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@RestController\n@RequestMapping(\"/post\")\n@Slf4j\npublic class PostController {\n\n    @Resource\n    private PostService postService;\n\n    @Resource\n    private UserService userService;\n\n    // region 增删改查\n\n    /**\n     * 创建\n     *\n     * @param postAddRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/add\")\n    public BaseResponse<Long> addPost(@RequestBody PostAddRequest postAddRequest, HttpServletRequest request) {\n        if (postAddRequest == null) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        Post post = new Post();\n        BeanUtils.copyProperties(postAddRequest, post);\n        List<String> tags = postAddRequest.getTags();\n        if (tags != null) {\n            post.setTags(JSONUtil.toJsonStr(tags));\n        }\n        postService.validPost(post, true);\n        User loginUser = userService.getLoginUser(request);\n        post.setUserId(loginUser.getId());\n        post.setFavourNum(0);\n        post.setThumbNum(0);\n        boolean result = postService.save(post);\n        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);\n        long newPostId = post.getId();\n        return ResultUtils.success(newPostId);\n    }\n\n    /**\n     * 删除\n     *\n     * @param deleteRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/delete\")\n    public BaseResponse<Boolean> deletePost(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {\n        if (deleteRequest == null || deleteRequest.getId() <= 0) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        User user = userService.getLoginUser(request);\n        long id = deleteRequest.getId();\n        // 判断是否存在\n        Post oldPost = postService.getById(id);\n        ThrowUtils.throwIf(oldPost == null, ErrorCode.NOT_FOUND_ERROR);\n        // 仅本人或管理员可删除\n        if (!oldPost.getUserId().equals(user.getId()) && !userService.isAdmin(request)) {\n            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);\n        }\n        boolean b = postService.removeById(id);\n        return ResultUtils.success(b);\n    }\n\n    /**\n     * 更新（仅管理员）\n     *\n     * @param postUpdateRequest\n     * @return\n     */\n    @PostMapping(\"/update\")\n    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)\n    public BaseResponse<Boolean> updatePost(@RequestBody PostUpdateRequest postUpdateRequest) {\n        if (postUpdateRequest == null || postUpdateRequest.getId() <= 0) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        Post post = new Post();\n        BeanUtils.copyProperties(postUpdateRequest, post);\n        List<String> tags = postUpdateRequest.getTags();\n        if (tags != null) {\n            post.setTags(JSONUtil.toJsonStr(tags));\n        }\n        // 参数校验\n        postService.validPost(post, false);\n        long id = postUpdateRequest.getId();\n        // 判断是否存在\n        Post oldPost = postService.getById(id);\n        ThrowUtils.throwIf(oldPost == null, ErrorCode.NOT_FOUND_ERROR);\n        boolean result = postService.updateById(post);\n        return ResultUtils.success(result);\n    }\n\n    /**\n     * 根据 id 获取\n     *\n     * @param id\n     * @return\n     */\n    @GetMapping(\"/get/vo\")\n    public BaseResponse<PostVO> getPostVOById(long id, HttpServletRequest request) {\n        if (id <= 0) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        Post post = postService.getById(id);\n        if (post == null) {\n            throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);\n        }\n        return ResultUtils.success(postService.getPostVO(post, request));\n    }\n\n    /**\n     * 分页获取列表（仅管理员）\n     *\n     * @param postQueryRequest\n     * @return\n     */\n    @PostMapping(\"/list/page\")\n    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)\n    public BaseResponse<Page<Post>> listPostByPage(@RequestBody PostQueryRequest postQueryRequest) {\n        long current = postQueryRequest.getCurrent();\n        long size = postQueryRequest.getPageSize();\n        Page<Post> postPage = postService.page(new Page<>(current, size),\n                postService.getQueryWrapper(postQueryRequest));\n        return ResultUtils.success(postPage);\n    }\n\n    /**\n     * 分页获取列表（封装类）\n     *\n     * @param postQueryRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/list/page/vo\")\n    public BaseResponse<Page<PostVO>> listPostVOByPage(@RequestBody PostQueryRequest postQueryRequest,\n            HttpServletRequest request) {\n        long current = postQueryRequest.getCurrent();\n        long size = postQueryRequest.getPageSize();\n        // 限制爬虫\n        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);\n        Page<Post> postPage = postService.page(new Page<>(current, size),\n                postService.getQueryWrapper(postQueryRequest));\n        return ResultUtils.success(postService.getPostVOPage(postPage, request));\n    }\n\n    /**\n     * 分页获取当前用户创建的资源列表\n     *\n     * @param postQueryRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/my/list/page/vo\")\n    public BaseResponse<Page<PostVO>> listMyPostVOByPage(@RequestBody PostQueryRequest postQueryRequest,\n            HttpServletRequest request) {\n        if (postQueryRequest == null) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        User loginUser = userService.getLoginUser(request);\n        postQueryRequest.setUserId(loginUser.getId());\n        long current = postQueryRequest.getCurrent();\n        long size = postQueryRequest.getPageSize();\n        // 限制爬虫\n        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);\n        Page<Post> postPage = postService.page(new Page<>(current, size),\n                postService.getQueryWrapper(postQueryRequest));\n        return ResultUtils.success(postService.getPostVOPage(postPage, request));\n    }\n\n    // endregion\n\n    /**\n     * 编辑（用户）\n     *\n     * @param postEditRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/edit\")\n    public BaseResponse<Boolean> editPost(@RequestBody PostEditRequest postEditRequest, HttpServletRequest request) {\n        if (postEditRequest == null || postEditRequest.getId() <= 0) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        Post post = new Post();\n        BeanUtils.copyProperties(postEditRequest, post);\n        List<String> tags = postEditRequest.getTags();\n        if (tags != null) {\n            post.setTags(JSONUtil.toJsonStr(tags));\n        }\n        // 参数校验\n        postService.validPost(post, false);\n        User loginUser = userService.getLoginUser(request);\n        long id = postEditRequest.getId();\n        // 判断是否存在\n        Post oldPost = postService.getById(id);\n        ThrowUtils.throwIf(oldPost == null, ErrorCode.NOT_FOUND_ERROR);\n        // 仅本人或管理员可编辑\n        if (!oldPost.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {\n            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);\n        }\n        boolean result = postService.updateById(post);\n        return ResultUtils.success(result);\n    }\n\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/controller/PostFavourController.java",
    "content": "package com.yupi.yudada.controller;\n\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.yupi.yudada.common.BaseResponse;\nimport com.yupi.yudada.common.ErrorCode;\nimport com.yupi.yudada.common.ResultUtils;\nimport com.yupi.yudada.exception.BusinessException;\nimport com.yupi.yudada.exception.ThrowUtils;\nimport com.yupi.yudada.model.dto.post.PostQueryRequest;\nimport com.yupi.yudada.model.dto.postfavour.PostFavourAddRequest;\nimport com.yupi.yudada.model.dto.postfavour.PostFavourQueryRequest;\nimport com.yupi.yudada.model.entity.Post;\nimport com.yupi.yudada.model.entity.User;\nimport com.yupi.yudada.model.vo.PostVO;\nimport com.yupi.yudada.service.PostFavourService;\nimport com.yupi.yudada.service.PostService;\nimport com.yupi.yudada.service.UserService;\nimport javax.annotation.Resource;\nimport javax.servlet.http.HttpServletRequest;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * 帖子收藏接口\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@RestController\n@RequestMapping(\"/post_favour\")\n@Slf4j\npublic class PostFavourController {\n\n    @Resource\n    private PostFavourService postFavourService;\n\n    @Resource\n    private PostService postService;\n\n    @Resource\n    private UserService userService;\n\n    /**\n     * 收藏 / 取消收藏\n     *\n     * @param postFavourAddRequest\n     * @param request\n     * @return resultNum 收藏变化数\n     */\n    @PostMapping(\"/\")\n    public BaseResponse<Integer> doPostFavour(@RequestBody PostFavourAddRequest postFavourAddRequest,\n            HttpServletRequest request) {\n        if (postFavourAddRequest == null || postFavourAddRequest.getPostId() <= 0) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        // 登录才能操作\n        final User loginUser = userService.getLoginUser(request);\n        long postId = postFavourAddRequest.getPostId();\n        int result = postFavourService.doPostFavour(postId, loginUser);\n        return ResultUtils.success(result);\n    }\n\n    /**\n     * 获取我收藏的帖子列表\n     *\n     * @param postQueryRequest\n     * @param request\n     */\n    @PostMapping(\"/my/list/page\")\n    public BaseResponse<Page<PostVO>> listMyFavourPostByPage(@RequestBody PostQueryRequest postQueryRequest,\n            HttpServletRequest request) {\n        if (postQueryRequest == null) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        User loginUser = userService.getLoginUser(request);\n        long current = postQueryRequest.getCurrent();\n        long size = postQueryRequest.getPageSize();\n        // 限制爬虫\n        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);\n        Page<Post> postPage = postFavourService.listFavourPostByPage(new Page<>(current, size),\n                postService.getQueryWrapper(postQueryRequest), loginUser.getId());\n        return ResultUtils.success(postService.getPostVOPage(postPage, request));\n    }\n\n    /**\n     * 获取用户收藏的帖子列表\n     *\n     * @param postFavourQueryRequest\n     * @param request\n     */\n    @PostMapping(\"/list/page\")\n    public BaseResponse<Page<PostVO>> listFavourPostByPage(@RequestBody PostFavourQueryRequest postFavourQueryRequest,\n            HttpServletRequest request) {\n        if (postFavourQueryRequest == null) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        long current = postFavourQueryRequest.getCurrent();\n        long size = postFavourQueryRequest.getPageSize();\n        Long userId = postFavourQueryRequest.getUserId();\n        // 限制爬虫\n        ThrowUtils.throwIf(size > 20 || userId == null, ErrorCode.PARAMS_ERROR);\n        Page<Post> postPage = postFavourService.listFavourPostByPage(new Page<>(current, size),\n                postService.getQueryWrapper(postFavourQueryRequest.getPostQueryRequest()), userId);\n        return ResultUtils.success(postService.getPostVOPage(postPage, request));\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/controller/PostThumbController.java",
    "content": "package com.yupi.yudada.controller;\n\nimport com.yupi.yudada.common.BaseResponse;\nimport com.yupi.yudada.common.ErrorCode;\nimport com.yupi.yudada.common.ResultUtils;\nimport com.yupi.yudada.exception.BusinessException;\nimport com.yupi.yudada.model.dto.postthumb.PostThumbAddRequest;\nimport com.yupi.yudada.model.entity.User;\nimport com.yupi.yudada.service.PostThumbService;\nimport com.yupi.yudada.service.UserService;\nimport javax.annotation.Resource;\nimport javax.servlet.http.HttpServletRequest;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * 帖子点赞接口\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@RestController\n@RequestMapping(\"/post_thumb\")\n@Slf4j\npublic class PostThumbController {\n\n    @Resource\n    private PostThumbService postThumbService;\n\n    @Resource\n    private UserService userService;\n\n    /**\n     * 点赞 / 取消点赞\n     *\n     * @param postThumbAddRequest\n     * @param request\n     * @return resultNum 本次点赞变化数\n     */\n    @PostMapping(\"/\")\n    public BaseResponse<Integer> doThumb(@RequestBody PostThumbAddRequest postThumbAddRequest,\n            HttpServletRequest request) {\n        if (postThumbAddRequest == null || postThumbAddRequest.getPostId() <= 0) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        // 登录才能点赞\n        final User loginUser = userService.getLoginUser(request);\n        long postId = postThumbAddRequest.getPostId();\n        int result = postThumbService.doPostThumb(postId, loginUser);\n        return ResultUtils.success(result);\n    }\n\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/controller/QuestionController.java",
    "content": "package com.yupi.yudada.controller;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.json.JSONUtil;\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.yupi.yudada.annotation.AuthCheck;\nimport com.yupi.yudada.common.BaseResponse;\nimport com.yupi.yudada.common.DeleteRequest;\nimport com.yupi.yudada.common.ErrorCode;\nimport com.yupi.yudada.common.ResultUtils;\nimport com.yupi.yudada.constant.UserConstant;\nimport com.yupi.yudada.exception.BusinessException;\nimport com.yupi.yudada.exception.ThrowUtils;\nimport com.yupi.yudada.manager.AiManager;\nimport com.yupi.yudada.model.dto.question.*;\nimport com.yupi.yudada.model.entity.App;\nimport com.yupi.yudada.model.entity.Question;\nimport com.yupi.yudada.model.entity.User;\nimport com.yupi.yudada.model.enums.AppTypeEnum;\nimport com.yupi.yudada.model.vo.QuestionVO;\nimport com.yupi.yudada.service.AppService;\nimport com.yupi.yudada.service.QuestionService;\nimport com.yupi.yudada.service.UserService;\nimport com.zhipu.oapi.service.v4.model.ModelData;\nimport io.reactivex.Flowable;\nimport io.reactivex.Scheduler;\nimport io.reactivex.schedulers.Schedulers;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.web.bind.annotation.*;\nimport org.springframework.web.servlet.mvc.method.annotation.SseEmitter;\n\nimport javax.annotation.Resource;\nimport javax.servlet.http.HttpServletRequest;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.stream.Stream;\n\n/**\n * 题目接口\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@RestController\n@RequestMapping(\"/question\")\n@Slf4j\npublic class QuestionController {\n\n    @Resource\n    private QuestionService questionService;\n\n    @Resource\n    private UserService userService;\n\n    @Resource\n    private AppService appService;\n\n    @Resource\n    private AiManager aiManager;\n\n    @Resource\n    private Scheduler vipScheduler;\n\n    // region 增删改查\n\n    /**\n     * 创建题目\n     *\n     * @param questionAddRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/add\")\n    public BaseResponse<Long> addQuestion(@RequestBody QuestionAddRequest questionAddRequest, HttpServletRequest request) {\n        ThrowUtils.throwIf(questionAddRequest == null, ErrorCode.PARAMS_ERROR);\n        // 在此处将实体类和 DTO 进行转换\n        Question question = new Question();\n        BeanUtils.copyProperties(questionAddRequest, question);\n        List<QuestionContentDTO> questionContentDTO = questionAddRequest.getQuestionContent();\n        question.setQuestionContent(JSONUtil.toJsonStr(questionContentDTO));\n        // 数据校验\n        questionService.validQuestion(question, true);\n        // 填充默认值\n        User loginUser = userService.getLoginUser(request);\n        question.setUserId(loginUser.getId());\n        // 写入数据库\n        boolean result = questionService.save(question);\n        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);\n        // 返回新写入的数据 id\n        long newQuestionId = question.getId();\n        return ResultUtils.success(newQuestionId);\n    }\n\n    /**\n     * 删除题目\n     *\n     * @param deleteRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/delete\")\n    public BaseResponse<Boolean> deleteQuestion(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {\n        if (deleteRequest == null || deleteRequest.getId() <= 0) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        User user = userService.getLoginUser(request);\n        long id = deleteRequest.getId();\n        // 判断是否存在\n        Question oldQuestion = questionService.getById(id);\n        ThrowUtils.throwIf(oldQuestion == null, ErrorCode.NOT_FOUND_ERROR);\n        // 仅本人或管理员可删除\n        if (!oldQuestion.getUserId().equals(user.getId()) && !userService.isAdmin(request)) {\n            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);\n        }\n        // 操作数据库\n        boolean result = questionService.removeById(id);\n        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);\n        return ResultUtils.success(true);\n    }\n\n    /**\n     * 更新题目（仅管理员可用）\n     *\n     * @param questionUpdateRequest\n     * @return\n     */\n    @PostMapping(\"/update\")\n    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)\n    public BaseResponse<Boolean> updateQuestion(@RequestBody QuestionUpdateRequest questionUpdateRequest) {\n        if (questionUpdateRequest == null || questionUpdateRequest.getId() <= 0) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        // 在此处将实体类和 DTO 进行转换\n        Question question = new Question();\n        BeanUtils.copyProperties(questionUpdateRequest, question);\n        List<QuestionContentDTO> questionContentDTO = questionUpdateRequest.getQuestionContent();\n        question.setQuestionContent(JSONUtil.toJsonStr(questionContentDTO));\n        // 数据校验\n        questionService.validQuestion(question, false);\n        // 判断是否存在\n        long id = questionUpdateRequest.getId();\n        Question oldQuestion = questionService.getById(id);\n        ThrowUtils.throwIf(oldQuestion == null, ErrorCode.NOT_FOUND_ERROR);\n        // 操作数据库\n        boolean result = questionService.updateById(question);\n        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);\n        return ResultUtils.success(true);\n    }\n\n    /**\n     * 根据 id 获取题目（封装类）\n     *\n     * @param id\n     * @return\n     */\n    @GetMapping(\"/get/vo\")\n    public BaseResponse<QuestionVO> getQuestionVOById(long id, HttpServletRequest request) {\n        ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR);\n        // 查询数据库\n        Question question = questionService.getById(id);\n        ThrowUtils.throwIf(question == null, ErrorCode.NOT_FOUND_ERROR);\n        // 获取封装类\n        return ResultUtils.success(questionService.getQuestionVO(question, request));\n    }\n\n    /**\n     * 分页获取题目列表（仅管理员可用）\n     *\n     * @param questionQueryRequest\n     * @return\n     */\n    @PostMapping(\"/list/page\")\n    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)\n    public BaseResponse<Page<Question>> listQuestionByPage(@RequestBody QuestionQueryRequest questionQueryRequest) {\n        long current = questionQueryRequest.getCurrent();\n        long size = questionQueryRequest.getPageSize();\n        // 查询数据库\n        Page<Question> questionPage = questionService.page(new Page<>(current, size),\n                questionService.getQueryWrapper(questionQueryRequest));\n        return ResultUtils.success(questionPage);\n    }\n\n    /**\n     * 分页获取题目列表（封装类）\n     *\n     * @param questionQueryRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/list/page/vo\")\n    public BaseResponse<Page<QuestionVO>> listQuestionVOByPage(@RequestBody QuestionQueryRequest questionQueryRequest,\n                                                               HttpServletRequest request) {\n        long current = questionQueryRequest.getCurrent();\n        long size = questionQueryRequest.getPageSize();\n        // 限制爬虫\n        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);\n        // 查询数据库\n        Page<Question> questionPage = questionService.page(new Page<>(current, size),\n                questionService.getQueryWrapper(questionQueryRequest));\n        // 获取封装类\n        return ResultUtils.success(questionService.getQuestionVOPage(questionPage, request));\n    }\n\n    /**\n     * 分页获取当前登录用户创建的题目列表\n     *\n     * @param questionQueryRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/my/list/page/vo\")\n    public BaseResponse<Page<QuestionVO>> listMyQuestionVOByPage(@RequestBody QuestionQueryRequest questionQueryRequest,\n                                                                 HttpServletRequest request) {\n        ThrowUtils.throwIf(questionQueryRequest == null, ErrorCode.PARAMS_ERROR);\n        // 补充查询条件，只查询当前登录用户的数据\n        User loginUser = userService.getLoginUser(request);\n        questionQueryRequest.setUserId(loginUser.getId());\n        long current = questionQueryRequest.getCurrent();\n        long size = questionQueryRequest.getPageSize();\n        // 限制爬虫\n        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);\n        // 查询数据库\n        Page<Question> questionPage = questionService.page(new Page<>(current, size),\n                questionService.getQueryWrapper(questionQueryRequest));\n        // 获取封装类\n        return ResultUtils.success(questionService.getQuestionVOPage(questionPage, request));\n    }\n\n    /**\n     * 编辑题目（给用户使用）\n     *\n     * @param questionEditRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/edit\")\n    public BaseResponse<Boolean> editQuestion(@RequestBody QuestionEditRequest questionEditRequest, HttpServletRequest request) {\n        if (questionEditRequest == null || questionEditRequest.getId() <= 0) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        // 在此处将实体类和 DTO 进行转换\n        Question question = new Question();\n        BeanUtils.copyProperties(questionEditRequest, question);\n        List<QuestionContentDTO> questionContentDTO = questionEditRequest.getQuestionContent();\n        question.setQuestionContent(JSONUtil.toJsonStr(questionContentDTO));\n        // 数据校验\n        questionService.validQuestion(question, false);\n        User loginUser = userService.getLoginUser(request);\n        // 判断是否存在\n        long id = questionEditRequest.getId();\n        Question oldQuestion = questionService.getById(id);\n        ThrowUtils.throwIf(oldQuestion == null, ErrorCode.NOT_FOUND_ERROR);\n        // 仅本人或管理员可编辑\n        if (!oldQuestion.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {\n            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);\n        }\n        // 操作数据库\n        boolean result = questionService.updateById(question);\n        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);\n        return ResultUtils.success(true);\n    }\n\n    // endregion\n\n    // region AI 生成题目功能\n    private static final String GENERATE_QUESTION_SYSTEM_MESSAGE = \"你是一位严谨的出题专家，我会给你如下信息：\\n\" +\n            \"```\\n\" +\n            \"应用名称，\\n\" +\n            \"【【【应用描述】】】，\\n\" +\n            \"应用类别，\\n\" +\n            \"要生成的题目数，\\n\" +\n            \"每个题目的选项数\\n\" +\n            \"```\\n\" +\n            \"\\n\" +\n            \"请你根据上述信息，按照以下步骤来出题：\\n\" +\n            \"1. 要求：题目和选项尽可能地短，题目不要包含序号，每题的选项数以我提供的为主，题目不能重复\\n\" +\n            \"2. 严格按照下面的 json 格式输出题目和选项\\n\" +\n            \"```\\n\" +\n            \"[{\\\"options\\\":[{\\\"value\\\":\\\"选项内容\\\",\\\"key\\\":\\\"A\\\"},{\\\"value\\\":\\\"\\\",\\\"key\\\":\\\"B\\\"}],\\\"title\\\":\\\"题目标题\\\"}]\\n\" +\n            \"```\\n\" +\n            \"title 是题目，options 是选项，每个选项的 key 按照英文字母序（比如 A、B、C、D）以此类推，value 是选项内容\\n\" +\n            \"3. 检查题目是否包含序号，若包含序号则去除序号\\n\" +\n            \"4. 返回的题目列表格式必须为 JSON 数组\";\n\n    /**\n     * 生成题目的用户消息\n     *\n     * @param app\n     * @param questionNumber\n     * @param optionNumber\n     * @return\n     */\n    private String getGenerateQuestionUserMessage(App app, int questionNumber, int optionNumber) {\n        StringBuilder userMessage = new StringBuilder();\n        userMessage.append(app.getAppName()).append(\"\\n\");\n        userMessage.append(app.getAppDesc()).append(\"\\n\");\n        userMessage.append(AppTypeEnum.getEnumByValue(app.getAppType()).getText() + \"类\").append(\"\\n\");\n        userMessage.append(questionNumber).append(\"\\n\");\n        userMessage.append(optionNumber);\n        return userMessage.toString();\n    }\n\n    @PostMapping(\"/ai_generate\")\n    public BaseResponse<List<QuestionContentDTO>> aiGenerateQuestion(\n            @RequestBody AiGenerateQuestionRequest aiGenerateQuestionRequest) {\n        ThrowUtils.throwIf(aiGenerateQuestionRequest == null, ErrorCode.PARAMS_ERROR);\n        // 获取参数\n        Long appId = aiGenerateQuestionRequest.getAppId();\n        int questionNumber = aiGenerateQuestionRequest.getQuestionNumber();\n        int optionNumber = aiGenerateQuestionRequest.getOptionNumber();\n        // 获取应用信息\n        App app = appService.getById(appId);\n        ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR);\n        // 封装 Prompt\n        String userMessage = getGenerateQuestionUserMessage(app, questionNumber, optionNumber);\n        // AI 生成\n        String result = aiManager.doSyncRequest(GENERATE_QUESTION_SYSTEM_MESSAGE, userMessage, null);\n        // 截取需要的 JSON 信息\n        int start = result.indexOf(\"[\");\n        int end = result.lastIndexOf(\"]\");\n        String json = result.substring(start, end + 1);\n        List<QuestionContentDTO> questionContentDTOList = JSONUtil.toList(json, QuestionContentDTO.class);\n        return ResultUtils.success(questionContentDTOList);\n    }\n\n    @GetMapping(\"/ai_generate/sse\")\n    public SseEmitter aiGenerateQuestionSSE(AiGenerateQuestionRequest aiGenerateQuestionRequest, HttpServletRequest request) {\n        ThrowUtils.throwIf(aiGenerateQuestionRequest == null, ErrorCode.PARAMS_ERROR);\n        // 获取参数\n        Long appId = aiGenerateQuestionRequest.getAppId();\n        int questionNumber = aiGenerateQuestionRequest.getQuestionNumber();\n        int optionNumber = aiGenerateQuestionRequest.getOptionNumber();\n        // 获取应用信息\n        App app = appService.getById(appId);\n        ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR);\n        // 封装 Prompt\n        String userMessage = getGenerateQuestionUserMessage(app, questionNumber, optionNumber);\n        // 建立 SSE 连接对象，0 表示永不超时\n        SseEmitter sseEmitter = new SseEmitter(0L);\n        // AI 生成，SSE 流式返回\n        Flowable<ModelData> modelDataFlowable = aiManager.doStreamRequest(GENERATE_QUESTION_SYSTEM_MESSAGE, userMessage, null);\n        // 左括号计数器，除了默认值外，当回归为 0 时，表示左括号等于右括号，可以截取\n        AtomicInteger counter = new AtomicInteger(0);\n        // 拼接完整题目\n        StringBuilder stringBuilder = new StringBuilder();\n\n        // 获取登录用户\n        User loginUser = userService.getLoginUser(request);\n        // 默认全局线程池\n        Scheduler scheduler = Schedulers.io();\n        if (\"vip\".equals(loginUser.getUserRole())) {\n            scheduler = vipScheduler;\n        }\n        modelDataFlowable\n                .observeOn(scheduler)\n                .map(modelData -> modelData.getChoices().get(0).getDelta().getContent())\n                .map(message -> message.replaceAll(\"\\\\s\", \"\"))\n                .filter(StrUtil::isNotBlank)\n                .flatMap(message -> {\n                    List<Character> characterList = new ArrayList<>();\n                    for (char c : message.toCharArray()) {\n                        characterList.add(c);\n                    }\n                    return Flowable.fromIterable(characterList);\n                })\n                .doOnNext(c -> {\n                    // 如果是 '{'，计数器 + 1\n                    if (c == '{') {\n                        counter.addAndGet(1);\n                    }\n                    if (counter.get() > 0) {\n                        stringBuilder.append(c);\n                    }\n                    if (c == '}') {\n                        counter.addAndGet(-1);\n                        if (counter.get() == 0) {\n                            // 可以拼接题目，并且通过 SSE 返回给前端\n                            sseEmitter.send(JSONUtil.toJsonStr(stringBuilder.toString()));\n                            // 重置，准备拼接下一道题\n                            stringBuilder.setLength(0);\n                        }\n                    }\n                })\n                .doOnError((e) -> log.error(\"sse error\", e))\n                .doOnComplete(sseEmitter::complete)\n                .subscribe();\n        return sseEmitter;\n    }\n\n    // 仅测试隔离线程池使用\n    @Deprecated\n    @GetMapping(\"/ai_generate/sse/test\")\n    public SseEmitter aiGenerateQuestionSSETest(AiGenerateQuestionRequest aiGenerateQuestionRequest,\n                                                boolean isVip) {\n        ThrowUtils.throwIf(aiGenerateQuestionRequest == null, ErrorCode.PARAMS_ERROR);\n        // 获取参数\n        Long appId = aiGenerateQuestionRequest.getAppId();\n        int questionNumber = aiGenerateQuestionRequest.getQuestionNumber();\n        int optionNumber = aiGenerateQuestionRequest.getOptionNumber();\n        // 获取应用信息\n        App app = appService.getById(appId);\n        ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR);\n        // 封装 Prompt\n        String userMessage = getGenerateQuestionUserMessage(app, questionNumber, optionNumber);\n        // 建立 SSE 连接对象，0 表示永不超时\n        SseEmitter sseEmitter = new SseEmitter(0L);\n        // AI 生成，SSE 流式返回\n        Flowable<ModelData> modelDataFlowable = aiManager.doStreamRequest(GENERATE_QUESTION_SYSTEM_MESSAGE, userMessage, null);\n        // 左括号计数器，除了默认值外，当回归为 0 时，表示左括号等于右括号，可以截取\n        AtomicInteger counter = new AtomicInteger(0);\n        // 拼接完整题目\n        StringBuilder stringBuilder = new StringBuilder();\n        // 默认全局线程池\n        Scheduler scheduler = Schedulers.single();\n        if (isVip) {\n            scheduler = vipScheduler;\n        }\n        modelDataFlowable\n                .observeOn(scheduler)\n                .map(modelData -> modelData.getChoices().get(0).getDelta().getContent())\n                .map(message -> message.replaceAll(\"\\\\s\", \"\"))\n                .filter(StrUtil::isNotBlank)\n                .flatMap(message -> {\n                    List<Character> characterList = new ArrayList<>();\n                    for (char c : message.toCharArray()) {\n                        characterList.add(c);\n                    }\n                    return Flowable.fromIterable(characterList);\n                })\n                .doOnNext(c -> {\n                    // 如果是 '{'，计数器 + 1\n                    if (c == '{') {\n                        counter.addAndGet(1);\n                    }\n                    if (counter.get() > 0) {\n                        stringBuilder.append(c);\n                    }\n                    if (c == '}') {\n                        counter.addAndGet(-1);\n                        if (counter.get() == 0) {\n                            // 输出当前线程的名称\n                            System.out.println(Thread.currentThread().getName());\n                            // 模拟普通用户阻塞\n                            if (!isVip) {\n                                Thread.sleep(10000L);\n                            }\n                            // 可以拼接题目，并且通过 SSE 返回给前端\n                            sseEmitter.send(JSONUtil.toJsonStr(stringBuilder.toString()));\n                            // 重置，准备拼接下一道题\n                            stringBuilder.setLength(0);\n                        }\n                    }\n                })\n                .doOnError((e) -> log.error(\"sse error\", e))\n                .doOnComplete(sseEmitter::complete)\n                .subscribe();\n        return sseEmitter;\n    }\n\n    // endregion\n}\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/controller/ScoringResultController.java",
    "content": "package com.yupi.yudada.controller;\n\nimport cn.hutool.json.JSONUtil;\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.yupi.yudada.annotation.AuthCheck;\nimport com.yupi.yudada.common.BaseResponse;\nimport com.yupi.yudada.common.DeleteRequest;\nimport com.yupi.yudada.common.ErrorCode;\nimport com.yupi.yudada.common.ResultUtils;\nimport com.yupi.yudada.constant.UserConstant;\nimport com.yupi.yudada.exception.BusinessException;\nimport com.yupi.yudada.exception.ThrowUtils;\nimport com.yupi.yudada.model.dto.scoringResult.ScoringResultAddRequest;\nimport com.yupi.yudada.model.dto.scoringResult.ScoringResultEditRequest;\nimport com.yupi.yudada.model.dto.scoringResult.ScoringResultQueryRequest;\nimport com.yupi.yudada.model.dto.scoringResult.ScoringResultUpdateRequest;\nimport com.yupi.yudada.model.entity.ScoringResult;\nimport com.yupi.yudada.model.entity.User;\nimport com.yupi.yudada.model.vo.ScoringResultVO;\nimport com.yupi.yudada.service.ScoringResultService;\nimport com.yupi.yudada.service.UserService;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.web.bind.annotation.*;\n\nimport javax.annotation.Resource;\nimport javax.servlet.http.HttpServletRequest;\nimport java.util.List;\n\n/**\n * 评分结果接口\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@RestController\n@RequestMapping(\"/scoringResult\")\n@Slf4j\npublic class ScoringResultController {\n\n    @Resource\n    private ScoringResultService scoringResultService;\n\n    @Resource\n    private UserService userService;\n\n    // region 增删改查\n\n    /**\n     * 创建评分结果\n     *\n     * @param scoringResultAddRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/add\")\n    public BaseResponse<Long> addScoringResult(@RequestBody ScoringResultAddRequest scoringResultAddRequest, HttpServletRequest request) {\n        ThrowUtils.throwIf(scoringResultAddRequest == null, ErrorCode.PARAMS_ERROR);\n        // 在此处将实体类和 DTO 进行转换\n        ScoringResult scoringResult = new ScoringResult();\n        BeanUtils.copyProperties(scoringResultAddRequest, scoringResult);\n        List<String> resultProp = scoringResultAddRequest.getResultProp();\n        scoringResult.setResultProp(JSONUtil.toJsonStr(resultProp));\n        // 数据校验\n        scoringResultService.validScoringResult(scoringResult, true);\n        // 填充默认值\n        User loginUser = userService.getLoginUser(request);\n        scoringResult.setUserId(loginUser.getId());\n        // 写入数据库\n        boolean result = scoringResultService.save(scoringResult);\n        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);\n        // 返回新写入的数据 id\n        long newScoringResultId = scoringResult.getId();\n        return ResultUtils.success(newScoringResultId);\n    }\n\n    /**\n     * 删除评分结果\n     *\n     * @param deleteRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/delete\")\n    public BaseResponse<Boolean> deleteScoringResult(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {\n        if (deleteRequest == null || deleteRequest.getId() <= 0) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        User user = userService.getLoginUser(request);\n        long id = deleteRequest.getId();\n        // 判断是否存在\n        ScoringResult oldScoringResult = scoringResultService.getById(id);\n        ThrowUtils.throwIf(oldScoringResult == null, ErrorCode.NOT_FOUND_ERROR);\n        // 仅本人或管理员可删除\n        if (!oldScoringResult.getUserId().equals(user.getId()) && !userService.isAdmin(request)) {\n            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);\n        }\n        // 操作数据库\n        boolean result = scoringResultService.removeById(id);\n        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);\n        return ResultUtils.success(true);\n    }\n\n    /**\n     * 更新评分结果（仅管理员可用）\n     *\n     * @param scoringResultUpdateRequest\n     * @return\n     */\n    @PostMapping(\"/update\")\n    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)\n    public BaseResponse<Boolean> updateScoringResult(@RequestBody ScoringResultUpdateRequest scoringResultUpdateRequest) {\n        if (scoringResultUpdateRequest == null || scoringResultUpdateRequest.getId() <= 0) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        // 在此处将实体类和 DTO 进行转换\n        ScoringResult scoringResult = new ScoringResult();\n        BeanUtils.copyProperties(scoringResultUpdateRequest, scoringResult);\n        List<String> resultProp = scoringResultUpdateRequest.getResultProp();\n        scoringResult.setResultProp(JSONUtil.toJsonStr(resultProp));\n        // 数据校验\n        scoringResultService.validScoringResult(scoringResult, false);\n        // 判断是否存在\n        long id = scoringResultUpdateRequest.getId();\n        ScoringResult oldScoringResult = scoringResultService.getById(id);\n        ThrowUtils.throwIf(oldScoringResult == null, ErrorCode.NOT_FOUND_ERROR);\n        // 操作数据库\n        boolean result = scoringResultService.updateById(scoringResult);\n        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);\n        return ResultUtils.success(true);\n    }\n\n    /**\n     * 根据 id 获取评分结果（封装类）\n     *\n     * @param id\n     * @return\n     */\n    @GetMapping(\"/get/vo\")\n    public BaseResponse<ScoringResultVO> getScoringResultVOById(long id, HttpServletRequest request) {\n        ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR);\n        // 查询数据库\n        ScoringResult scoringResult = scoringResultService.getById(id);\n        ThrowUtils.throwIf(scoringResult == null, ErrorCode.NOT_FOUND_ERROR);\n        // 获取封装类\n        return ResultUtils.success(scoringResultService.getScoringResultVO(scoringResult, request));\n    }\n\n    /**\n     * 分页获取评分结果列表（仅管理员可用）\n     *\n     * @param scoringResultQueryRequest\n     * @return\n     */\n    @PostMapping(\"/list/page\")\n    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)\n    public BaseResponse<Page<ScoringResult>> listScoringResultByPage(@RequestBody ScoringResultQueryRequest scoringResultQueryRequest) {\n        long current = scoringResultQueryRequest.getCurrent();\n        long size = scoringResultQueryRequest.getPageSize();\n        // 查询数据库\n        Page<ScoringResult> scoringResultPage = scoringResultService.page(new Page<>(current, size),\n                scoringResultService.getQueryWrapper(scoringResultQueryRequest));\n        return ResultUtils.success(scoringResultPage);\n    }\n\n    /**\n     * 分页获取评分结果列表（封装类）\n     *\n     * @param scoringResultQueryRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/list/page/vo\")\n    public BaseResponse<Page<ScoringResultVO>> listScoringResultVOByPage(@RequestBody ScoringResultQueryRequest scoringResultQueryRequest,\n                                                               HttpServletRequest request) {\n        long current = scoringResultQueryRequest.getCurrent();\n        long size = scoringResultQueryRequest.getPageSize();\n        // 限制爬虫\n        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);\n        // 查询数据库\n        Page<ScoringResult> scoringResultPage = scoringResultService.page(new Page<>(current, size),\n                scoringResultService.getQueryWrapper(scoringResultQueryRequest));\n        // 获取封装类\n        return ResultUtils.success(scoringResultService.getScoringResultVOPage(scoringResultPage, request));\n    }\n\n    /**\n     * 分页获取当前登录用户创建的评分结果列表\n     *\n     * @param scoringResultQueryRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/my/list/page/vo\")\n    public BaseResponse<Page<ScoringResultVO>> listMyScoringResultVOByPage(@RequestBody ScoringResultQueryRequest scoringResultQueryRequest,\n                                                                 HttpServletRequest request) {\n        ThrowUtils.throwIf(scoringResultQueryRequest == null, ErrorCode.PARAMS_ERROR);\n        // 补充查询条件，只查询当前登录用户的数据\n        User loginUser = userService.getLoginUser(request);\n        scoringResultQueryRequest.setUserId(loginUser.getId());\n        long current = scoringResultQueryRequest.getCurrent();\n        long size = scoringResultQueryRequest.getPageSize();\n        // 限制爬虫\n        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);\n        // 查询数据库\n        Page<ScoringResult> scoringResultPage = scoringResultService.page(new Page<>(current, size),\n                scoringResultService.getQueryWrapper(scoringResultQueryRequest));\n        // 获取封装类\n        return ResultUtils.success(scoringResultService.getScoringResultVOPage(scoringResultPage, request));\n    }\n\n    /**\n     * 编辑评分结果（给用户使用）\n     *\n     * @param scoringResultEditRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/edit\")\n    public BaseResponse<Boolean> editScoringResult(@RequestBody ScoringResultEditRequest scoringResultEditRequest, HttpServletRequest request) {\n        if (scoringResultEditRequest == null || scoringResultEditRequest.getId() <= 0) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        // 在此处将实体类和 DTO 进行转换\n        ScoringResult scoringResult = new ScoringResult();\n        BeanUtils.copyProperties(scoringResultEditRequest, scoringResult);\n        List<String> resultProp = scoringResultEditRequest.getResultProp();\n        scoringResult.setResultProp(JSONUtil.toJsonStr(resultProp));\n        // 数据校验\n        scoringResultService.validScoringResult(scoringResult, false);\n        User loginUser = userService.getLoginUser(request);\n        // 判断是否存在\n        long id = scoringResultEditRequest.getId();\n        ScoringResult oldScoringResult = scoringResultService.getById(id);\n        ThrowUtils.throwIf(oldScoringResult == null, ErrorCode.NOT_FOUND_ERROR);\n        // 仅本人或管理员可编辑\n        if (!oldScoringResult.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {\n            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);\n        }\n        // 操作数据库\n        boolean result = scoringResultService.updateById(scoringResult);\n        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);\n        return ResultUtils.success(true);\n    }\n\n    // endregion\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/controller/UserAnswerController.java",
    "content": "package com.yupi.yudada.controller;\n\nimport cn.hutool.core.util.IdUtil;\nimport cn.hutool.json.JSONUtil;\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.yupi.yudada.annotation.AuthCheck;\nimport com.yupi.yudada.common.BaseResponse;\nimport com.yupi.yudada.common.DeleteRequest;\nimport com.yupi.yudada.common.ErrorCode;\nimport com.yupi.yudada.common.ResultUtils;\nimport com.yupi.yudada.constant.UserConstant;\nimport com.yupi.yudada.exception.BusinessException;\nimport com.yupi.yudada.exception.ThrowUtils;\nimport com.yupi.yudada.model.dto.userAnswer.UserAnswerAddRequest;\nimport com.yupi.yudada.model.dto.userAnswer.UserAnswerEditRequest;\nimport com.yupi.yudada.model.dto.userAnswer.UserAnswerQueryRequest;\nimport com.yupi.yudada.model.dto.userAnswer.UserAnswerUpdateRequest;\nimport com.yupi.yudada.model.entity.App;\nimport com.yupi.yudada.model.entity.User;\nimport com.yupi.yudada.model.entity.UserAnswer;\nimport com.yupi.yudada.model.enums.ReviewStatusEnum;\nimport com.yupi.yudada.model.vo.UserAnswerVO;\nimport com.yupi.yudada.scoring.ScoringStrategyExecutor;\nimport com.yupi.yudada.service.AppService;\nimport com.yupi.yudada.service.UserAnswerService;\nimport com.yupi.yudada.service.UserService;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.dao.DuplicateKeyException;\nimport org.springframework.web.bind.annotation.*;\n\nimport javax.annotation.Resource;\nimport javax.servlet.http.HttpServletRequest;\nimport java.util.List;\n\n/**\n * 用户答案接口\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@RestController\n@RequestMapping(\"/userAnswer\")\n@Slf4j\npublic class UserAnswerController {\n\n    @Resource\n    private UserAnswerService userAnswerService;\n\n    @Resource\n    private AppService appService;\n\n    @Resource\n    private UserService userService;\n\n    @Resource\n    private ScoringStrategyExecutor scoringStrategyExecutor;\n\n    // region 增删改查\n\n    /**\n     * 创建用户答案\n     *\n     * @param userAnswerAddRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/add\")\n    public BaseResponse<Long> addUserAnswer(@RequestBody UserAnswerAddRequest userAnswerAddRequest, HttpServletRequest request) {\n        ThrowUtils.throwIf(userAnswerAddRequest == null, ErrorCode.PARAMS_ERROR);\n        // 在此处将实体类和 DTO 进行转换\n        UserAnswer userAnswer = new UserAnswer();\n        BeanUtils.copyProperties(userAnswerAddRequest, userAnswer);\n        List<String> choices = userAnswerAddRequest.getChoices();\n        userAnswer.setChoices(JSONUtil.toJsonStr(choices));\n        // 数据校验\n        userAnswerService.validUserAnswer(userAnswer, true);\n        // 判断 app 是否存在\n        Long appId = userAnswerAddRequest.getAppId();\n        App app = appService.getById(appId);\n        ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR);\n        if (!ReviewStatusEnum.PASS.equals(ReviewStatusEnum.getEnumByValue(app.getReviewStatus()))) {\n            throw new BusinessException(ErrorCode.NO_AUTH_ERROR, \"应用未通过审核，无法答题\");\n        }\n        // 填充默认值\n        User loginUser = userService.getLoginUser(request);\n        userAnswer.setUserId(loginUser.getId());\n        // 写入数据库\n        try {\n            boolean result = userAnswerService.save(userAnswer);\n            ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);\n        } catch (DuplicateKeyException e) {\n            // ignore error\n        }\n        // 返回新写入的数据 id\n        long newUserAnswerId = userAnswer.getId();\n        // 调用评分模块\n        try {\n            UserAnswer userAnswerWithResult = scoringStrategyExecutor.doScore(choices, app);\n            userAnswerWithResult.setId(newUserAnswerId);\n            userAnswerWithResult.setAppId(null);\n            userAnswerService.updateById(userAnswerWithResult);\n        } catch (Exception e) {\n            e.printStackTrace();\n            throw new BusinessException(ErrorCode.OPERATION_ERROR, \"评分错误\");\n        }\n        return ResultUtils.success(newUserAnswerId);\n    }\n\n    /**\n     * 删除用户答案\n     *\n     * @param deleteRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/delete\")\n    public BaseResponse<Boolean> deleteUserAnswer(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {\n        if (deleteRequest == null || deleteRequest.getId() <= 0) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        User user = userService.getLoginUser(request);\n        long id = deleteRequest.getId();\n        // 判断是否存在\n        UserAnswer oldUserAnswer = userAnswerService.getById(id);\n        ThrowUtils.throwIf(oldUserAnswer == null, ErrorCode.NOT_FOUND_ERROR);\n        // 仅本人或管理员可删除\n        if (!oldUserAnswer.getUserId().equals(user.getId()) && !userService.isAdmin(request)) {\n            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);\n        }\n        // 操作数据库\n        boolean result = userAnswerService.removeById(id);\n        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);\n        return ResultUtils.success(true);\n    }\n\n    /**\n     * 更新用户答案（仅管理员可用）\n     *\n     * @param userAnswerUpdateRequest\n     * @return\n     */\n    @PostMapping(\"/update\")\n    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)\n    public BaseResponse<Boolean> updateUserAnswer(@RequestBody UserAnswerUpdateRequest userAnswerUpdateRequest) {\n        if (userAnswerUpdateRequest == null || userAnswerUpdateRequest.getId() <= 0) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        // 在此处将实体类和 DTO 进行转换\n        UserAnswer userAnswer = new UserAnswer();\n        BeanUtils.copyProperties(userAnswerUpdateRequest, userAnswer);\n        List<String> choices = userAnswerUpdateRequest.getChoices();\n        userAnswer.setChoices(JSONUtil.toJsonStr(choices));\n        // 数据校验\n        userAnswerService.validUserAnswer(userAnswer, false);\n        // 判断是否存在\n        long id = userAnswerUpdateRequest.getId();\n        UserAnswer oldUserAnswer = userAnswerService.getById(id);\n        ThrowUtils.throwIf(oldUserAnswer == null, ErrorCode.NOT_FOUND_ERROR);\n        // 操作数据库\n        boolean result = userAnswerService.updateById(userAnswer);\n        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);\n        return ResultUtils.success(true);\n    }\n\n    /**\n     * 根据 id 获取用户答案（封装类）\n     *\n     * @param id\n     * @return\n     */\n    @GetMapping(\"/get/vo\")\n    public BaseResponse<UserAnswerVO> getUserAnswerVOById(long id, HttpServletRequest request) {\n        ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR);\n        // 查询数据库\n        UserAnswer userAnswer = userAnswerService.getById(id);\n        ThrowUtils.throwIf(userAnswer == null, ErrorCode.NOT_FOUND_ERROR);\n        // 获取封装类\n        return ResultUtils.success(userAnswerService.getUserAnswerVO(userAnswer, request));\n    }\n\n    /**\n     * 分页获取用户答案列表（仅管理员可用）\n     *\n     * @param userAnswerQueryRequest\n     * @return\n     */\n    @PostMapping(\"/list/page\")\n    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)\n    public BaseResponse<Page<UserAnswer>> listUserAnswerByPage(@RequestBody UserAnswerQueryRequest userAnswerQueryRequest) {\n        long current = userAnswerQueryRequest.getCurrent();\n        long size = userAnswerQueryRequest.getPageSize();\n        // 查询数据库\n        Page<UserAnswer> userAnswerPage = userAnswerService.page(new Page<>(current, size),\n                userAnswerService.getQueryWrapper(userAnswerQueryRequest));\n        return ResultUtils.success(userAnswerPage);\n    }\n\n    /**\n     * 分页获取用户答案列表（封装类）\n     *\n     * @param userAnswerQueryRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/list/page/vo\")\n    public BaseResponse<Page<UserAnswerVO>> listUserAnswerVOByPage(@RequestBody UserAnswerQueryRequest userAnswerQueryRequest,\n                                                                   HttpServletRequest request) {\n        long current = userAnswerQueryRequest.getCurrent();\n        long size = userAnswerQueryRequest.getPageSize();\n        // 限制爬虫\n        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);\n        // 查询数据库\n        Page<UserAnswer> userAnswerPage = userAnswerService.page(new Page<>(current, size),\n                userAnswerService.getQueryWrapper(userAnswerQueryRequest));\n        // 获取封装类\n        return ResultUtils.success(userAnswerService.getUserAnswerVOPage(userAnswerPage, request));\n    }\n\n    /**\n     * 分页获取当前登录用户创建的用户答案列表\n     *\n     * @param userAnswerQueryRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/my/list/page/vo\")\n    public BaseResponse<Page<UserAnswerVO>> listMyUserAnswerVOByPage(@RequestBody UserAnswerQueryRequest userAnswerQueryRequest,\n                                                                     HttpServletRequest request) {\n        ThrowUtils.throwIf(userAnswerQueryRequest == null, ErrorCode.PARAMS_ERROR);\n        // 补充查询条件，只查询当前登录用户的数据\n        User loginUser = userService.getLoginUser(request);\n        userAnswerQueryRequest.setUserId(loginUser.getId());\n        long current = userAnswerQueryRequest.getCurrent();\n        long size = userAnswerQueryRequest.getPageSize();\n        // 限制爬虫\n        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);\n        // 查询数据库\n        Page<UserAnswer> userAnswerPage = userAnswerService.page(new Page<>(current, size),\n                userAnswerService.getQueryWrapper(userAnswerQueryRequest));\n        // 获取封装类\n        return ResultUtils.success(userAnswerService.getUserAnswerVOPage(userAnswerPage, request));\n    }\n\n    /**\n     * 编辑用户答案（给用户使用）\n     *\n     * @param userAnswerEditRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/edit\")\n    public BaseResponse<Boolean> editUserAnswer(@RequestBody UserAnswerEditRequest userAnswerEditRequest, HttpServletRequest request) {\n        if (userAnswerEditRequest == null || userAnswerEditRequest.getId() <= 0) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        // 在此处将实体类和 DTO 进行转换\n        UserAnswer userAnswer = new UserAnswer();\n        BeanUtils.copyProperties(userAnswerEditRequest, userAnswer);\n        List<String> choices = userAnswerEditRequest.getChoices();\n        userAnswer.setChoices(JSONUtil.toJsonStr(choices));\n        // 数据校验\n        userAnswerService.validUserAnswer(userAnswer, false);\n        User loginUser = userService.getLoginUser(request);\n        // 判断是否存在\n        long id = userAnswerEditRequest.getId();\n        UserAnswer oldUserAnswer = userAnswerService.getById(id);\n        ThrowUtils.throwIf(oldUserAnswer == null, ErrorCode.NOT_FOUND_ERROR);\n        // 仅本人或管理员可编辑\n        if (!oldUserAnswer.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {\n            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);\n        }\n        // 操作数据库\n        boolean result = userAnswerService.updateById(userAnswer);\n        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);\n        return ResultUtils.success(true);\n    }\n\n    // endregion\n    @GetMapping(\"/generate/id\")\n    public BaseResponse<Long> generateUserAnswerId() {\n        return ResultUtils.success(IdUtil.getSnowflakeNextId());\n    }\n\n\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/controller/UserController.java",
    "content": "package com.yupi.yudada.controller;\n\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.yupi.yudada.annotation.AuthCheck;\nimport com.yupi.yudada.common.BaseResponse;\nimport com.yupi.yudada.common.DeleteRequest;\nimport com.yupi.yudada.common.ErrorCode;\nimport com.yupi.yudada.common.ResultUtils;\nimport com.yupi.yudada.constant.UserConstant;\nimport com.yupi.yudada.exception.BusinessException;\nimport com.yupi.yudada.exception.ThrowUtils;\nimport com.yupi.yudada.model.dto.user.UserAddRequest;\nimport com.yupi.yudada.model.dto.user.UserLoginRequest;\nimport com.yupi.yudada.model.dto.user.UserQueryRequest;\nimport com.yupi.yudada.model.dto.user.UserRegisterRequest;\nimport com.yupi.yudada.model.dto.user.UserUpdateMyRequest;\nimport com.yupi.yudada.model.dto.user.UserUpdateRequest;\nimport com.yupi.yudada.model.entity.User;\nimport com.yupi.yudada.model.vo.LoginUserVO;\nimport com.yupi.yudada.model.vo.UserVO;\nimport com.yupi.yudada.service.UserService;\n\nimport java.util.List;\nimport javax.annotation.Resource;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.util.DigestUtils;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport static com.yupi.yudada.service.impl.UserServiceImpl.SALT;\n\n/**\n * 用户接口\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@RestController\n@RequestMapping(\"/user\")\n@Slf4j\npublic class UserController {\n\n    @Resource\n    private UserService userService;\n\n    // region 登录相关\n\n    /**\n     * 用户注册\n     *\n     * @param userRegisterRequest\n     * @return\n     */\n    @PostMapping(\"/register\")\n    public BaseResponse<Long> userRegister(@RequestBody UserRegisterRequest userRegisterRequest) {\n        if (userRegisterRequest == null) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        String userAccount = userRegisterRequest.getUserAccount();\n        String userPassword = userRegisterRequest.getUserPassword();\n        String checkPassword = userRegisterRequest.getCheckPassword();\n        if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) {\n            return null;\n        }\n        long result = userService.userRegister(userAccount, userPassword, checkPassword);\n        return ResultUtils.success(result);\n    }\n\n    /**\n     * 用户登录\n     *\n     * @param userLoginRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/login\")\n    public BaseResponse<LoginUserVO> userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) {\n        if (userLoginRequest == null) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        String userAccount = userLoginRequest.getUserAccount();\n        String userPassword = userLoginRequest.getUserPassword();\n        if (StringUtils.isAnyBlank(userAccount, userPassword)) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        LoginUserVO loginUserVO = userService.userLogin(userAccount, userPassword, request);\n        return ResultUtils.success(loginUserVO);\n    }\n\n    /**\n     * 用户注销\n     *\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/logout\")\n    public BaseResponse<Boolean> userLogout(HttpServletRequest request) {\n        if (request == null) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        boolean result = userService.userLogout(request);\n        return ResultUtils.success(result);\n    }\n\n    /**\n     * 获取当前登录用户\n     *\n     * @param request\n     * @return\n     */\n    @GetMapping(\"/get/login\")\n    public BaseResponse<LoginUserVO> getLoginUser(HttpServletRequest request) {\n        User user = userService.getLoginUser(request);\n        return ResultUtils.success(userService.getLoginUserVO(user));\n    }\n\n    // endregion\n\n    // region 增删改查\n\n    /**\n     * 创建用户\n     *\n     * @param userAddRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/add\")\n    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)\n    public BaseResponse<Long> addUser(@RequestBody UserAddRequest userAddRequest, HttpServletRequest request) {\n        if (userAddRequest == null) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        User user = new User();\n        BeanUtils.copyProperties(userAddRequest, user);\n        // 默认密码 12345678\n        String defaultPassword = \"12345678\";\n        String encryptPassword = DigestUtils.md5DigestAsHex((SALT + defaultPassword).getBytes());\n        user.setUserPassword(encryptPassword);\n        boolean result = userService.save(user);\n        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);\n        return ResultUtils.success(user.getId());\n    }\n\n    /**\n     * 删除用户\n     *\n     * @param deleteRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/delete\")\n    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)\n    public BaseResponse<Boolean> deleteUser(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {\n        if (deleteRequest == null || deleteRequest.getId() <= 0) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        boolean b = userService.removeById(deleteRequest.getId());\n        return ResultUtils.success(b);\n    }\n\n    /**\n     * 更新用户\n     *\n     * @param userUpdateRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/update\")\n    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)\n    public BaseResponse<Boolean> updateUser(@RequestBody UserUpdateRequest userUpdateRequest,\n            HttpServletRequest request) {\n        if (userUpdateRequest == null || userUpdateRequest.getId() == null) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        User user = new User();\n        BeanUtils.copyProperties(userUpdateRequest, user);\n        boolean result = userService.updateById(user);\n        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);\n        return ResultUtils.success(true);\n    }\n\n    /**\n     * 根据 id 获取用户（仅管理员）\n     *\n     * @param id\n     * @param request\n     * @return\n     */\n    @GetMapping(\"/get\")\n    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)\n    public BaseResponse<User> getUserById(long id, HttpServletRequest request) {\n        if (id <= 0) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        User user = userService.getById(id);\n        ThrowUtils.throwIf(user == null, ErrorCode.NOT_FOUND_ERROR);\n        return ResultUtils.success(user);\n    }\n\n    /**\n     * 根据 id 获取包装类\n     *\n     * @param id\n     * @param request\n     * @return\n     */\n    @GetMapping(\"/get/vo\")\n    public BaseResponse<UserVO> getUserVOById(long id, HttpServletRequest request) {\n        BaseResponse<User> response = getUserById(id, request);\n        User user = response.getData();\n        return ResultUtils.success(userService.getUserVO(user));\n    }\n\n    /**\n     * 分页获取用户列表（仅管理员）\n     *\n     * @param userQueryRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/list/page\")\n    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)\n    public BaseResponse<Page<User>> listUserByPage(@RequestBody UserQueryRequest userQueryRequest,\n            HttpServletRequest request) {\n        long current = userQueryRequest.getCurrent();\n        long size = userQueryRequest.getPageSize();\n        Page<User> userPage = userService.page(new Page<>(current, size),\n                userService.getQueryWrapper(userQueryRequest));\n        return ResultUtils.success(userPage);\n    }\n\n    /**\n     * 分页获取用户封装列表\n     *\n     * @param userQueryRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/list/page/vo\")\n    public BaseResponse<Page<UserVO>> listUserVOByPage(@RequestBody UserQueryRequest userQueryRequest,\n            HttpServletRequest request) {\n        if (userQueryRequest == null) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        long current = userQueryRequest.getCurrent();\n        long size = userQueryRequest.getPageSize();\n        // 限制爬虫\n        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);\n        Page<User> userPage = userService.page(new Page<>(current, size),\n                userService.getQueryWrapper(userQueryRequest));\n        Page<UserVO> userVOPage = new Page<>(current, size, userPage.getTotal());\n        List<UserVO> userVO = userService.getUserVO(userPage.getRecords());\n        userVOPage.setRecords(userVO);\n        return ResultUtils.success(userVOPage);\n    }\n\n    // endregion\n\n    /**\n     * 更新个人信息\n     *\n     * @param userUpdateMyRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/update/my\")\n    public BaseResponse<Boolean> updateMyUser(@RequestBody UserUpdateMyRequest userUpdateMyRequest,\n            HttpServletRequest request) {\n        if (userUpdateMyRequest == null) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        User loginUser = userService.getLoginUser(request);\n        User user = new User();\n        BeanUtils.copyProperties(userUpdateMyRequest, user);\n        user.setId(loginUser.getId());\n        boolean result = userService.updateById(user);\n        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);\n        return ResultUtils.success(true);\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/exception/BusinessException.java",
    "content": "package com.yupi.yudada.exception;\n\nimport com.yupi.yudada.common.ErrorCode;\n\n/**\n * 自定义异常类\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\npublic class BusinessException extends RuntimeException {\n\n    /**\n     * 错误码\n     */\n    private final int code;\n\n    public BusinessException(int code, String message) {\n        super(message);\n        this.code = code;\n    }\n\n    public BusinessException(ErrorCode errorCode) {\n        super(errorCode.getMessage());\n        this.code = errorCode.getCode();\n    }\n\n    public BusinessException(ErrorCode errorCode, String message) {\n        super(message);\n        this.code = errorCode.getCode();\n    }\n\n    public int getCode() {\n        return code;\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/exception/GlobalExceptionHandler.java",
    "content": "package com.yupi.yudada.exception;\n\nimport com.yupi.yudada.common.BaseResponse;\nimport com.yupi.yudada.common.ErrorCode;\nimport com.yupi.yudada.common.ResultUtils;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\n/**\n * 全局异常处理器\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@RestControllerAdvice\n@Slf4j\npublic class GlobalExceptionHandler {\n\n    @ExceptionHandler(BusinessException.class)\n    public BaseResponse<?> businessExceptionHandler(BusinessException e) {\n        log.error(\"BusinessException\", e);\n        return ResultUtils.error(e.getCode(), e.getMessage());\n    }\n\n    @ExceptionHandler(RuntimeException.class)\n    public BaseResponse<?> runtimeExceptionHandler(RuntimeException e) {\n        log.error(\"RuntimeException\", e);\n        return ResultUtils.error(ErrorCode.SYSTEM_ERROR, \"系统错误\");\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/exception/ThrowUtils.java",
    "content": "package com.yupi.yudada.exception;\n\nimport com.yupi.yudada.common.ErrorCode;\n\n/**\n * 抛异常工具类\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\npublic class ThrowUtils {\n\n    /**\n     * 条件成立则抛异常\n     *\n     * @param condition\n     * @param runtimeException\n     */\n    public static void throwIf(boolean condition, RuntimeException runtimeException) {\n        if (condition) {\n            throw runtimeException;\n        }\n    }\n\n    /**\n     * 条件成立则抛异常\n     *\n     * @param condition\n     * @param errorCode\n     */\n    public static void throwIf(boolean condition, ErrorCode errorCode) {\n        throwIf(condition, new BusinessException(errorCode));\n    }\n\n    /**\n     * 条件成立则抛异常\n     *\n     * @param condition\n     * @param errorCode\n     * @param message\n     */\n    public static void throwIf(boolean condition, ErrorCode errorCode, String message) {\n        throwIf(condition, new BusinessException(errorCode, message));\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/generate/CodeGenerator.java",
    "content": "package com.yupi.yudada.generate;\n\nimport cn.hutool.core.io.FileUtil;\nimport freemarker.template.Configuration;\nimport freemarker.template.Template;\nimport freemarker.template.TemplateException;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.io.FileWriter;\nimport java.io.Writer;\n\n/**\n * 代码生成器\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\npublic class CodeGenerator {\n\n    /**\n     * 用法：修改生成参数和生成路径，注释掉不需要的生成逻辑，然后运行即可\n     *\n     * @param args\n     * @throws TemplateException\n     * @throws IOException\n     */\n    public static void main(String[] args) throws TemplateException, IOException {\n        // 指定生成参数\n        String packageName = \"com.yupi.yudada\";\n        String dataName = \"用户答案\";\n        String dataKey = \"userAnswer\";\n        String upperDataKey = \"UserAnswer\";\n\n        // 封装生成参数\n        Map<String, Object> dataModel = new HashMap<>();\n        dataModel.put(\"packageName\", packageName);\n        dataModel.put(\"dataName\", dataName);\n        dataModel.put(\"dataKey\", dataKey);\n        dataModel.put(\"upperDataKey\", upperDataKey);\n\n        // 生成路径默认值\n        String projectPath = System.getProperty(\"user.dir\");\n        // 参考路径，可以自己调整下面的 outputPath\n        String inputPath = projectPath + File.separator + \"src/main/resources/templates/模板名称.java.ftl\";\n        String outputPath = String.format(\"%s/generator/包名/%s类后缀.java\", projectPath, upperDataKey);\n\n        // 1、生成 Controller\n        // 指定生成路径\n        inputPath = projectPath + File.separator + \"src/main/resources/templates/TemplateController.java.ftl\";\n        outputPath = String.format(\"%s/generator/controller/%sController.java\", projectPath, upperDataKey);\n        // 生成\n        doGenerate(inputPath, outputPath, dataModel);\n        System.out.println(\"生成 Controller 成功，文件路径：\" + outputPath);\n\n        // 2、生成 Service 接口和实现类\n        // 生成 Service 接口\n        inputPath = projectPath + File.separator + \"src/main/resources/templates/TemplateService.java.ftl\";\n        outputPath = String.format(\"%s/generator/service/%sService.java\", projectPath, upperDataKey);\n        doGenerate(inputPath, outputPath, dataModel);\n        System.out.println(\"生成 Service 接口成功，文件路径：\" + outputPath);\n        // 生成 Service 实现类\n        inputPath = projectPath + File.separator + \"src/main/resources/templates/TemplateServiceImpl.java.ftl\";\n        outputPath = String.format(\"%s/generator/service/impl/%sServiceImpl.java\", projectPath, upperDataKey);\n        doGenerate(inputPath, outputPath, dataModel);\n        System.out.println(\"生成 Service 实现类成功，文件路径：\" + outputPath);\n\n        // 3、生成数据模型封装类（包括 DTO 和 VO）\n        // 生成 DTO\n        inputPath = projectPath + File.separator + \"src/main/resources/templates/model/TemplateAddRequest.java.ftl\";\n        outputPath = String.format(\"%s/generator/model/dto/%sAddRequest.java\", projectPath, upperDataKey);\n        doGenerate(inputPath, outputPath, dataModel);\n        inputPath = projectPath + File.separator + \"src/main/resources/templates/model/TemplateQueryRequest.java.ftl\";\n        outputPath = String.format(\"%s/generator/model/dto/%sQueryRequest.java\", projectPath, upperDataKey);\n        doGenerate(inputPath, outputPath, dataModel);\n        inputPath = projectPath + File.separator + \"src/main/resources/templates/model/TemplateEditRequest.java.ftl\";\n        outputPath = String.format(\"%s/generator/model/dto/%sEditRequest.java\", projectPath, upperDataKey);\n        doGenerate(inputPath, outputPath, dataModel);\n        inputPath = projectPath + File.separator + \"src/main/resources/templates/model/TemplateUpdateRequest.java.ftl\";\n        outputPath = String.format(\"%s/generator/model/dto/%sUpdateRequest.java\", projectPath, upperDataKey);\n        doGenerate(inputPath, outputPath, dataModel);\n        System.out.println(\"生成 DTO 成功，文件路径：\" + outputPath);\n        // 生成 VO\n        inputPath = projectPath + File.separator + \"src/main/resources/templates/model/TemplateVO.java.ftl\";\n        outputPath = String.format(\"%s/generator/model/vo/%sVO.java\", projectPath, upperDataKey);\n        doGenerate(inputPath, outputPath, dataModel);\n        System.out.println(\"生成 VO 成功，文件路径：\" + outputPath);\n    }\n\n    /**\n     * 生成文件\n     *\n     * @param inputPath  模板文件输入路径\n     * @param outputPath 输出路径\n     * @param model      数据模型\n     * @throws IOException\n     * @throws TemplateException\n     */\n    public static void doGenerate(String inputPath, String outputPath, Object model) throws IOException, TemplateException {\n        // new 出 Configuration 对象，参数为 FreeMarker 版本号\n        Configuration configuration = new Configuration(Configuration.VERSION_2_3_31);\n\n        // 指定模板文件所在的路径\n        File templateDir = new File(inputPath).getParentFile();\n        configuration.setDirectoryForTemplateLoading(templateDir);\n\n        // 设置模板文件使用的字符集\n        configuration.setDefaultEncoding(\"utf-8\");\n\n        // 创建模板对象，加载指定模板\n        String templateName = new File(inputPath).getName();\n        Template template = configuration.getTemplate(templateName);\n\n        // 文件不存在则创建文件和父目录\n        if (!FileUtil.exist(outputPath)) {\n            FileUtil.touch(outputPath);\n        }\n\n        // 生成\n        Writer out = new FileWriter(outputPath);\n        template.process(model, out);\n\n        // 生成文件后别忘了关闭哦\n        out.close();\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/manager/AiManager.java",
    "content": "package com.yupi.yudada.manager;\n\nimport com.yupi.yudada.common.ErrorCode;\nimport com.yupi.yudada.exception.BusinessException;\nimport com.zhipu.oapi.ClientV4;\nimport com.zhipu.oapi.Constants;\nimport com.zhipu.oapi.service.v4.model.*;\nimport io.reactivex.Flowable;\nimport org.springframework.stereotype.Component;\n\nimport javax.annotation.Resource;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 通用 AI 调用能力\n */\n@Component\npublic class AiManager {\n\n    @Resource\n    private ClientV4 clientV4;\n\n    // 稳定的随机数\n    private static final float STABLE_TEMPERATURE = 0.05f;\n\n    // 不稳定的随机数\n    private static final float UNSTABLE_TEMPERATURE = 0.99f;\n\n    /**\n     * 同步请求（答案不稳定）\n     *\n     * @param systemMessage\n     * @param userMessage\n     * @return\n     */\n    public String doSyncUnstableRequest(String systemMessage, String userMessage) {\n        return doRequest(systemMessage, userMessage, Boolean.FALSE, UNSTABLE_TEMPERATURE);\n    }\n\n    /**\n     * 同步请求（答案较稳定）\n     *\n     * @param systemMessage\n     * @param userMessage\n     * @return\n     */\n    public String doSyncStableRequest(String systemMessage, String userMessage) {\n        return doRequest(systemMessage, userMessage, Boolean.FALSE, STABLE_TEMPERATURE);\n    }\n\n    /**\n     * 同步请求\n     *\n     * @param systemMessage\n     * @param userMessage\n     * @param temperature\n     * @return\n     */\n    public String doSyncRequest(String systemMessage, String userMessage, Float temperature) {\n        return doRequest(systemMessage, userMessage, Boolean.FALSE, temperature);\n    }\n\n    /**\n     * 通用请求（简化消息传递）\n     *\n     * @param systemMessage\n     * @param userMessage\n     * @param stream\n     * @param temperature\n     * @return\n     */\n    public String doRequest(String systemMessage, String userMessage, Boolean stream, Float temperature) {\n        List<ChatMessage> chatMessageList = new ArrayList<>();\n        ChatMessage systemChatMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), systemMessage);\n        chatMessageList.add(systemChatMessage);\n        ChatMessage userChatMessage = new ChatMessage(ChatMessageRole.USER.value(), userMessage);\n        chatMessageList.add(userChatMessage);\n        return doRequest(chatMessageList, stream, temperature);\n    }\n\n    /**\n     * 通用请求\n     *\n     * @param messages\n     * @param stream\n     * @param temperature\n     * @return\n     */\n    public String doRequest(List<ChatMessage> messages, Boolean stream, Float temperature) {\n        // 构建请求\n        ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()\n                .model(Constants.ModelChatGLM4)\n                .stream(stream)\n                .temperature(temperature)\n                .invokeMethod(Constants.invokeMethod)\n                .messages(messages)\n                .build();\n        try {\n            ModelApiResponse invokeModelApiResp = clientV4.invokeModelApi(chatCompletionRequest);\n            return invokeModelApiResp.getData().getChoices().get(0).toString();\n        } catch (Exception e) {\n            e.printStackTrace();\n            throw new BusinessException(ErrorCode.SYSTEM_ERROR, e.getMessage());\n        }\n    }\n\n    /**\n     * 通用流式请求（简化消息传递）\n     *\n     * @param systemMessage\n     * @param userMessage\n     * @param temperature\n     * @return\n     */\n    public Flowable<ModelData> doStreamRequest(String systemMessage, String userMessage, Float temperature) {\n        List<ChatMessage> chatMessageList = new ArrayList<>();\n        ChatMessage systemChatMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), systemMessage);\n        chatMessageList.add(systemChatMessage);\n        ChatMessage userChatMessage = new ChatMessage(ChatMessageRole.USER.value(), userMessage);\n        chatMessageList.add(userChatMessage);\n        return doStreamRequest(chatMessageList, temperature);\n    }\n\n\n    /**\n     * 通用流式请求\n     *\n     * @param messages\n     * @param temperature\n     * @return\n     */\n    public Flowable<ModelData> doStreamRequest(List<ChatMessage> messages, Float temperature) {\n        // 构建请求\n        ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()\n                .model(Constants.ModelChatGLM4)\n                .stream(Boolean.TRUE)\n                .temperature(temperature)\n                .invokeMethod(Constants.invokeMethod)\n                .messages(messages)\n                .build();\n        try {\n            ModelApiResponse invokeModelApiResp = clientV4.invokeModelApi(chatCompletionRequest);\n            return invokeModelApiResp.getFlowable();\n        } catch (Exception e) {\n            e.printStackTrace();\n            throw new BusinessException(ErrorCode.SYSTEM_ERROR, e.getMessage());\n        }\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/manager/CosManager.java",
    "content": "package com.yupi.yudada.manager;\n\nimport com.qcloud.cos.COSClient;\nimport com.qcloud.cos.model.PutObjectRequest;\nimport com.qcloud.cos.model.PutObjectResult;\nimport com.yupi.yudada.config.CosClientConfig;\nimport java.io.File;\nimport javax.annotation.Resource;\nimport org.springframework.stereotype.Component;\n\n/**\n * Cos 对象存储操作\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Component\npublic class CosManager {\n\n    @Resource\n    private CosClientConfig cosClientConfig;\n\n    @Resource\n    private COSClient cosClient;\n\n    /**\n     * 上传对象\n     *\n     * @param key 唯一键\n     * @param localFilePath 本地文件路径\n     * @return\n     */\n    public PutObjectResult putObject(String key, String localFilePath) {\n        PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key,\n                new File(localFilePath));\n        return cosClient.putObject(putObjectRequest);\n    }\n\n    /**\n     * 上传对象\n     *\n     * @param key 唯一键\n     * @param file 文件\n     * @return\n     */\n    public PutObjectResult putObject(String key, File file) {\n        PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key,\n                file);\n        return cosClient.putObject(putObjectRequest);\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/mapper/AppMapper.java",
    "content": "package com.yupi.yudada.mapper;\n\nimport com.yupi.yudada.model.entity.App;\nimport com.baomidou.mybatisplus.core.mapper.BaseMapper;\n\n/**\n* @author 李鱼皮\n* @description 针对表【app(应用)】的数据库操作Mapper\n* @createDate 2024-05-09 20:41:03\n* @Entity com.yupi.yudada.model.entity.App\n*/\npublic interface AppMapper extends BaseMapper<App> {\n\n}\n\n\n\n\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/mapper/PostFavourMapper.java",
    "content": "package com.yupi.yudada.mapper;\n\nimport com.baomidou.mybatisplus.core.conditions.Wrapper;\nimport com.baomidou.mybatisplus.core.mapper.BaseMapper;\nimport com.baomidou.mybatisplus.core.metadata.IPage;\nimport com.baomidou.mybatisplus.core.toolkit.Constants;\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.yupi.yudada.model.entity.Post;\nimport com.yupi.yudada.model.entity.PostFavour;\nimport org.apache.ibatis.annotations.Param;\n\n/**\n * 帖子收藏数据库操作\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\npublic interface PostFavourMapper extends BaseMapper<PostFavour> {\n\n    /**\n     * 分页查询收藏帖子列表\n     *\n     * @param page\n     * @param queryWrapper\n     * @param favourUserId\n     * @return\n     */\n    Page<Post> listFavourPostByPage(IPage<Post> page, @Param(Constants.WRAPPER) Wrapper<Post> queryWrapper,\n            long favourUserId);\n\n}\n\n\n\n\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/mapper/PostMapper.java",
    "content": "package com.yupi.yudada.mapper;\n\nimport com.baomidou.mybatisplus.core.mapper.BaseMapper;\nimport com.yupi.yudada.model.entity.Post;\nimport java.util.Date;\nimport java.util.List;\n\n/**\n * 帖子数据库操作\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\npublic interface PostMapper extends BaseMapper<Post> {\n\n    /**\n     * 查询帖子列表（包括已被删除的数据）\n     */\n    List<Post> listPostWithDelete(Date minUpdateTime);\n\n}\n\n\n\n\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/mapper/PostThumbMapper.java",
    "content": "package com.yupi.yudada.mapper;\n\nimport com.yupi.yudada.model.entity.PostThumb;\nimport com.baomidou.mybatisplus.core.mapper.BaseMapper;\n\n/**\n * 帖子点赞数据库操作\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\npublic interface PostThumbMapper extends BaseMapper<PostThumb> {\n\n}\n\n\n\n\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/mapper/QuestionMapper.java",
    "content": "package com.yupi.yudada.mapper;\n\nimport com.yupi.yudada.model.entity.Question;\nimport com.baomidou.mybatisplus.core.mapper.BaseMapper;\n\n/**\n* @author 李鱼皮\n* @description 针对表【question(题目)】的数据库操作Mapper\n* @createDate 2024-05-09 20:41:03\n* @Entity com.yupi.yudada.model.entity.Question\n*/\npublic interface QuestionMapper extends BaseMapper<Question> {\n\n}\n\n\n\n\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/mapper/ScoringResultMapper.java",
    "content": "package com.yupi.yudada.mapper;\n\nimport com.yupi.yudada.model.entity.ScoringResult;\nimport com.baomidou.mybatisplus.core.mapper.BaseMapper;\n\n/**\n* @author 李鱼皮\n* @description 针对表【scoring_result(评分结果)】的数据库操作Mapper\n* @createDate 2024-05-09 20:41:03\n* @Entity com.yupi.yudada.model.entity.ScoringResult\n*/\npublic interface ScoringResultMapper extends BaseMapper<ScoringResult> {\n\n}\n\n\n\n\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/mapper/UserAnswerMapper.java",
    "content": "package com.yupi.yudada.mapper;\n\nimport com.yupi.yudada.model.dto.statistic.AppAnswerCountDTO;\nimport com.yupi.yudada.model.dto.statistic.AppAnswerResultCountDTO;\nimport com.yupi.yudada.model.entity.UserAnswer;\nimport com.baomidou.mybatisplus.core.mapper.BaseMapper;\nimport org.apache.ibatis.annotations.Select;\n\nimport java.util.List;\n\n/**\n* @author 李鱼皮\n* @description 针对表【user_answer(用户答题记录)】的数据库操作Mapper\n* @createDate 2024-05-09 20:41:03\n* @Entity com.yupi.yudada.model.entity.UserAnswer\n*/\npublic interface UserAnswerMapper extends BaseMapper<UserAnswer> {\n\n    @Select(\"select appId, count(userId) as answerCount from user_answer\\n\" +\n            \"    group by appId order by answerCount desc limit 10;\")\n    List<AppAnswerCountDTO> doAppAnswerCount();\n\n\n    @Select(\"select resultName, count(resultName) as resultCount from user_answer\\n\" +\n            \"    where appId = #{appId}\\n\" +\n            \"    group by resultName order by resultCount desc;\")\n    List<AppAnswerResultCountDTO> doAppAnswerResultCount(Long appId);\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/mapper/UserMapper.java",
    "content": "package com.yupi.yudada.mapper;\n\nimport com.baomidou.mybatisplus.core.mapper.BaseMapper;\nimport com.yupi.yudada.model.entity.User;\n\n/**\n * 用户数据库操作\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\npublic interface UserMapper extends BaseMapper<User> {\n\n}\n\n\n\n\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/app/AppAddRequest.java",
    "content": "package com.yupi.yudada.model.dto.app;\n\nimport lombok.Data;\n\nimport java.io.Serializable;\n\n/**\n * 创建应用请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Data\npublic class AppAddRequest implements Serializable {\n\n    /**\n     * 应用名\n     */\n    private String appName;\n\n    /**\n     * 应用描述\n     */\n    private String appDesc;\n\n    /**\n     * 应用图标\n     */\n    private String appIcon;\n\n    /**\n     * 应用类型（0-得分类，1-测评类）\n     */\n    private Integer appType;\n\n    /**\n     * 评分策略（0-自定义，1-AI）\n     */\n    private Integer scoringStrategy;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/app/AppEditRequest.java",
    "content": "package com.yupi.yudada.model.dto.app;\n\nimport lombok.Data;\n\nimport java.io.Serializable;\n\n/**\n * 编辑应用请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Data\npublic class AppEditRequest implements Serializable {\n    /**\n     * id\n     */\n    private Long id;\n\n    /**\n     * 应用名\n     */\n    private String appName;\n\n    /**\n     * 应用描述\n     */\n    private String appDesc;\n\n    /**\n     * 应用图标\n     */\n    private String appIcon;\n\n    /**\n     * 应用类型（0-得分类，1-测评类）\n     */\n    private Integer appType;\n\n    /**\n     * 评分策略（0-自定义，1-AI）\n     */\n    private Integer scoringStrategy;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/app/AppQueryRequest.java",
    "content": "package com.yupi.yudada.model.dto.app;\n\nimport com.yupi.yudada.common.PageRequest;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\n\nimport java.io.Serializable;\n\n/**\n * 查询应用请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@EqualsAndHashCode(callSuper = true)\n@Data\npublic class AppQueryRequest extends PageRequest implements Serializable {\n\n    /**\n     * id\n     */\n    private Long id;\n\n    /**\n     * 应用名\n     */\n    private String appName;\n\n    /**\n     * 应用描述\n     */\n    private String appDesc;\n\n    /**\n     * 应用图标\n     */\n    private String appIcon;\n\n    /**\n     * 应用类型（0-得分类，1-测评类）\n     */\n    private Integer appType;\n\n    /**\n     * 评分策略（0-自定义，1-AI）\n     */\n    private Integer scoringStrategy;\n\n    /**\n     * 审核状态：0-待审核, 1-通过, 2-拒绝\n     */\n    private Integer reviewStatus;\n\n    /**\n     * 审核信息\n     */\n    private String reviewMessage;\n\n    /**\n     * 审核人 id\n     */\n    private Long reviewerId;\n\n    /**\n     * 创建用户 id\n     */\n    private Long userId;\n\n    /**\n     * id\n     */\n    private Long notId;\n\n    /**\n     * 搜索词\n     */\n    private String searchText;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/app/AppUpdateRequest.java",
    "content": "package com.yupi.yudada.model.dto.app;\n\nimport lombok.Data;\n\nimport java.io.Serializable;\nimport java.util.Date;\n\n/**\n * 更新应用请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Data\npublic class AppUpdateRequest implements Serializable {\n\n    /**\n     * id\n     */\n    private Long id;\n\n    /**\n     * 应用名\n     */\n    private String appName;\n\n    /**\n     * 应用描述\n     */\n    private String appDesc;\n\n    /**\n     * 应用图标\n     */\n    private String appIcon;\n\n    /**\n     * 应用类型（0-得分类，1-测评类）\n     */\n    private Integer appType;\n\n    /**\n     * 评分策略（0-自定义，1-AI）\n     */\n    private Integer scoringStrategy;\n\n    /**\n     * 审核状态：0-待审核, 1-通过, 2-拒绝\n     */\n    private Integer reviewStatus;\n\n    /**\n     * 审核信息\n     */\n    private String reviewMessage;\n\n    /**\n     * 审核人 id\n     */\n    private Long reviewerId;\n\n    /**\n     * 审核时间\n     */\n    private Date reviewTime;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/file/UploadFileRequest.java",
    "content": "package com.yupi.yudada.model.dto.file;\n\nimport java.io.Serializable;\nimport lombok.Data;\n\n/**\n * 文件上传请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Data\npublic class UploadFileRequest implements Serializable {\n\n    /**\n     * 业务\n     */\n    private String biz;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/post/PostAddRequest.java",
    "content": "package com.yupi.yudada.model.dto.post;\n\nimport java.io.Serializable;\nimport java.util.List;\nimport lombok.Data;\n\n/**\n * 创建请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Data\npublic class PostAddRequest implements Serializable {\n\n    /**\n     * 标题\n     */\n    private String title;\n\n    /**\n     * 内容\n     */\n    private String content;\n\n    /**\n     * 标签列表\n     */\n    private List<String> tags;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/post/PostEditRequest.java",
    "content": "package com.yupi.yudada.model.dto.post;\n\nimport java.io.Serializable;\nimport java.util.List;\nimport lombok.Data;\n\n/**\n * 编辑请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Data\npublic class PostEditRequest implements Serializable {\n\n    /**\n     * id\n     */\n    private Long id;\n\n    /**\n     * 标题\n     */\n    private String title;\n\n    /**\n     * 内容\n     */\n    private String content;\n\n    /**\n     * 标签列表\n     */\n    private List<String> tags;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/post/PostQueryRequest.java",
    "content": "package com.yupi.yudada.model.dto.post;\n\nimport com.yupi.yudada.common.PageRequest;\nimport java.io.Serializable;\nimport java.util.List;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\n\n/**\n * 查询请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@EqualsAndHashCode(callSuper = true)\n@Data\npublic class PostQueryRequest extends PageRequest implements Serializable {\n\n    /**\n     * id\n     */\n    private Long id;\n\n    /**\n     * id\n     */\n    private Long notId;\n\n    /**\n     * 搜索词\n     */\n    private String searchText;\n\n    /**\n     * 标题\n     */\n    private String title;\n\n    /**\n     * 内容\n     */\n    private String content;\n\n    /**\n     * 标签列表\n     */\n    private List<String> tags;\n\n    /**\n     * 至少有一个标签\n     */\n    private List<String> orTags;\n\n    /**\n     * 创建用户 id\n     */\n    private Long userId;\n\n    /**\n     * 收藏用户 id\n     */\n    private Long favourUserId;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/post/PostUpdateRequest.java",
    "content": "package com.yupi.yudada.model.dto.post;\n\nimport java.io.Serializable;\nimport java.util.List;\nimport lombok.Data;\n\n/**\n * 更新请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Data\npublic class PostUpdateRequest implements Serializable {\n\n    /**\n     * id\n     */\n    private Long id;\n\n    /**\n     * 标题\n     */\n    private String title;\n\n    /**\n     * 内容\n     */\n    private String content;\n\n    /**\n     * 标签列表\n     */\n    private List<String> tags;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/postfavour/PostFavourAddRequest.java",
    "content": "package com.yupi.yudada.model.dto.postfavour;\n\nimport java.io.Serializable;\nimport lombok.Data;\n\n/**\n * 帖子收藏 / 取消收藏请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Data\npublic class PostFavourAddRequest implements Serializable {\n\n    /**\n     * 帖子 id\n     */\n    private Long postId;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/postfavour/PostFavourQueryRequest.java",
    "content": "package com.yupi.yudada.model.dto.postfavour;\n\nimport com.yupi.yudada.common.PageRequest;\nimport com.yupi.yudada.model.dto.post.PostQueryRequest;\nimport java.io.Serializable;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\n\n/**\n * 帖子收藏查询请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Data\n@EqualsAndHashCode(callSuper = true)\npublic class PostFavourQueryRequest extends PageRequest implements Serializable {\n\n    /**\n     * 帖子查询请求\n     */\n    private PostQueryRequest postQueryRequest;\n\n    /**\n     * 用户 id\n     */\n    private Long userId;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/postthumb/PostThumbAddRequest.java",
    "content": "package com.yupi.yudada.model.dto.postthumb;\n\nimport java.io.Serializable;\nimport lombok.Data;\n\n/**\n * 帖子点赞请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Data\npublic class PostThumbAddRequest implements Serializable {\n\n    /**\n     * 帖子 id\n     */\n    private Long postId;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/question/AiGenerateQuestionRequest.java",
    "content": "package com.yupi.yudada.model.dto.question;\n\nimport lombok.Data;\n\nimport java.io.Serializable;\n\n/**\n * AI 生成题目请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Data\npublic class AiGenerateQuestionRequest implements Serializable {\n\n    /**\n     * 应用 id\n     */\n    private Long appId;\n\n    /**\n     * 题目数\n     */\n    int questionNumber = 10;\n\n    /**\n     * 选项数\n     */\n    int optionNumber = 2;\n\n    private static final long serialVersionUID = 1L;\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/question/QuestionAddRequest.java",
    "content": "package com.yupi.yudada.model.dto.question;\n\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableId;\nimport lombok.Data;\n\nimport java.io.Serializable;\nimport java.util.Date;\nimport java.util.List;\n\n/**\n * 创建题目请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Data\npublic class QuestionAddRequest implements Serializable {\n\n    /**\n     * 题目内容（json格式）\n     */\n    private List<QuestionContentDTO> questionContent;\n\n    /**\n     * 应用 id\n     */\n    private Long appId;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/question/QuestionAnswerDTO.java",
    "content": "package com.yupi.yudada.model.dto.question;\n\nimport lombok.Data;\n\n/**\n * 题目答案封装类（用于 AI 评分）\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Data\npublic class QuestionAnswerDTO {\n\n    /**\n     * 题目\n     */\n    private String title;\n\n    /**\n     * 用户答案\n     */\n    private String userAnswer;\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/question/QuestionContentDTO.java",
    "content": "package com.yupi.yudada.model.dto.question;\n\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\nimport lombok.AllArgsConstructor;\n\nimport java.util.List;\n\n@Data\n@NoArgsConstructor\n@AllArgsConstructor\npublic class QuestionContentDTO {\n\n    /**\n     * 题目标题\n     */\n    private String title;\n\n    /**\n     * 题目选项列表\n     */\n    private List<Option> options;\n\n    /**\n     * 题目选项\n     */\n    @Data\n    @NoArgsConstructor\n    @AllArgsConstructor\n    public static class Option {\n        private String result;\n        private int score;\n        private String value;\n        private String key;\n    }\n}\n\n\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/question/QuestionEditRequest.java",
    "content": "package com.yupi.yudada.model.dto.question;\n\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableId;\nimport lombok.Data;\n\nimport java.io.Serializable;\nimport java.util.Date;\nimport java.util.List;\n\n/**\n * 编辑题目请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Data\npublic class QuestionEditRequest implements Serializable {\n\n    /**\n     * id\n     */\n    private Long id;\n\n    /**\n     * 题目内容（json格式）\n     */\n    private List<QuestionContentDTO> questionContent;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/question/QuestionQueryRequest.java",
    "content": "package com.yupi.yudada.model.dto.question;\n\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableId;\nimport com.yupi.yudada.common.PageRequest;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\n\nimport java.io.Serializable;\nimport java.util.Date;\nimport java.util.List;\n\n/**\n * 查询题目请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@EqualsAndHashCode(callSuper = true)\n@Data\npublic class QuestionQueryRequest extends PageRequest implements Serializable {\n\n    /**\n     * id\n     */\n    private Long id;\n\n    /**\n     * 题目内容（json格式）\n     */\n    private String questionContent;\n\n    /**\n     * 应用 id\n     */\n    private Long appId;\n\n    /**\n     * 创建用户 id\n     */\n    private Long userId;\n\n    /**\n     * id\n     */\n    private Long notId;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/question/QuestionUpdateRequest.java",
    "content": "package com.yupi.yudada.model.dto.question;\n\nimport lombok.Data;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n/**\n * 更新题目请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Data\npublic class QuestionUpdateRequest implements Serializable {\n\n    /**\n     * id\n     */\n    private Long id;\n\n    /**\n     * 题目内容（json格式）\n     */\n    private List<QuestionContentDTO> questionContent;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/scoringResult/ScoringResultAddRequest.java",
    "content": "package com.yupi.yudada.model.dto.scoringResult;\n\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableId;\nimport lombok.Data;\n\nimport java.io.Serializable;\nimport java.util.Date;\nimport java.util.List;\n\n/**\n * 创建评分结果请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Data\npublic class ScoringResultAddRequest implements Serializable {\n\n    /**\n     * 结果名称，如物流师\n     */\n    private String resultName;\n\n    /**\n     * 结果描述\n     */\n    private String resultDesc;\n\n    /**\n     * 结果图片\n     */\n    private String resultPicture;\n\n    /**\n     * 结果属性集合 JSON，如 [I,S,T,J]\n     */\n    private List<String> resultProp;\n\n    /**\n     * 结果得分范围，如 80，表示 80及以上的分数命中此结果\n     */\n    private Integer resultScoreRange;\n\n    /**\n     * 应用 id\n     */\n    private Long appId;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/scoringResult/ScoringResultEditRequest.java",
    "content": "package com.yupi.yudada.model.dto.scoringResult;\n\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableId;\nimport lombok.Data;\n\nimport java.io.Serializable;\nimport java.util.Date;\nimport java.util.List;\n\n/**\n * 编辑评分结果请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Data\npublic class ScoringResultEditRequest implements Serializable {\n\n    /**\n     * id\n     */\n    private Long id;\n\n    /**\n     * 结果名称，如物流师\n     */\n    private String resultName;\n\n    /**\n     * 结果描述\n     */\n    private String resultDesc;\n\n    /**\n     * 结果图片\n     */\n    private String resultPicture;\n\n    /**\n     * 结果属性集合 JSON，如 [I,S,T,J]\n     */\n    private List<String> resultProp;\n\n    /**\n     * 结果得分范围，如 80，表示 80及以上的分数命中此结果\n     */\n    private Integer resultScoreRange;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/scoringResult/ScoringResultQueryRequest.java",
    "content": "package com.yupi.yudada.model.dto.scoringResult;\n\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableId;\nimport com.yupi.yudada.common.PageRequest;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\n\nimport java.io.Serializable;\nimport java.util.Date;\nimport java.util.List;\n\n/**\n * 查询评分结果请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@EqualsAndHashCode(callSuper = true)\n@Data\npublic class ScoringResultQueryRequest extends PageRequest implements Serializable {\n\n    /**\n     * id\n     */\n    private Long id;\n\n    /**\n     * 结果名称，如物流师\n     */\n    private String resultName;\n\n    /**\n     * 结果描述\n     */\n    private String resultDesc;\n\n    /**\n     * 结果图片\n     */\n    private String resultPicture;\n\n    /**\n     * 结果属性集合 JSON，如 [I,S,T,J]\n     */\n    private String resultProp;\n\n    /**\n     * 结果得分范围，如 80，表示 80及以上的分数命中此结果\n     */\n    private Integer resultScoreRange;\n\n    /**\n     * 应用 id\n     */\n    private Long appId;\n\n    /**\n     * 创建用户 id\n     */\n    private Long userId;\n\n    /**\n     * id\n     */\n    private Long notId;\n\n    /**\n     * 搜索词\n     */\n    private String searchText;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/scoringResult/ScoringResultUpdateRequest.java",
    "content": "package com.yupi.yudada.model.dto.scoringResult;\n\nimport lombok.Data;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n/**\n * 更新评分结果请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Data\npublic class ScoringResultUpdateRequest implements Serializable {\n\n    /**\n     * id\n     */\n    private Long id;\n\n    /**\n     * 结果名称，如物流师\n     */\n    private String resultName;\n\n    /**\n     * 结果描述\n     */\n    private String resultDesc;\n\n    /**\n     * 结果图片\n     */\n    private String resultPicture;\n\n    /**\n     * 结果属性集合 JSON，如 [I,S,T,J]\n     */\n    private List<String> resultProp;\n\n    /**\n     * 结果得分范围，如 80，表示 80及以上的分数命中此结果\n     */\n    private Integer resultScoreRange;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/statistic/AppAnswerCountDTO.java",
    "content": "package com.yupi.yudada.model.dto.statistic;\n\nimport lombok.Data;\n\n/**\n * App 用户提交答案书统计\n */\n@Data\npublic class AppAnswerCountDTO {\n\n    private Long appId;\n\n    /**\n     * 用户提交答案数\n     */\n    private Long answerCount;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/statistic/AppAnswerResultCountDTO.java",
    "content": "package com.yupi.yudada.model.dto.statistic;\n\nimport lombok.Data;\n\n/**\n * App 答案结果统计\n */\n@Data\npublic class AppAnswerResultCountDTO {\n    // 结果名称\n    private String resultName;\n    // 对应个数\n    private String resultCount;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/user/UserAddRequest.java",
    "content": "package com.yupi.yudada.model.dto.user;\n\nimport java.io.Serializable;\nimport lombok.Data;\n\n/**\n * 用户创建请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Data\npublic class UserAddRequest implements Serializable {\n\n    /**\n     * 用户昵称\n     */\n    private String userName;\n\n    /**\n     * 账号\n     */\n    private String userAccount;\n\n    /**\n     * 用户头像\n     */\n    private String userAvatar;\n\n    /**\n     * 用户角色: user, admin\n     */\n    private String userRole;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/user/UserLoginRequest.java",
    "content": "package com.yupi.yudada.model.dto.user;\n\nimport java.io.Serializable;\nimport lombok.Data;\n\n/**\n * 用户登录请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Data\npublic class UserLoginRequest implements Serializable {\n\n    private static final long serialVersionUID = 3191241716373120793L;\n\n    private String userAccount;\n\n    private String userPassword;\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/user/UserQueryRequest.java",
    "content": "package com.yupi.yudada.model.dto.user;\n\nimport com.yupi.yudada.common.PageRequest;\nimport java.io.Serializable;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\n\n/**\n * 用户查询请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@EqualsAndHashCode(callSuper = true)\n@Data\npublic class UserQueryRequest extends PageRequest implements Serializable {\n    /**\n     * id\n     */\n    private Long id;\n\n    /**\n     * 开放平台id\n     */\n    private String unionId;\n\n    /**\n     * 公众号openId\n     */\n    private String mpOpenId;\n\n    /**\n     * 用户昵称\n     */\n    private String userName;\n\n    /**\n     * 简介\n     */\n    private String userProfile;\n\n    /**\n     * 用户角色：user/admin/ban\n     */\n    private String userRole;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/user/UserRegisterRequest.java",
    "content": "package com.yupi.yudada.model.dto.user;\n\nimport java.io.Serializable;\nimport lombok.Data;\n\n/**\n * 用户注册请求体\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Data\npublic class UserRegisterRequest implements Serializable {\n\n    private static final long serialVersionUID = 3191241716373120793L;\n\n    private String userAccount;\n\n    private String userPassword;\n\n    private String checkPassword;\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/user/UserUpdateMyRequest.java",
    "content": "package com.yupi.yudada.model.dto.user;\n\nimport java.io.Serializable;\nimport lombok.Data;\n\n/**\n * 用户更新个人信息请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Data\npublic class UserUpdateMyRequest implements Serializable {\n\n    /**\n     * 用户昵称\n     */\n    private String userName;\n\n    /**\n     * 用户头像\n     */\n    private String userAvatar;\n\n    /**\n     * 简介\n     */\n    private String userProfile;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/user/UserUpdateRequest.java",
    "content": "package com.yupi.yudada.model.dto.user;\n\nimport java.io.Serializable;\nimport lombok.Data;\n\n/**\n * 用户更新请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Data\npublic class UserUpdateRequest implements Serializable {\n    /**\n     * id\n     */\n    private Long id;\n\n    /**\n     * 用户昵称\n     */\n    private String userName;\n\n    /**\n     * 用户头像\n     */\n    private String userAvatar;\n\n    /**\n     * 简介\n     */\n    private String userProfile;\n\n    /**\n     * 用户角色：user/admin/ban\n     */\n    private String userRole;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/userAnswer/UserAnswerAddRequest.java",
    "content": "package com.yupi.yudada.model.dto.userAnswer;\n\nimport lombok.Data;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n/**\n * 创建用户答案请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Data\npublic class UserAnswerAddRequest implements Serializable {\n\n    /**\n     * id（用户答案 id，用于保证提交答案的幂等性）\n     */\n    private Long id;\n\n    /**\n     * 应用 id\n     */\n    private Long appId;\n\n    /**\n     * 用户答案（JSON 数组）\n     */\n    private List<String> choices;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/userAnswer/UserAnswerEditRequest.java",
    "content": "package com.yupi.yudada.model.dto.userAnswer;\n\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableId;\nimport lombok.Data;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n/**\n * 编辑用户答案请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Data\npublic class UserAnswerEditRequest implements Serializable {\n\n    /**\n     * id\n     */\n    private Long id;\n\n\n    /**\n     * 应用 id\n     */\n    private Long appId;\n\n    /**\n     * 用户答案（JSON 数组）\n     */\n    private List<String> choices;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/userAnswer/UserAnswerQueryRequest.java",
    "content": "package com.yupi.yudada.model.dto.userAnswer;\n\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableId;\nimport com.yupi.yudada.common.PageRequest;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n/**\n * 查询用户答案请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@EqualsAndHashCode(callSuper = true)\n@Data\npublic class UserAnswerQueryRequest extends PageRequest implements Serializable {\n\n    /**\n     *\n     */\n    private Long id;\n\n    /**\n     * 应用 id\n     */\n    private Long appId;\n\n    /**\n     * 应用类型（0-得分类，1-角色测评类）\n     */\n    private Integer appType;\n\n    /**\n     * 评分策略（0-自定义，1-AI）\n     */\n    private Integer scoringStrategy;\n\n    /**\n     * 用户答案（JSON 数组）\n     */\n    private String choices;\n\n    /**\n     * 评分结果 id\n     */\n    private Long resultId;\n\n    /**\n     * 结果名称，如物流师\n     */\n    private String resultName;\n\n    /**\n     * 结果描述\n     */\n    private String resultDesc;\n\n    /**\n     * 结果图标\n     */\n    private String resultPicture;\n\n    /**\n     * 得分\n     */\n    private Integer resultScore;\n\n    /**\n     * 用户 id\n     */\n    private Long userId;\n\n    /**\n     * id\n     */\n    private Long notId;\n\n    /**\n     * 搜索词\n     */\n    private String searchText;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/dto/userAnswer/UserAnswerUpdateRequest.java",
    "content": "package com.yupi.yudada.model.dto.userAnswer;\n\nimport lombok.Data;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n/**\n * 更新用户答案请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Data\npublic class UserAnswerUpdateRequest implements Serializable {\n\n    /**\n     * id\n     */\n    private Long id;\n\n    /**\n     * 应用 id\n     */\n    private Long appId;\n\n    /**\n     * 用户答案（JSON 数组）\n     */\n    private List<String> choices;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/entity/App.java",
    "content": "package com.yupi.yudada.model.entity;\n\nimport com.baomidou.mybatisplus.annotation.*;\n\nimport java.io.Serializable;\nimport java.util.Date;\nimport lombok.Data;\n\n/**\n * 应用\n * @TableName app\n */\n@TableName(value =\"app\")\n@Data\npublic class App implements Serializable {\n    /**\n     * id\n     */\n    @TableId(type = IdType.ASSIGN_ID)\n    private Long id;\n\n    /**\n     * 应用名\n     */\n    private String appName;\n\n    /**\n     * 应用描述\n     */\n    private String appDesc;\n\n    /**\n     * 应用图标\n     */\n    private String appIcon;\n\n    /**\n     * 应用类型（0-得分类，1-测评类）\n     */\n    private Integer appType;\n\n    /**\n     * 评分策略（0-自定义，1-AI）\n     */\n    private Integer scoringStrategy;\n\n    /**\n     * 审核状态：0-待审核, 1-通过, 2-拒绝\n     */\n    private Integer reviewStatus;\n\n    /**\n     * 审核信息\n     */\n    private String reviewMessage;\n\n    /**\n     * 审核人 id\n     */\n    private Long reviewerId;\n\n    /**\n     * 审核时间\n     */\n    private Date reviewTime;\n\n    /**\n     * 创建用户 id\n     */\n    private Long userId;\n\n    /**\n     * 创建时间\n     */\n    private Date createTime;\n\n    /**\n     * 更新时间\n     */\n    private Date updateTime;\n\n    /**\n     * 是否删除\n     */\n    @TableLogic\n    private Integer isDelete;\n\n    @TableField(exist = false)\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/entity/Post.java",
    "content": "package com.yupi.yudada.model.entity;\n\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableField;\nimport com.baomidou.mybatisplus.annotation.TableId;\nimport com.baomidou.mybatisplus.annotation.TableLogic;\nimport com.baomidou.mybatisplus.annotation.TableName;\nimport java.io.Serializable;\nimport java.util.Date;\nimport lombok.Data;\n\n/**\n * 帖子\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@TableName(value = \"post\")\n@Data\npublic class Post implements Serializable {\n\n    /**\n     * id\n     */\n    @TableId(type = IdType.ASSIGN_ID)\n    private Long id;\n\n    /**\n     * 标题\n     */\n    private String title;\n\n    /**\n     * 内容\n     */\n    private String content;\n\n    /**\n     * 标签列表 json\n     */\n    private String tags;\n\n    /**\n     * 点赞数\n     */\n    private Integer thumbNum;\n\n    /**\n     * 收藏数\n     */\n    private Integer favourNum;\n\n    /**\n     * 创建用户 id\n     */\n    private Long userId;\n\n    /**\n     * 创建时间\n     */\n    private Date createTime;\n\n    /**\n     * 更新时间\n     */\n    private Date updateTime;\n\n    /**\n     * 是否删除\n     */\n    @TableLogic\n    private Integer isDelete;\n\n    @TableField(exist = false)\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/entity/PostFavour.java",
    "content": "package com.yupi.yudada.model.entity;\n\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableField;\nimport com.baomidou.mybatisplus.annotation.TableId;\nimport com.baomidou.mybatisplus.annotation.TableName;\nimport java.io.Serializable;\nimport java.util.Date;\nimport lombok.Data;\n\n/**\n * 帖子收藏\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n **/\n@TableName(value = \"post_favour\")\n@Data\npublic class PostFavour implements Serializable {\n\n    /**\n     * id\n     */\n    @TableId(type = IdType.AUTO)\n    private Long id;\n\n    /**\n     * 帖子 id\n     */\n    private Long postId;\n\n    /**\n     * 创建用户 id\n     */\n    private Long userId;\n\n    /**\n     * 创建时间\n     */\n    private Date createTime;\n\n    /**\n     * 更新时间\n     */\n    private Date updateTime;\n\n    @TableField(exist = false)\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/entity/PostThumb.java",
    "content": "package com.yupi.yudada.model.entity;\n\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableField;\nimport com.baomidou.mybatisplus.annotation.TableId;\nimport com.baomidou.mybatisplus.annotation.TableName;\nimport java.io.Serializable;\nimport java.util.Date;\nimport lombok.Data;\n\n/**\n * 帖子点赞\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@TableName(value = \"post_thumb\")\n@Data\npublic class PostThumb implements Serializable {\n\n    /**\n     * id\n     */\n    @TableId(type = IdType.AUTO)\n    private Long id;\n\n    /**\n     * 帖子 id\n     */\n    private Long postId;\n\n    /**\n     * 创建用户 id\n     */\n    private Long userId;\n\n    /**\n     * 创建时间\n     */\n    private Date createTime;\n\n    /**\n     * 更新时间\n     */\n    private Date updateTime;\n\n    @TableField(exist = false)\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/entity/Question.java",
    "content": "package com.yupi.yudada.model.entity;\n\nimport com.baomidou.mybatisplus.annotation.*;\n\nimport java.io.Serializable;\nimport java.util.Date;\nimport lombok.Data;\n\n/**\n * 题目\n * @TableName question\n */\n@TableName(value =\"question\")\n@Data\npublic class Question implements Serializable {\n    /**\n     * id\n     */\n    @TableId(type = IdType.ASSIGN_ID)\n    private Long id;\n\n    /**\n     * 题目内容（json格式）\n     */\n    private String questionContent;\n\n    /**\n     * 应用 id\n     */\n    private Long appId;\n\n    /**\n     * 创建用户 id\n     */\n    private Long userId;\n\n    /**\n     * 创建时间\n     */\n    private Date createTime;\n\n    /**\n     * 更新时间\n     */\n    private Date updateTime;\n\n    /**\n     * 是否删除\n     */\n    @TableLogic\n    private Integer isDelete;\n\n    @TableField(exist = false)\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/entity/ScoringResult.java",
    "content": "package com.yupi.yudada.model.entity;\n\nimport com.baomidou.mybatisplus.annotation.*;\n\nimport java.io.Serializable;\nimport java.util.Date;\nimport lombok.Data;\n\n/**\n * 评分结果\n * @TableName scoring_result\n */\n@TableName(value =\"scoring_result\")\n@Data\npublic class ScoringResult implements Serializable {\n    /**\n     * id\n     */\n    @TableId(type = IdType.ASSIGN_ID)\n    private Long id;\n\n    /**\n     * 结果名称，如物流师\n     */\n    private String resultName;\n\n    /**\n     * 结果描述\n     */\n    private String resultDesc;\n\n    /**\n     * 结果图片\n     */\n    private String resultPicture;\n\n    /**\n     * 结果属性集合 JSON，如 [I,S,T,J]\n     */\n    private String resultProp;\n\n    /**\n     * 结果得分范围，如 80，表示 80及以上的分数命中此结果\n     */\n    private Integer resultScoreRange;\n\n    /**\n     * 应用 id\n     */\n    private Long appId;\n\n    /**\n     * 创建用户 id\n     */\n    private Long userId;\n\n    /**\n     * 创建时间\n     */\n    private Date createTime;\n\n    /**\n     * 更新时间\n     */\n    private Date updateTime;\n\n    /**\n     * 是否删除\n     */\n    @TableLogic\n    private Integer isDelete;\n\n    @TableField(exist = false)\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/entity/User.java",
    "content": "package com.yupi.yudada.model.entity;\n\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableField;\nimport com.baomidou.mybatisplus.annotation.TableId;\nimport com.baomidou.mybatisplus.annotation.TableLogic;\nimport com.baomidou.mybatisplus.annotation.TableName;\nimport java.io.Serializable;\nimport java.util.Date;\nimport lombok.Data;\n\n/**\n * 用户\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@TableName(value = \"user\")\n@Data\npublic class User implements Serializable {\n\n    /**\n     * id\n     */\n    @TableId(type = IdType.ASSIGN_ID)\n    private Long id;\n\n    /**\n     * 用户账号\n     */\n    private String userAccount;\n\n    /**\n     * 用户密码\n     */\n    private String userPassword;\n\n    /**\n     * 开放平台id\n     */\n    private String unionId;\n\n    /**\n     * 公众号openId\n     */\n    private String mpOpenId;\n\n    /**\n     * 用户昵称\n     */\n    private String userName;\n\n    /**\n     * 用户头像\n     */\n    private String userAvatar;\n\n    /**\n     * 用户简介\n     */\n    private String userProfile;\n\n    /**\n     * 用户角色：user/admin/ban\n     */\n    private String userRole;\n\n    /**\n     * 创建时间\n     */\n    private Date createTime;\n\n    /**\n     * 更新时间\n     */\n    private Date updateTime;\n\n    /**\n     * 是否删除\n     */\n    @TableLogic\n    private Integer isDelete;\n\n    @TableField(exist = false)\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/entity/UserAnswer.java",
    "content": "package com.yupi.yudada.model.entity;\n\nimport com.baomidou.mybatisplus.annotation.*;\n\nimport java.io.Serializable;\nimport java.util.Date;\nimport lombok.Data;\n\n/**\n * 用户答题记录\n * @TableName user_answer\n */\n@TableName(value =\"user_answer\")\n@Data\npublic class UserAnswer implements Serializable {\n    /**\n     * \n     */\n    @TableId(type = IdType.ASSIGN_ID)\n    private Long id;\n\n    /**\n     * 应用 id\n     */\n    private Long appId;\n\n    /**\n     * 应用类型（0-得分类，1-角色测评类）\n     */\n    private Integer appType;\n\n    /**\n     * 评分策略（0-自定义，1-AI）\n     */\n    private Integer scoringStrategy;\n\n    /**\n     * 用户答案（JSON 数组）\n     */\n    private String choices;\n\n    /**\n     * 评分结果 id\n     */\n    private Long resultId;\n\n    /**\n     * 结果名称，如物流师\n     */\n    private String resultName;\n\n    /**\n     * 结果描述\n     */\n    private String resultDesc;\n\n    /**\n     * 结果图标\n     */\n    private String resultPicture;\n\n    /**\n     * 得分\n     */\n    private Integer resultScore;\n\n    /**\n     * 用户 id\n     */\n    private Long userId;\n\n    /**\n     * 创建时间\n     */\n    private Date createTime;\n\n    /**\n     * 更新时间\n     */\n    private Date updateTime;\n\n    /**\n     * 是否删除\n     */\n    @TableLogic\n    private Integer isDelete;\n\n    @TableField(exist = false)\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/enums/AppScoringStrategyEnum.java",
    "content": "package com.yupi.yudada.model.enums;\n\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * App 评分策略枚举\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\npublic enum AppScoringStrategyEnum {\n\n    CUSTOM(\"自定义\", 0),\n    AI(\"AI\", 1);\n\n    private final String text;\n\n    private final int value;\n\n    AppScoringStrategyEnum(String text, int value) {\n        this.text = text;\n        this.value = value;\n    }\n\n    /**\n     * 根据 value 获取枚举\n     *\n     * @param value\n     * @return\n     */\n    public static AppScoringStrategyEnum getEnumByValue(Integer value) {\n        if (ObjectUtil.isEmpty(value)) {\n            return null;\n        }\n        for (AppScoringStrategyEnum anEnum : AppScoringStrategyEnum.values()) {\n            if (anEnum.value == value) {\n                return anEnum;\n            }\n        }\n        return null;\n    }\n\n    /**\n     * 获取值列表\n     *\n     * @return\n     */\n    public static List<Integer> getValues() {\n        return Arrays.stream(values()).map(item -> item.value).collect(Collectors.toList());\n    }\n\n    public int getValue() {\n        return value;\n    }\n\n    public String getText() {\n        return text;\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/enums/AppTypeEnum.java",
    "content": "package com.yupi.yudada.model.enums;\n\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * App 应用类型枚举\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\npublic enum AppTypeEnum {\n\n    SCORE(\"得分类\", 0),\n    TEST(\"测评类\", 1);\n\n    private final String text;\n\n    private final int value;\n\n    AppTypeEnum(String text, int value) {\n        this.text = text;\n        this.value = value;\n    }\n\n    /**\n     * 根据 value 获取枚举\n     *\n     * @param value\n     * @return\n     */\n    public static AppTypeEnum getEnumByValue(Integer value) {\n        if (ObjectUtil.isEmpty(value)) {\n            return null;\n        }\n        for (AppTypeEnum anEnum : AppTypeEnum.values()) {\n            if (anEnum.value == value) {\n                return anEnum;\n            }\n        }\n        return null;\n    }\n\n    /**\n     * 获取值列表\n     *\n     * @return\n     */\n    public static List<Integer> getValues() {\n        return Arrays.stream(values()).map(item -> item.value).collect(Collectors.toList());\n    }\n\n    public int getValue() {\n        return value;\n    }\n\n    public String getText() {\n        return text;\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/enums/FileUploadBizEnum.java",
    "content": "package com.yupi.yudada.model.enums;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport org.apache.commons.lang3.ObjectUtils;\n\n/**\n * 文件上传业务类型枚举\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\npublic enum FileUploadBizEnum {\n\n    USER_AVATAR(\"用户头像\", \"user_avatar\"),\n    APP_ICON(\"应用图标\", \"app_icon\"),\n    SCORING_RESULT_PICTURE(\"评分结果图片\", \"scoring_result_picture\");\n\n    private final String text;\n\n    private final String value;\n\n    FileUploadBizEnum(String text, String value) {\n        this.text = text;\n        this.value = value;\n    }\n\n    /**\n     * 获取值列表\n     *\n     * @return\n     */\n    public static List<String> getValues() {\n        return Arrays.stream(values()).map(item -> item.value).collect(Collectors.toList());\n    }\n\n    /**\n     * 根据 value 获取枚举\n     *\n     * @param value\n     * @return\n     */\n    public static FileUploadBizEnum getEnumByValue(String value) {\n        if (ObjectUtils.isEmpty(value)) {\n            return null;\n        }\n        for (FileUploadBizEnum anEnum : FileUploadBizEnum.values()) {\n            if (anEnum.value.equals(value)) {\n                return anEnum;\n            }\n        }\n        return null;\n    }\n\n    public String getValue() {\n        return value;\n    }\n\n    public String getText() {\n        return text;\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/enums/ReviewStatusEnum.java",
    "content": "package com.yupi.yudada.model.enums;\n\nimport cn.hutool.core.util.ObjectUtil;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * 审核状态枚举\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\npublic enum ReviewStatusEnum {\n\n    REVIEWING(\"待审核\", 0),\n    PASS(\"通过\", 1),\n    REJECT(\"拒绝\", 2);\n\n    private final String text;\n\n    private final int value;\n\n    ReviewStatusEnum(String text, int value) {\n        this.text = text;\n        this.value = value;\n    }\n\n    /**\n     * 根据 value 获取枚举\n     *\n     * @param value\n     * @return\n     */\n    public static ReviewStatusEnum getEnumByValue(Integer value) {\n        if (ObjectUtil.isEmpty(value)) {\n            return null;\n        }\n        for (ReviewStatusEnum anEnum : ReviewStatusEnum.values()) {\n            if (anEnum.value == value) {\n                return anEnum;\n            }\n        }\n        return null;\n    }\n\n    /**\n     * 获取值列表\n     *\n     * @return\n     */\n    public static List<Integer> getValues() {\n        return Arrays.stream(values()).map(item -> item.value).collect(Collectors.toList());\n    }\n\n    public int getValue() {\n        return value;\n    }\n\n    public String getText() {\n        return text;\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/enums/UserRoleEnum.java",
    "content": "package com.yupi.yudada.model.enums;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport org.apache.commons.lang3.ObjectUtils;\n\n/**\n * 用户角色枚举\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\npublic enum UserRoleEnum {\n\n    USER(\"用户\", \"user\"),\n    ADMIN(\"管理员\", \"admin\"),\n    BAN(\"被封号\", \"ban\");\n\n    private final String text;\n\n    private final String value;\n\n    UserRoleEnum(String text, String value) {\n        this.text = text;\n        this.value = value;\n    }\n\n    /**\n     * 获取值列表\n     *\n     * @return\n     */\n    public static List<String> getValues() {\n        return Arrays.stream(values()).map(item -> item.value).collect(Collectors.toList());\n    }\n\n    /**\n     * 根据 value 获取枚举\n     *\n     * @param value\n     * @return\n     */\n    public static UserRoleEnum getEnumByValue(String value) {\n        if (ObjectUtils.isEmpty(value)) {\n            return null;\n        }\n        for (UserRoleEnum anEnum : UserRoleEnum.values()) {\n            if (anEnum.value.equals(value)) {\n                return anEnum;\n            }\n        }\n        return null;\n    }\n\n    public String getValue() {\n        return value;\n    }\n\n    public String getText() {\n        return text;\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/vo/AppVO.java",
    "content": "package com.yupi.yudada.model.vo;\n\nimport cn.hutool.json.JSONUtil;\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableId;\nimport com.yupi.yudada.model.entity.App;\nimport lombok.Data;\nimport org.springframework.beans.BeanUtils;\n\nimport java.io.Serializable;\nimport java.util.Date;\nimport java.util.List;\n\n/**\n * 应用视图\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Data\npublic class AppVO implements Serializable {\n\n    /**\n     * id\n     */\n    private Long id;\n\n    /**\n     * 应用名\n     */\n    private String appName;\n\n    /**\n     * 应用描述\n     */\n    private String appDesc;\n\n    /**\n     * 应用图标\n     */\n    private String appIcon;\n\n    /**\n     * 应用类型（0-得分类，1-测评类）\n     */\n    private Integer appType;\n\n    /**\n     * 评分策略（0-自定义，1-AI）\n     */\n    private Integer scoringStrategy;\n\n    /**\n     * 审核状态：0-待审核, 1-通过, 2-拒绝\n     */\n    private Integer reviewStatus;\n\n    /**\n     * 审核信息\n     */\n    private String reviewMessage;\n\n    /**\n     * 审核人 id\n     */\n    private Long reviewerId;\n\n    /**\n     * 审核时间\n     */\n    private Date reviewTime;\n\n    /**\n     * 创建用户 id\n     */\n    private Long userId;\n\n    /**\n     * 创建时间\n     */\n    private Date createTime;\n\n    /**\n     * 更新时间\n     */\n    private Date updateTime;\n\n    /**\n     * 创建用户信息\n     */\n    private UserVO user;\n\n    /**\n     * 封装类转对象\n     *\n     * @param appVO\n     * @return\n     */\n    public static App voToObj(AppVO appVO) {\n        if (appVO == null) {\n            return null;\n        }\n        App app = new App();\n        BeanUtils.copyProperties(appVO, app);\n        return app;\n    }\n\n    /**\n     * 对象转封装类\n     *\n     * @param app\n     * @return\n     */\n    public static AppVO objToVo(App app) {\n        if (app == null) {\n            return null;\n        }\n        AppVO appVO = new AppVO();\n        BeanUtils.copyProperties(app, appVO);\n        return appVO;\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/vo/LoginUserVO.java",
    "content": "package com.yupi.yudada.model.vo;\n\nimport java.io.Serializable;\nimport java.util.Date;\nimport lombok.Data;\n\n/**\n * 已登录用户视图（脱敏）\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n **/\n@Data\npublic class LoginUserVO implements Serializable {\n\n    /**\n     * 用户 id\n     */\n    private Long id;\n\n    /**\n     * 用户昵称\n     */\n    private String userName;\n\n    /**\n     * 用户头像\n     */\n    private String userAvatar;\n\n    /**\n     * 用户简介\n     */\n    private String userProfile;\n\n    /**\n     * 用户角色：user/admin/ban\n     */\n    private String userRole;\n\n    /**\n     * 创建时间\n     */\n    private Date createTime;\n\n    /**\n     * 更新时间\n     */\n    private Date updateTime;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/vo/PostVO.java",
    "content": "package com.yupi.yudada.model.vo;\n\nimport cn.hutool.json.JSONUtil;\nimport com.yupi.yudada.model.entity.Post;\nimport java.io.Serializable;\nimport java.util.Date;\nimport java.util.List;\nimport lombok.Data;\nimport org.springframework.beans.BeanUtils;\n\n/**\n * 帖子视图\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Data\npublic class PostVO implements Serializable {\n\n    /**\n     * id\n     */\n    private Long id;\n\n    /**\n     * 标题\n     */\n    private String title;\n\n    /**\n     * 内容\n     */\n    private String content;\n\n    /**\n     * 点赞数\n     */\n    private Integer thumbNum;\n\n    /**\n     * 收藏数\n     */\n    private Integer favourNum;\n\n    /**\n     * 创建用户 id\n     */\n    private Long userId;\n\n    /**\n     * 创建时间\n     */\n    private Date createTime;\n\n    /**\n     * 更新时间\n     */\n    private Date updateTime;\n\n    /**\n     * 标签列表\n     */\n    private List<String> tagList;\n\n    /**\n     * 创建人信息\n     */\n    private UserVO user;\n\n    /**\n     * 是否已点赞\n     */\n    private Boolean hasThumb;\n\n    /**\n     * 是否已收藏\n     */\n    private Boolean hasFavour;\n\n    /**\n     * 包装类转对象\n     *\n     * @param postVO\n     * @return\n     */\n    public static Post voToObj(PostVO postVO) {\n        if (postVO == null) {\n            return null;\n        }\n        Post post = new Post();\n        BeanUtils.copyProperties(postVO, post);\n        List<String> tagList = postVO.getTagList();\n        post.setTags(JSONUtil.toJsonStr(tagList));\n        return post;\n    }\n\n    /**\n     * 对象转包装类\n     *\n     * @param post\n     * @return\n     */\n    public static PostVO objToVo(Post post) {\n        if (post == null) {\n            return null;\n        }\n        PostVO postVO = new PostVO();\n        BeanUtils.copyProperties(post, postVO);\n        postVO.setTagList(JSONUtil.toList(post.getTags(), String.class));\n        return postVO;\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/vo/QuestionVO.java",
    "content": "package com.yupi.yudada.model.vo;\n\nimport cn.hutool.json.JSONUtil;\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableId;\nimport com.yupi.yudada.model.dto.question.QuestionContentDTO;\nimport com.yupi.yudada.model.entity.Question;\nimport lombok.Data;\nimport org.springframework.beans.BeanUtils;\n\nimport java.io.Serializable;\nimport java.util.Date;\nimport java.util.List;\n\n/**\n * 题目视图\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Data\npublic class QuestionVO implements Serializable {\n    /**\n     * id\n     */\n    private Long id;\n\n    /**\n     * 题目内容（json格式）\n     */\n    private List<QuestionContentDTO> questionContent;\n\n    /**\n     * 应用 id\n     */\n    private Long appId;\n\n    /**\n     * 创建用户 id\n     */\n    private Long userId;\n\n    /**\n     * 创建时间\n     */\n    private Date createTime;\n\n    /**\n     * 更新时间\n     */\n    private Date updateTime;\n\n    /**\n     * 创建用户信息\n     */\n    private UserVO user;\n\n    /**\n     * 封装类转对象\n     *\n     * @param questionVO\n     * @return\n     */\n    public static Question voToObj(QuestionVO questionVO) {\n        if (questionVO == null) {\n            return null;\n        }\n        Question question = new Question();\n        BeanUtils.copyProperties(questionVO, question);\n        List<QuestionContentDTO> questionContentDTO = questionVO.getQuestionContent();\n        question.setQuestionContent(JSONUtil.toJsonStr(questionContentDTO));\n        return question;\n    }\n\n    /**\n     * 对象转封装类\n     *\n     * @param question\n     * @return\n     */\n    public static QuestionVO objToVo(Question question) {\n        if (question == null) {\n            return null;\n        }\n        QuestionVO questionVO = new QuestionVO();\n        BeanUtils.copyProperties(question, questionVO);\n        String questionContent = question.getQuestionContent();\n        if (questionContent != null) {\n            questionVO.setQuestionContent(JSONUtil.toList(questionContent, QuestionContentDTO.class));\n        }\n        return questionVO;\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/vo/ScoringResultVO.java",
    "content": "package com.yupi.yudada.model.vo;\n\nimport cn.hutool.json.JSONUtil;\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableId;\nimport com.yupi.yudada.model.entity.ScoringResult;\nimport lombok.Data;\nimport org.springframework.beans.BeanUtils;\n\nimport java.io.Serializable;\nimport java.util.Date;\nimport java.util.List;\n\n/**\n * 评分结果视图\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Data\npublic class ScoringResultVO implements Serializable {\n\n    /**\n     * id\n     */\n    private Long id;\n\n    /**\n     * 结果名称，如物流师\n     */\n    private String resultName;\n\n    /**\n     * 结果描述\n     */\n    private String resultDesc;\n\n    /**\n     * 结果图片\n     */\n    private String resultPicture;\n\n    /**\n     * 结果属性集合 JSON，如 [I,S,T,J]\n     */\n    private List<String> resultProp;\n\n    /**\n     * 结果得分范围，如 80，表示 80及以上的分数命中此结果\n     */\n    private Integer resultScoreRange;\n\n    /**\n     * 应用 id\n     */\n    private Long appId;\n\n    /**\n     * 创建用户 id\n     */\n    private Long userId;\n\n    /**\n     * 创建时间\n     */\n    private Date createTime;\n\n    /**\n     * 更新时间\n     */\n    private Date updateTime;\n\n    /**\n     * 创建用户信息\n     */\n    private UserVO user;\n\n    /**\n     * 封装类转对象\n     *\n     * @param scoringResultVO\n     * @return\n     */\n    public static ScoringResult voToObj(ScoringResultVO scoringResultVO) {\n        if (scoringResultVO == null) {\n            return null;\n        }\n        ScoringResult scoringResult = new ScoringResult();\n        BeanUtils.copyProperties(scoringResultVO, scoringResult);\n        scoringResult.setResultProp(JSONUtil.toJsonStr(scoringResultVO.getResultProp()));\n        return scoringResult;\n    }\n\n    /**\n     * 对象转封装类\n     *\n     * @param scoringResult\n     * @return\n     */\n    public static ScoringResultVO objToVo(ScoringResult scoringResult) {\n        if (scoringResult == null) {\n            return null;\n        }\n        ScoringResultVO scoringResultVO = new ScoringResultVO();\n        BeanUtils.copyProperties(scoringResult, scoringResultVO);\n        scoringResultVO.setResultProp(JSONUtil.toList(scoringResult.getResultProp(), String.class));\n        return scoringResultVO;\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/vo/UserAnswerVO.java",
    "content": "package com.yupi.yudada.model.vo;\n\nimport cn.hutool.json.JSONUtil;\nimport com.baomidou.mybatisplus.annotation.IdType;\nimport com.baomidou.mybatisplus.annotation.TableId;\nimport com.yupi.yudada.model.entity.UserAnswer;\nimport lombok.Data;\nimport org.springframework.beans.BeanUtils;\n\nimport java.io.Serializable;\nimport java.util.Date;\nimport java.util.List;\n\n/**\n * 用户答案视图\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Data\npublic class UserAnswerVO implements Serializable {\n    /**\n     *\n     */\n    private Long id;\n\n    /**\n     * 应用 id\n     */\n    private Long appId;\n\n    /**\n     * 应用类型（0-得分类，1-角色测评类）\n     */\n    private Integer appType;\n\n    /**\n     * 评分策略（0-自定义，1-AI）\n     */\n    private Integer scoringStrategy;\n\n    /**\n     * 用户答案（JSON 数组）\n     */\n    private List<String> choices;\n\n    /**\n     * 评分结果 id\n     */\n    private Long resultId;\n\n    /**\n     * 结果名称，如物流师\n     */\n    private String resultName;\n\n    /**\n     * 结果描述\n     */\n    private String resultDesc;\n\n    /**\n     * 结果图标\n     */\n    private String resultPicture;\n\n    /**\n     * 得分\n     */\n    private Integer resultScore;\n\n    /**\n     * 用户 id\n     */\n    private Long userId;\n\n    /**\n     * 创建时间\n     */\n    private Date createTime;\n\n    /**\n     * 更新时间\n     */\n    private Date updateTime;\n\n    /**\n     * 创建用户信息\n     */\n    private UserVO user;\n\n    /**\n     * 封装类转对象\n     *\n     * @param userAnswerVO\n     * @return\n     */\n    public static UserAnswer voToObj(UserAnswerVO userAnswerVO) {\n        if (userAnswerVO == null) {\n            return null;\n        }\n        UserAnswer userAnswer = new UserAnswer();\n        BeanUtils.copyProperties(userAnswerVO, userAnswer);\n        userAnswer.setChoices(JSONUtil.toJsonStr(userAnswerVO.getChoices()));\n        return userAnswer;\n    }\n\n    /**\n     * 对象转封装类\n     *\n     * @param userAnswer\n     * @return\n     */\n    public static UserAnswerVO objToVo(UserAnswer userAnswer) {\n        if (userAnswer == null) {\n            return null;\n        }\n        UserAnswerVO userAnswerVO = new UserAnswerVO();\n        BeanUtils.copyProperties(userAnswer, userAnswerVO);\n        userAnswerVO.setChoices(JSONUtil.toList(userAnswer.getChoices(), String.class));\n        return userAnswerVO;\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/model/vo/UserVO.java",
    "content": "package com.yupi.yudada.model.vo;\n\nimport java.io.Serializable;\nimport java.util.Date;\nimport lombok.Data;\n\n/**\n * 用户视图（脱敏）\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Data\npublic class UserVO implements Serializable {\n\n    /**\n     * id\n     */\n    private Long id;\n\n    /**\n     * 用户昵称\n     */\n    private String userName;\n\n    /**\n     * 用户头像\n     */\n    private String userAvatar;\n\n    /**\n     * 用户简介\n     */\n    private String userProfile;\n\n    /**\n     * 用户角色：user/admin/ban\n     */\n    private String userRole;\n\n    /**\n     * 创建时间\n     */\n    private Date createTime;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/scoring/AiTestScoringStrategy.java",
    "content": "package com.yupi.yudada.scoring;\n\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.crypto.digest.DigestUtil;\nimport cn.hutool.json.JSONUtil;\nimport com.baomidou.mybatisplus.core.toolkit.Wrappers;\nimport com.github.benmanes.caffeine.cache.Cache;\nimport com.github.benmanes.caffeine.cache.Caffeine;\nimport com.yupi.yudada.manager.AiManager;\nimport com.yupi.yudada.model.dto.question.QuestionAnswerDTO;\nimport com.yupi.yudada.model.dto.question.QuestionContentDTO;\nimport com.yupi.yudada.model.entity.App;\nimport com.yupi.yudada.model.entity.Question;\nimport com.yupi.yudada.model.entity.UserAnswer;\nimport com.yupi.yudada.model.vo.QuestionVO;\nimport com.yupi.yudada.service.QuestionService;\nimport org.redisson.api.RLock;\nimport org.redisson.api.RedissonClient;\n\nimport javax.annotation.Resource;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * AI 测评类应用评分策略\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@ScoringStrategyConfig(appType = 1, scoringStrategy = 1)\npublic class AiTestScoringStrategy implements ScoringStrategy {\n\n    @Resource\n    private QuestionService questionService;\n\n    @Resource\n    private AiManager aiManager;\n\n    @Resource\n    private RedissonClient redissonClient;\n\n    // 分布式锁的 key\n    private static final String AI_ANSWER_LOCK = \"AI_ANSWER_LOCK\";\n\n    /**\n     * AI 评分结果本地缓存\n     */\n    private final Cache<String, String> answerCacheMap =\n            Caffeine.newBuilder().initialCapacity(1024)\n                    // 缓存 5 分钟移除\n                    .expireAfterAccess(5L, TimeUnit.MINUTES)\n                    .build();\n\n    /**\n     * AI 评分系统消息\n     */\n    private static final String AI_TEST_SCORING_SYSTEM_MESSAGE = \"你是一位严谨的判题专家，我会给你如下信息：\\n\" +\n            \"```\\n\" +\n            \"应用名称，\\n\" +\n            \"【【【应用描述】】】，\\n\" +\n            \"题目和用户回答的列表：格式为 [{\\\"title\\\": \\\"题目\\\",\\\"answer\\\": \\\"用户回答\\\"}]\\n\" +\n            \"```\\n\" +\n            \"\\n\" +\n            \"请你根据上述信息，按照以下步骤来对用户进行评价：\\n\" +\n            \"1. 要求：需要给出一个明确的评价结果，包括评价名称（尽量简短）和评价描述（尽量详细，大于 200 字）\\n\" +\n            \"2. 严格按照下面的 json 格式输出评价名称和评价描述\\n\" +\n            \"```\\n\" +\n            \"{\\\"resultName\\\": \\\"评价名称\\\", \\\"resultDesc\\\": \\\"评价描述\\\"}\\n\" +\n            \"```\\n\" +\n            \"3. 返回格式必须为 JSON 对象\";\n\n    @Override\n    public UserAnswer doScore(List<String> choices, App app) throws Exception {\n        Long appId = app.getId();\n        String jsonStr = JSONUtil.toJsonStr(choices);\n        String cacheKey = buildCacheKey(appId, jsonStr);\n        String answerJson = answerCacheMap.getIfPresent(cacheKey);\n        // 如果有缓存，直接返回\n        if (StrUtil.isNotBlank(answerJson)) {\n            // 构造返回值，填充答案对象的属性\n            UserAnswer userAnswer = JSONUtil.toBean(answerJson, UserAnswer.class);\n            userAnswer.setAppId(appId);\n            userAnswer.setAppType(app.getAppType());\n            userAnswer.setScoringStrategy(app.getScoringStrategy());\n            userAnswer.setChoices(jsonStr);\n            return userAnswer;\n        }\n\n        // 定义锁\n        RLock lock = redissonClient.getLock(AI_ANSWER_LOCK + cacheKey);\n        try {\n            // 竞争锁\n            boolean res = lock.tryLock(3, 15, TimeUnit.SECONDS);\n            // 没抢到锁，强行返回\n            if (!res) {\n                return null;\n            }\n            // 抢到锁了，执行后续业务逻辑\n            // 1. 根据 id 查询到题目\n            Question question = questionService.getOne(\n                    Wrappers.lambdaQuery(Question.class).eq(Question::getAppId, appId)\n            );\n            QuestionVO questionVO = QuestionVO.objToVo(question);\n            List<QuestionContentDTO> questionContent = questionVO.getQuestionContent();\n\n            // 2. 调用 AI 获取结果\n            // 封装 Prompt\n            String userMessage = getAiTestScoringUserMessage(app, questionContent, choices);\n            // AI 生成\n            String result = aiManager.doSyncStableRequest(AI_TEST_SCORING_SYSTEM_MESSAGE, userMessage);\n            // 截取需要的 JSON 信息\n            int start = result.indexOf(\"{\");\n            int end = result.lastIndexOf(\"}\");\n            String json = result.substring(start, end + 1);\n\n            // 缓存结果\n            answerCacheMap.put(cacheKey, json);\n\n            // 3. 构造返回值，填充答案对象的属性\n            UserAnswer userAnswer = JSONUtil.toBean(json, UserAnswer.class);\n            userAnswer.setAppId(appId);\n            userAnswer.setAppType(app.getAppType());\n            userAnswer.setScoringStrategy(app.getScoringStrategy());\n            userAnswer.setChoices(jsonStr);\n            return userAnswer;\n        } finally {\n            if (lock != null && lock.isLocked()) {\n                if (lock.isHeldByCurrentThread()) {\n                    lock.unlock();\n                }\n            }\n        }\n\n    }\n\n    /**\n     * AI 评分用户消息封装\n     *\n     * @param app\n     * @param questionContentDTOList\n     * @param choices\n     * @return\n     */\n    private String getAiTestScoringUserMessage(App app, List<QuestionContentDTO> questionContentDTOList, List<String> choices) {\n        StringBuilder userMessage = new StringBuilder();\n        userMessage.append(app.getAppName()).append(\"\\n\");\n        userMessage.append(app.getAppDesc()).append(\"\\n\");\n        List<QuestionAnswerDTO> questionAnswerDTOList = new ArrayList<>();\n        for (int i = 0; i < questionContentDTOList.size(); i++) {\n            QuestionAnswerDTO questionAnswerDTO = new QuestionAnswerDTO();\n            questionAnswerDTO.setTitle(questionContentDTOList.get(i).getTitle());\n            questionAnswerDTO.setUserAnswer(choices.get(i));\n            questionAnswerDTOList.add(questionAnswerDTO);\n        }\n        userMessage.append(JSONUtil.toJsonStr(questionAnswerDTOList));\n        return userMessage.toString();\n    }\n\n\n    /**\n     * 构建缓存 key\n     *\n     * @param appId\n     * @param choices\n     * @return\n     */\n    private String buildCacheKey(Long appId, String choices) {\n        return DigestUtil.md5Hex(appId + \":\" + choices);\n    }\n\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/scoring/CustomScoreScoringStrategy.java",
    "content": "package com.yupi.yudada.scoring;\n\nimport cn.hutool.json.JSONUtil;\nimport com.baomidou.mybatisplus.core.toolkit.Wrappers;\nimport com.yupi.yudada.common.ErrorCode;\nimport com.yupi.yudada.exception.BusinessException;\nimport com.yupi.yudada.model.dto.question.QuestionContentDTO;\nimport com.yupi.yudada.model.entity.App;\nimport com.yupi.yudada.model.entity.Question;\nimport com.yupi.yudada.model.entity.ScoringResult;\nimport com.yupi.yudada.model.entity.UserAnswer;\nimport com.yupi.yudada.model.vo.QuestionVO;\nimport com.yupi.yudada.service.QuestionService;\nimport com.yupi.yudada.service.ScoringResultService;\n\nimport javax.annotation.Resource;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.stream.Collectors;\n\n/**\n * 自定义打分类应用评分策略\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@ScoringStrategyConfig(appType = 0, scoringStrategy = 0)\npublic class CustomScoreScoringStrategy implements ScoringStrategy {\n\n    @Resource\n    private QuestionService questionService;\n\n    @Resource\n    private ScoringResultService scoringResultService;\n\n    @Override\n    public UserAnswer doScore(List<String> choices, App app) throws Exception {\n        Long appId = app.getId();\n        // 1. 根据 id 查询到题目和题目结果信息（按分数降序排序）\n        Question question = questionService.getOne(\n                Wrappers.lambdaQuery(Question.class).eq(Question::getAppId, appId)\n        );\n        List<ScoringResult> scoringResultList = scoringResultService.list(\n                Wrappers.lambdaQuery(ScoringResult.class)\n                        .eq(ScoringResult::getAppId, appId)\n                        .orderByDesc(ScoringResult::getResultScoreRange)\n        );\n\n        // 2. 统计用户的总得分\n        int totalScore = 0;\n        QuestionVO questionVO = QuestionVO.objToVo(question);\n        List<QuestionContentDTO> questionContent = questionVO.getQuestionContent();\n        // 校验数量\n        if (questionContent.size() != choices.size()) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR, \"题目和用户答案数量不一致\");\n        }\n        // 遍历题目列表\n        for (int i = 0; i < questionContent.size(); i++) {\n            Map<String, Integer> resultMap = questionContent.get(i).getOptions().stream()\n                    .collect(Collectors.toMap(QuestionContentDTO.Option::getKey, QuestionContentDTO.Option::getScore));\n            Integer score = Optional.ofNullable(resultMap.get(choices.get(i))).orElse(0);\n            totalScore += score;\n        }\n\n        // 3. 遍历得分结果，找到第一个用户分数大于得分范围的结果，作为最终结果\n        ScoringResult maxScoringResult = scoringResultList.get(0);\n        for (ScoringResult scoringResult : scoringResultList) {\n            if (totalScore >= scoringResult.getResultScoreRange()) {\n                maxScoringResult = scoringResult;\n                break;\n            }\n        }\n\n        // 4. 构造返回值，填充答案对象的属性\n        UserAnswer userAnswer = new UserAnswer();\n        userAnswer.setAppId(appId);\n        userAnswer.setAppType(app.getAppType());\n        userAnswer.setScoringStrategy(app.getScoringStrategy());\n        userAnswer.setChoices(JSONUtil.toJsonStr(choices));\n        userAnswer.setResultId(maxScoringResult.getId());\n        userAnswer.setResultName(maxScoringResult.getResultName());\n        userAnswer.setResultDesc(maxScoringResult.getResultDesc());\n        userAnswer.setResultPicture(maxScoringResult.getResultPicture());\n        userAnswer.setResultScore(totalScore);\n        return userAnswer;\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/scoring/CustomTestScoringStrategy.java",
    "content": "package com.yupi.yudada.scoring;\n\nimport cn.hutool.json.JSONUtil;\nimport com.baomidou.mybatisplus.core.toolkit.Wrappers;\nimport com.yupi.yudada.model.dto.question.QuestionContentDTO;\nimport com.yupi.yudada.model.entity.App;\nimport com.yupi.yudada.model.entity.Question;\nimport com.yupi.yudada.model.entity.ScoringResult;\nimport com.yupi.yudada.model.entity.UserAnswer;\nimport com.yupi.yudada.model.vo.QuestionVO;\nimport com.yupi.yudada.service.QuestionService;\nimport com.yupi.yudada.service.ScoringResultService;\n\nimport javax.annotation.Resource;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * 自定义测评类应用评分策略\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@ScoringStrategyConfig(appType = 1, scoringStrategy = 0)\npublic class CustomTestScoringStrategy implements ScoringStrategy {\n\n    @Resource\n    private QuestionService questionService;\n\n    @Resource\n    private ScoringResultService scoringResultService;\n\n    @Override\n    public UserAnswer doScore(List<String> choices, App app) throws Exception {\n        Long appId = app.getId();\n        // 1. 根据 id 查询到题目和题目结果信息\n        Question question = questionService.getOne(\n                Wrappers.lambdaQuery(Question.class).eq(Question::getAppId, appId)\n        );\n        List<ScoringResult> scoringResultList = scoringResultService.list(\n                Wrappers.lambdaQuery(ScoringResult.class)\n                        .eq(ScoringResult::getAppId, appId)\n        );\n\n        // 2. 统计用户每个选择对应的属性个数，如 I = 10 个，E = 5 个\n        // 初始化一个Map，用于存储每个选项的计数\n        Map<String, Integer> optionCount = new HashMap<>();\n\n        QuestionVO questionVO = QuestionVO.objToVo(question);\n        List<QuestionContentDTO> questionContent = questionVO.getQuestionContent();\n\n        // 遍历题目列表\n        for (QuestionContentDTO questionContentDTO : questionContent) {\n            // 遍历答案列表\n            for (String answer : choices) {\n                // 遍历题目中的选项\n                for (QuestionContentDTO.Option option : questionContentDTO.getOptions()) {\n                    // 如果答案和选项的key匹配\n                    if (option.getKey().equals(answer)) {\n                        // 获取选项的result属性\n                        String result = option.getResult();\n\n                        // 如果result属性不在optionCount中，初始化为0\n                        if (!optionCount.containsKey(result)) {\n                            optionCount.put(result, 0);\n                        }\n\n                        // 在optionCount中增加计数\n                        optionCount.put(result, optionCount.get(result) + 1);\n                    }\n                }\n            }\n        }\n\n        // 3. 遍历每种评分结果，计算哪个结果的得分更高\n        // 初始化最高分数和最高分数对应的评分结果\n        int maxScore = 0;\n        ScoringResult maxScoringResult = scoringResultList.get(0);\n\n        // 遍历评分结果列表\n        for (ScoringResult scoringResult : scoringResultList) {\n            List<String> resultProp = JSONUtil.toList(scoringResult.getResultProp(), String.class);\n            // 计算当前评分结果的分数，[I, E] => [10, 5] => 15\n            int score = resultProp.stream()\n                    .mapToInt(prop -> optionCount.getOrDefault(prop, 0))\n                    .sum();\n\n            // 如果分数高于当前最高分数，更新最高分数和最高分数对应的评分结果\n            if (score > maxScore) {\n                maxScore = score;\n                maxScoringResult = scoringResult;\n            }\n        }\n\n        // 4. 构造返回值，填充答案对象的属性\n        UserAnswer userAnswer = new UserAnswer();\n        userAnswer.setAppId(appId);\n        userAnswer.setAppType(app.getAppType());\n        userAnswer.setScoringStrategy(app.getScoringStrategy());\n        userAnswer.setChoices(JSONUtil.toJsonStr(choices));\n        userAnswer.setResultId(maxScoringResult.getId());\n        userAnswer.setResultName(maxScoringResult.getResultName());\n        userAnswer.setResultDesc(maxScoringResult.getResultDesc());\n        userAnswer.setResultPicture(maxScoringResult.getResultPicture());\n        return userAnswer;\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/scoring/ScoringStrategy.java",
    "content": "package com.yupi.yudada.scoring;\n\nimport com.yupi.yudada.model.entity.App;\nimport com.yupi.yudada.model.entity.UserAnswer;\n\nimport java.util.List;\n\n/**\n * 评分策略\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\npublic interface ScoringStrategy {\n\n    /**\n     * 执行评分\n     *\n     * @param choices\n     * @param app\n     * @return\n     * @throws Exception\n     */\n    UserAnswer doScore(List<String> choices, App app) throws Exception;\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/scoring/ScoringStrategyConfig.java",
    "content": "package com.yupi.yudada.scoring;\n\nimport org.springframework.stereotype.Component;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Target(ElementType.TYPE)\n@Retention(RetentionPolicy.RUNTIME)\n@Component\npublic @interface ScoringStrategyConfig {\n\n    /**\n     * 应用类型\n     * @return\n     */\n    int appType();\n\n    /**\n     * 评分策略\n     * @return\n     */\n    int scoringStrategy();\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/scoring/ScoringStrategyContext.java",
    "content": "package com.yupi.yudada.scoring;\n\nimport com.yupi.yudada.common.ErrorCode;\nimport com.yupi.yudada.exception.BusinessException;\nimport com.yupi.yudada.model.entity.App;\nimport com.yupi.yudada.model.entity.UserAnswer;\nimport com.yupi.yudada.model.enums.AppScoringStrategyEnum;\nimport com.yupi.yudada.model.enums.AppTypeEnum;\nimport org.springframework.stereotype.Service;\n\nimport javax.annotation.Resource;\nimport java.util.List;\n\n@Service\n@Deprecated\npublic class ScoringStrategyContext {\n\n    @Resource\n    private CustomScoreScoringStrategy customScoreScoringStrategy;\n\n    @Resource\n    private CustomTestScoringStrategy customTestScoringStrategy;\n\n    /**\n     * 评分\n     *\n     * @param choiceList\n     * @param app\n     * @return\n     * @throws Exception\n     */\n    public UserAnswer doScore(List<String> choiceList, App app) throws Exception {\n        AppTypeEnum appTypeEnum = AppTypeEnum.getEnumByValue(app.getAppType());\n        AppScoringStrategyEnum appScoringStrategyEnum = AppScoringStrategyEnum.getEnumByValue(app.getScoringStrategy());\n        if (appTypeEnum == null || appScoringStrategyEnum == null) {\n            throw new BusinessException(ErrorCode.SYSTEM_ERROR, \"应用配置有误，未找到匹配的策略\");\n        }\n        // 根据不同的应用类别和评分策略，选择对应的策略执行\n        switch (appTypeEnum) {\n            case SCORE:\n                switch (appScoringStrategyEnum) {\n                    case CUSTOM:\n                        return customScoreScoringStrategy.doScore(choiceList, app);\n                    case AI:\n                        break;\n                }\n                break;\n            case TEST:\n                switch (appScoringStrategyEnum) {\n                    case CUSTOM:\n                        return customTestScoringStrategy.doScore(choiceList, app);\n                    case AI:\n                        break;\n                }\n                break;\n        }\n        throw new BusinessException(ErrorCode.SYSTEM_ERROR, \"应用配置有误，未找到匹配的策略\");\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/scoring/ScoringStrategyExecutor.java",
    "content": "package com.yupi.yudada.scoring;\n\nimport com.yupi.yudada.common.ErrorCode;\nimport com.yupi.yudada.exception.BusinessException;\nimport com.yupi.yudada.model.entity.App;\nimport com.yupi.yudada.model.entity.UserAnswer;\nimport org.springframework.stereotype.Service;\n\nimport javax.annotation.Resource;\nimport java.util.List;\n\n/**\n * 评分策略执行器\n */\n@Service\npublic class ScoringStrategyExecutor {\n\n    // 策略列表\n    @Resource\n    private List<ScoringStrategy> scoringStrategyList;\n\n\n    /**\n     * 评分\n     *\n     * @param choiceList\n     * @param app\n     * @return\n     * @throws Exception\n     */\n    public UserAnswer doScore(List<String> choiceList, App app) throws Exception {\n        Integer appType = app.getAppType();\n        Integer appScoringStrategy = app.getScoringStrategy();\n        if (appType == null || appScoringStrategy == null) {\n            throw new BusinessException(ErrorCode.SYSTEM_ERROR, \"应用配置有误，未找到匹配的策略\");\n        }\n        // 根据注解获取策略\n        for (ScoringStrategy strategy : scoringStrategyList) {\n            if (strategy.getClass().isAnnotationPresent(ScoringStrategyConfig.class)) {\n                ScoringStrategyConfig scoringStrategyConfig = strategy.getClass().getAnnotation(ScoringStrategyConfig.class);\n                if (scoringStrategyConfig.appType() == appType && scoringStrategyConfig.scoringStrategy() == appScoringStrategy) {\n                    return strategy.doScore(choiceList, app);\n                }\n            }\n        }\n        throw new BusinessException(ErrorCode.SYSTEM_ERROR, \"应用配置有误，未找到匹配的策略\");\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/service/AppService.java",
    "content": "package com.yupi.yudada.service;\n\nimport com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.baomidou.mybatisplus.extension.service.IService;\nimport com.yupi.yudada.model.dto.app.AppQueryRequest;\nimport com.yupi.yudada.model.entity.App;\nimport com.yupi.yudada.model.vo.AppVO;\n\nimport javax.servlet.http.HttpServletRequest;\n\n/**\n * 应用服务\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\npublic interface AppService extends IService<App> {\n\n    /**\n     * 校验数据\n     *\n     * @param app\n     * @param add 对创建的数据进行校验\n     */\n    void validApp(App app, boolean add);\n\n    /**\n     * 获取查询条件\n     *\n     * @param appQueryRequest\n     * @return\n     */\n    QueryWrapper<App> getQueryWrapper(AppQueryRequest appQueryRequest);\n    \n    /**\n     * 获取应用封装\n     *\n     * @param app\n     * @param request\n     * @return\n     */\n    AppVO getAppVO(App app, HttpServletRequest request);\n\n    /**\n     * 分页获取应用封装\n     *\n     * @param appPage\n     * @param request\n     * @return\n     */\n    Page<AppVO> getAppVOPage(Page<App> appPage, HttpServletRequest request);\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/service/PostFavourService.java",
    "content": "package com.yupi.yudada.service;\n\nimport com.baomidou.mybatisplus.core.conditions.Wrapper;\nimport com.baomidou.mybatisplus.core.metadata.IPage;\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.baomidou.mybatisplus.extension.service.IService;\nimport com.yupi.yudada.model.entity.Post;\nimport com.yupi.yudada.model.entity.PostFavour;\nimport com.yupi.yudada.model.entity.User;\n\n/**\n * 帖子收藏服务\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\npublic interface PostFavourService extends IService<PostFavour> {\n\n    /**\n     * 帖子收藏\n     *\n     * @param postId\n     * @param loginUser\n     * @return\n     */\n    int doPostFavour(long postId, User loginUser);\n\n    /**\n     * 分页获取用户收藏的帖子列表\n     *\n     * @param page\n     * @param queryWrapper\n     * @param favourUserId\n     * @return\n     */\n    Page<Post> listFavourPostByPage(IPage<Post> page, Wrapper<Post> queryWrapper,\n            long favourUserId);\n\n    /**\n     * 帖子收藏（内部服务）\n     *\n     * @param userId\n     * @param postId\n     * @return\n     */\n    int doPostFavourInner(long userId, long postId);\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/service/PostService.java",
    "content": "package com.yupi.yudada.service;\n\nimport com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.baomidou.mybatisplus.extension.service.IService;\nimport com.yupi.yudada.model.dto.post.PostQueryRequest;\nimport com.yupi.yudada.model.entity.Post;\nimport com.yupi.yudada.model.vo.PostVO;\nimport javax.servlet.http.HttpServletRequest;\n\n/**\n * 帖子服务\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\npublic interface PostService extends IService<Post> {\n\n    /**\n     * 校验\n     *\n     * @param post\n     * @param add\n     */\n    void validPost(Post post, boolean add);\n\n    /**\n     * 获取查询条件\n     *\n     * @param postQueryRequest\n     * @return\n     */\n    QueryWrapper<Post> getQueryWrapper(PostQueryRequest postQueryRequest);\n\n    /**\n     * 获取帖子封装\n     *\n     * @param post\n     * @param request\n     * @return\n     */\n    PostVO getPostVO(Post post, HttpServletRequest request);\n\n    /**\n     * 分页获取帖子封装\n     *\n     * @param postPage\n     * @param request\n     * @return\n     */\n    Page<PostVO> getPostVOPage(Page<Post> postPage, HttpServletRequest request);\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/service/PostThumbService.java",
    "content": "package com.yupi.yudada.service;\n\nimport com.yupi.yudada.model.entity.PostThumb;\nimport com.baomidou.mybatisplus.extension.service.IService;\nimport com.yupi.yudada.model.entity.User;\n\n/**\n * 帖子点赞服务\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\npublic interface PostThumbService extends IService<PostThumb> {\n\n    /**\n     * 点赞\n     *\n     * @param postId\n     * @param loginUser\n     * @return\n     */\n    int doPostThumb(long postId, User loginUser);\n\n    /**\n     * 帖子点赞（内部服务）\n     *\n     * @param userId\n     * @param postId\n     * @return\n     */\n    int doPostThumbInner(long userId, long postId);\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/service/QuestionService.java",
    "content": "package com.yupi.yudada.service;\n\nimport com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.baomidou.mybatisplus.extension.service.IService;\nimport com.yupi.yudada.model.dto.question.QuestionQueryRequest;\nimport com.yupi.yudada.model.entity.Question;\nimport com.yupi.yudada.model.vo.QuestionVO;\n\nimport javax.servlet.http.HttpServletRequest;\n\n/**\n * 题目服务\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\npublic interface QuestionService extends IService<Question> {\n\n    /**\n     * 校验数据\n     *\n     * @param question\n     * @param add 对创建的数据进行校验\n     */\n    void validQuestion(Question question, boolean add);\n\n    /**\n     * 获取查询条件\n     *\n     * @param questionQueryRequest\n     * @return\n     */\n    QueryWrapper<Question> getQueryWrapper(QuestionQueryRequest questionQueryRequest);\n    \n    /**\n     * 获取题目封装\n     *\n     * @param question\n     * @param request\n     * @return\n     */\n    QuestionVO getQuestionVO(Question question, HttpServletRequest request);\n\n    /**\n     * 分页获取题目封装\n     *\n     * @param questionPage\n     * @param request\n     * @return\n     */\n    Page<QuestionVO> getQuestionVOPage(Page<Question> questionPage, HttpServletRequest request);\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/service/ScoringResultService.java",
    "content": "package com.yupi.yudada.service;\n\nimport com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.baomidou.mybatisplus.extension.service.IService;\nimport com.yupi.yudada.model.dto.scoringResult.ScoringResultQueryRequest;\nimport com.yupi.yudada.model.entity.ScoringResult;\nimport com.yupi.yudada.model.vo.ScoringResultVO;\n\nimport javax.servlet.http.HttpServletRequest;\n\n/**\n * 评分结果服务\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\npublic interface ScoringResultService extends IService<ScoringResult> {\n\n    /**\n     * 校验数据\n     *\n     * @param scoringResult\n     * @param add 对创建的数据进行校验\n     */\n    void validScoringResult(ScoringResult scoringResult, boolean add);\n\n    /**\n     * 获取查询条件\n     *\n     * @param scoringResultQueryRequest\n     * @return\n     */\n    QueryWrapper<ScoringResult> getQueryWrapper(ScoringResultQueryRequest scoringResultQueryRequest);\n    \n    /**\n     * 获取评分结果封装\n     *\n     * @param scoringResult\n     * @param request\n     * @return\n     */\n    ScoringResultVO getScoringResultVO(ScoringResult scoringResult, HttpServletRequest request);\n\n    /**\n     * 分页获取评分结果封装\n     *\n     * @param scoringResultPage\n     * @param request\n     * @return\n     */\n    Page<ScoringResultVO> getScoringResultVOPage(Page<ScoringResult> scoringResultPage, HttpServletRequest request);\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/service/UserAnswerService.java",
    "content": "package com.yupi.yudada.service;\n\nimport com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.baomidou.mybatisplus.extension.service.IService;\nimport com.yupi.yudada.model.dto.userAnswer.UserAnswerQueryRequest;\nimport com.yupi.yudada.model.entity.UserAnswer;\nimport com.yupi.yudada.model.vo.UserAnswerVO;\n\nimport javax.servlet.http.HttpServletRequest;\n\n/**\n * 用户答案服务\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\npublic interface UserAnswerService extends IService<UserAnswer> {\n\n    /**\n     * 校验数据\n     *\n     * @param userAnswer\n     * @param add 对创建的数据进行校验\n     */\n    void validUserAnswer(UserAnswer userAnswer, boolean add);\n\n    /**\n     * 获取查询条件\n     *\n     * @param userAnswerQueryRequest\n     * @return\n     */\n    QueryWrapper<UserAnswer> getQueryWrapper(UserAnswerQueryRequest userAnswerQueryRequest);\n    \n    /**\n     * 获取用户答案封装\n     *\n     * @param userAnswer\n     * @param request\n     * @return\n     */\n    UserAnswerVO getUserAnswerVO(UserAnswer userAnswer, HttpServletRequest request);\n\n    /**\n     * 分页获取用户答案封装\n     *\n     * @param userAnswerPage\n     * @param request\n     * @return\n     */\n    Page<UserAnswerVO> getUserAnswerVOPage(Page<UserAnswer> userAnswerPage, HttpServletRequest request);\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/service/UserService.java",
    "content": "package com.yupi.yudada.service;\n\nimport com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;\nimport com.baomidou.mybatisplus.extension.service.IService;\nimport com.yupi.yudada.model.dto.user.UserQueryRequest;\nimport com.yupi.yudada.model.entity.User;\nimport com.yupi.yudada.model.vo.LoginUserVO;\nimport com.yupi.yudada.model.vo.UserVO;\nimport java.util.List;\nimport javax.servlet.http.HttpServletRequest;\n\n/**\n * 用户服务\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\npublic interface UserService extends IService<User> {\n\n    /**\n     * 用户注册\n     *\n     * @param userAccount   用户账户\n     * @param userPassword  用户密码\n     * @param checkPassword 校验密码\n     * @return 新用户 id\n     */\n    long userRegister(String userAccount, String userPassword, String checkPassword);\n\n    /**\n     * 用户登录\n     *\n     * @param userAccount  用户账户\n     * @param userPassword 用户密码\n     * @param request\n     * @return 脱敏后的用户信息\n     */\n    LoginUserVO userLogin(String userAccount, String userPassword, HttpServletRequest request);\n\n    /**\n     * 获取当前登录用户\n     *\n     * @param request\n     * @return\n     */\n    User getLoginUser(HttpServletRequest request);\n\n    /**\n     * 获取当前登录用户（允许未登录）\n     *\n     * @param request\n     * @return\n     */\n    User getLoginUserPermitNull(HttpServletRequest request);\n\n    /**\n     * 是否为管理员\n     *\n     * @param request\n     * @return\n     */\n    boolean isAdmin(HttpServletRequest request);\n\n    /**\n     * 是否为管理员\n     *\n     * @param user\n     * @return\n     */\n    boolean isAdmin(User user);\n\n    /**\n     * 用户注销\n     *\n     * @param request\n     * @return\n     */\n    boolean userLogout(HttpServletRequest request);\n\n    /**\n     * 获取脱敏的已登录用户信息\n     *\n     * @return\n     */\n    LoginUserVO getLoginUserVO(User user);\n\n    /**\n     * 获取脱敏的用户信息\n     *\n     * @param user\n     * @return\n     */\n    UserVO getUserVO(User user);\n\n    /**\n     * 获取脱敏的用户信息\n     *\n     * @param userList\n     * @return\n     */\n    List<UserVO> getUserVO(List<User> userList);\n\n    /**\n     * 获取查询条件\n     *\n     * @param userQueryRequest\n     * @return\n     */\n    QueryWrapper<User> getQueryWrapper(UserQueryRequest userQueryRequest);\n\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/service/impl/AppServiceImpl.java",
    "content": "package com.yupi.yudada.service.impl;\n\nimport cn.hutool.core.collection.CollUtil;\nimport com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;\nimport com.yupi.yudada.common.ErrorCode;\nimport com.yupi.yudada.constant.CommonConstant;\nimport com.yupi.yudada.exception.ThrowUtils;\nimport com.yupi.yudada.mapper.AppMapper;\nimport com.yupi.yudada.model.dto.app.AppQueryRequest;\nimport com.yupi.yudada.model.entity.App;\nimport com.yupi.yudada.model.entity.User;\nimport com.yupi.yudada.model.enums.AppScoringStrategyEnum;\nimport com.yupi.yudada.model.enums.AppTypeEnum;\nimport com.yupi.yudada.model.enums.ReviewStatusEnum;\nimport com.yupi.yudada.model.vo.AppVO;\nimport com.yupi.yudada.model.vo.UserVO;\nimport com.yupi.yudada.service.AppService;\nimport com.yupi.yudada.service.UserService;\nimport com.yupi.yudada.utils.SqlUtils;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.ObjectUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.stereotype.Service;\n\nimport javax.annotation.Resource;\nimport javax.servlet.http.HttpServletRequest;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\n/**\n * 应用服务实现\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Service\n@Slf4j\npublic class AppServiceImpl extends ServiceImpl<AppMapper, App> implements AppService {\n\n    @Resource\n    private UserService userService;\n\n    /**\n     * 校验数据\n     *\n     * @param app\n     * @param add 对创建的数据进行校验\n     */\n    @Override\n    public void validApp(App app, boolean add) {\n        ThrowUtils.throwIf(app == null, ErrorCode.PARAMS_ERROR);\n        // 从对象中取值\n        String appName = app.getAppName();\n        String appDesc = app.getAppDesc();\n        Integer appType = app.getAppType();\n        Integer scoringStrategy = app.getScoringStrategy();\n        Integer reviewStatus = app.getReviewStatus();\n\n        // 创建数据时，参数不能为空\n        if (add) {\n            // 补充校验规则\n            ThrowUtils.throwIf(StringUtils.isBlank(appName), ErrorCode.PARAMS_ERROR, \"应用名称不能为空\");\n            ThrowUtils.throwIf(StringUtils.isBlank(appDesc), ErrorCode.PARAMS_ERROR, \"应用描述不能为空\");\n            AppTypeEnum appTypeEnum = AppTypeEnum.getEnumByValue(appType);\n            ThrowUtils.throwIf(appTypeEnum == null, ErrorCode.PARAMS_ERROR, \"应用类别非法\");\n            AppScoringStrategyEnum scoringStrategyEnum = AppScoringStrategyEnum.getEnumByValue(scoringStrategy);\n            ThrowUtils.throwIf(scoringStrategyEnum == null, ErrorCode.PARAMS_ERROR, \"应用评分策略非法\");\n        }\n        // 修改数据时，有参数则校验\n        // 补充校验规则\n        if (StringUtils.isNotBlank(appName)) {\n            ThrowUtils.throwIf(appName.length() > 80, ErrorCode.PARAMS_ERROR, \"应用名称要小于 80\");\n        }\n        if (reviewStatus != null) {\n            ReviewStatusEnum reviewStatusEnum = ReviewStatusEnum.getEnumByValue(reviewStatus);\n            ThrowUtils.throwIf(reviewStatusEnum == null, ErrorCode.PARAMS_ERROR, \"审核状态非法\");\n        }\n    }\n\n    /**\n     * 获取查询条件\n     *\n     * @param appQueryRequest\n     * @return\n     */\n    @Override\n    public QueryWrapper<App> getQueryWrapper(AppQueryRequest appQueryRequest) {\n        QueryWrapper<App> queryWrapper = new QueryWrapper<>();\n        if (appQueryRequest == null) {\n            return queryWrapper;\n        }\n        // 从对象中取值\n        Long id = appQueryRequest.getId();\n        String appName = appQueryRequest.getAppName();\n        String appDesc = appQueryRequest.getAppDesc();\n        String appIcon = appQueryRequest.getAppIcon();\n        Integer appType = appQueryRequest.getAppType();\n        Integer scoringStrategy = appQueryRequest.getScoringStrategy();\n        Integer reviewStatus = appQueryRequest.getReviewStatus();\n        String reviewMessage = appQueryRequest.getReviewMessage();\n        Long reviewerId = appQueryRequest.getReviewerId();\n        Long userId = appQueryRequest.getUserId();\n        Long notId = appQueryRequest.getNotId();\n        String searchText = appQueryRequest.getSearchText();\n        String sortField = appQueryRequest.getSortField();\n        String sortOrder = appQueryRequest.getSortOrder();\n\n        // 补充需要的查询条件\n        // 从多字段中搜索\n        if (StringUtils.isNotBlank(searchText)) {\n            // 需要拼接查询条件\n            queryWrapper.and(qw -> qw.like(\"appName\", searchText).or().like(\"appDesc\", searchText));\n        }\n        // 模糊查询\n        queryWrapper.like(StringUtils.isNotBlank(appName), \"appName\", appName);\n        queryWrapper.like(StringUtils.isNotBlank(appDesc), \"appDesc\", appDesc);\n        queryWrapper.like(StringUtils.isNotBlank(reviewMessage), \"reviewMessage\", reviewMessage);\n        // 精确查询\n        queryWrapper.eq(StringUtils.isNotBlank(appIcon), \"appIcon\", appIcon);\n        queryWrapper.ne(ObjectUtils.isNotEmpty(notId), \"id\", notId);\n        queryWrapper.eq(ObjectUtils.isNotEmpty(id), \"id\", id);\n        queryWrapper.eq(ObjectUtils.isNotEmpty(appType), \"appType\", appType);\n        queryWrapper.eq(ObjectUtils.isNotEmpty(scoringStrategy), \"scoringStrategy\", scoringStrategy);\n        queryWrapper.eq(ObjectUtils.isNotEmpty(reviewStatus), \"reviewStatus\", reviewStatus);\n        queryWrapper.eq(ObjectUtils.isNotEmpty(reviewerId), \"reviewerId\", reviewerId);\n        queryWrapper.eq(ObjectUtils.isNotEmpty(userId), \"userId\", userId);\n        // 排序规则\n        queryWrapper.orderBy(SqlUtils.validSortField(sortField),\n                sortOrder.equals(CommonConstant.SORT_ORDER_ASC),\n                sortField);\n        return queryWrapper;\n    }\n\n    /**\n     * 获取应用封装\n     *\n     * @param app\n     * @param request\n     * @return\n     */\n    @Override\n    public AppVO getAppVO(App app, HttpServletRequest request) {\n        // 对象转封装类\n        AppVO appVO = AppVO.objToVo(app);\n\n        // 可以根据需要为封装对象补充值，不需要的内容可以删除\n        // region 可选\n        // 1. 关联查询用户信息\n        Long userId = app.getUserId();\n        User user = null;\n        if (userId != null && userId > 0) {\n            user = userService.getById(userId);\n        }\n        UserVO userVO = userService.getUserVO(user);\n        appVO.setUser(userVO);\n        // endregion\n        return appVO;\n    }\n\n    /**\n     * 分页获取应用封装\n     *\n     * @param appPage\n     * @param request\n     * @return\n     */\n    @Override\n    public Page<AppVO> getAppVOPage(Page<App> appPage, HttpServletRequest request) {\n        List<App> appList = appPage.getRecords();\n        Page<AppVO> appVOPage = new Page<>(appPage.getCurrent(), appPage.getSize(), appPage.getTotal());\n        if (CollUtil.isEmpty(appList)) {\n            return appVOPage;\n        }\n        // 对象列表 => 封装对象列表\n        List<AppVO> appVOList = appList.stream().map(app -> {\n            return AppVO.objToVo(app);\n        }).collect(Collectors.toList());\n\n        // 可以根据需要为封装对象补充值，不需要的内容可以删除\n        // region 可选\n        // 1. 关联查询用户信息\n        Set<Long> userIdSet = appList.stream().map(App::getUserId).collect(Collectors.toSet());\n        Map<Long, List<User>> userIdUserListMap = userService.listByIds(userIdSet).stream()\n                .collect(Collectors.groupingBy(User::getId));\n        // 填充信息\n        appVOList.forEach(appVO -> {\n            Long userId = appVO.getUserId();\n            User user = null;\n            if (userIdUserListMap.containsKey(userId)) {\n                user = userIdUserListMap.get(userId).get(0);\n            }\n            appVO.setUser(userService.getUserVO(user));\n        });\n        // endregion\n\n        appVOPage.setRecords(appVOList);\n        return appVOPage;\n    }\n\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/service/impl/PostFavourServiceImpl.java",
    "content": "package com.yupi.yudada.service.impl;\n\nimport com.baomidou.mybatisplus.core.conditions.Wrapper;\nimport com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;\nimport com.baomidou.mybatisplus.core.metadata.IPage;\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;\nimport com.yupi.yudada.common.ErrorCode;\nimport com.yupi.yudada.exception.BusinessException;\nimport com.yupi.yudada.mapper.PostFavourMapper;\nimport com.yupi.yudada.model.entity.Post;\nimport com.yupi.yudada.model.entity.PostFavour;\nimport com.yupi.yudada.model.entity.User;\nimport com.yupi.yudada.service.PostFavourService;\nimport com.yupi.yudada.service.PostService;\nimport javax.annotation.Resource;\nimport org.springframework.aop.framework.AopContext;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.annotation.Transactional;\n\n/**\n * 帖子收藏服务实现\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Service\npublic class PostFavourServiceImpl extends ServiceImpl<PostFavourMapper, PostFavour>\n        implements PostFavourService {\n\n    @Resource\n    private PostService postService;\n\n    /**\n     * 帖子收藏\n     *\n     * @param postId\n     * @param loginUser\n     * @return\n     */\n    @Override\n    public int doPostFavour(long postId, User loginUser) {\n        // 判断是否存在\n        Post post = postService.getById(postId);\n        if (post == null) {\n            throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);\n        }\n        // 是否已帖子收藏\n        long userId = loginUser.getId();\n        // 每个用户串行帖子收藏\n        // 锁必须要包裹住事务方法\n        PostFavourService postFavourService = (PostFavourService) AopContext.currentProxy();\n        synchronized (String.valueOf(userId).intern()) {\n            return postFavourService.doPostFavourInner(userId, postId);\n        }\n    }\n\n    @Override\n    public Page<Post> listFavourPostByPage(IPage<Post> page, Wrapper<Post> queryWrapper, long favourUserId) {\n        if (favourUserId <= 0) {\n            return new Page<>();\n        }\n        return baseMapper.listFavourPostByPage(page, queryWrapper, favourUserId);\n    }\n\n    /**\n     * 封装了事务的方法\n     *\n     * @param userId\n     * @param postId\n     * @return\n     */\n    @Override\n    @Transactional(rollbackFor = Exception.class)\n    public int doPostFavourInner(long userId, long postId) {\n        PostFavour postFavour = new PostFavour();\n        postFavour.setUserId(userId);\n        postFavour.setPostId(postId);\n        QueryWrapper<PostFavour> postFavourQueryWrapper = new QueryWrapper<>(postFavour);\n        PostFavour oldPostFavour = this.getOne(postFavourQueryWrapper);\n        boolean result;\n        // 已收藏\n        if (oldPostFavour != null) {\n            result = this.remove(postFavourQueryWrapper);\n            if (result) {\n                // 帖子收藏数 - 1\n                result = postService.update()\n                        .eq(\"id\", postId)\n                        .gt(\"favourNum\", 0)\n                        .setSql(\"favourNum = favourNum - 1\")\n                        .update();\n                return result ? -1 : 0;\n            } else {\n                throw new BusinessException(ErrorCode.SYSTEM_ERROR);\n            }\n        } else {\n            // 未帖子收藏\n            result = this.save(postFavour);\n            if (result) {\n                // 帖子收藏数 + 1\n                result = postService.update()\n                        .eq(\"id\", postId)\n                        .setSql(\"favourNum = favourNum + 1\")\n                        .update();\n                return result ? 1 : 0;\n            } else {\n                throw new BusinessException(ErrorCode.SYSTEM_ERROR);\n            }\n        }\n    }\n\n}\n\n\n\n\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/service/impl/PostServiceImpl.java",
    "content": "package com.yupi.yudada.service.impl;\n\nimport com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;\nimport com.yupi.yudada.common.ErrorCode;\nimport com.yupi.yudada.constant.CommonConstant;\nimport com.yupi.yudada.exception.BusinessException;\nimport com.yupi.yudada.exception.ThrowUtils;\nimport com.yupi.yudada.mapper.PostFavourMapper;\nimport com.yupi.yudada.mapper.PostMapper;\nimport com.yupi.yudada.mapper.PostThumbMapper;\nimport com.yupi.yudada.model.dto.post.PostQueryRequest;\nimport com.yupi.yudada.model.entity.Post;\nimport com.yupi.yudada.model.entity.PostFavour;\nimport com.yupi.yudada.model.entity.PostThumb;\nimport com.yupi.yudada.model.entity.User;\nimport com.yupi.yudada.model.vo.PostVO;\nimport com.yupi.yudada.model.vo.UserVO;\nimport com.yupi.yudada.service.PostService;\nimport com.yupi.yudada.service.UserService;\nimport com.yupi.yudada.utils.SqlUtils;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport javax.annotation.Resource;\nimport javax.servlet.http.HttpServletRequest;\nimport lombok.extern.slf4j.Slf4j;\nimport cn.hutool.core.collection.CollUtil;\nimport org.apache.commons.lang3.ObjectUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.stereotype.Service;\n\n/**\n * 帖子服务实现\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Service\n@Slf4j\npublic class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements PostService {\n\n    @Resource\n    private UserService userService;\n\n    @Resource\n    private PostThumbMapper postThumbMapper;\n\n    @Resource\n    private PostFavourMapper postFavourMapper;\n\n    @Override\n    public void validPost(Post post, boolean add) {\n        if (post == null) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        String title = post.getTitle();\n        String content = post.getContent();\n        String tags = post.getTags();\n        // 创建时，参数不能为空\n        if (add) {\n            ThrowUtils.throwIf(StringUtils.isAnyBlank(title, content, tags), ErrorCode.PARAMS_ERROR);\n        }\n        // 有参数则校验\n        if (StringUtils.isNotBlank(title) && title.length() > 80) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR, \"标题过长\");\n        }\n        if (StringUtils.isNotBlank(content) && content.length() > 8192) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR, \"内容过长\");\n        }\n    }\n\n    /**\n     * 获取查询包装类\n     *\n     * @param postQueryRequest\n     * @return\n     */\n    @Override\n    public QueryWrapper<Post> getQueryWrapper(PostQueryRequest postQueryRequest) {\n        QueryWrapper<Post> queryWrapper = new QueryWrapper<>();\n        if (postQueryRequest == null) {\n            return queryWrapper;\n        }\n        String searchText = postQueryRequest.getSearchText();\n        String sortField = postQueryRequest.getSortField();\n        String sortOrder = postQueryRequest.getSortOrder();\n        Long id = postQueryRequest.getId();\n        String title = postQueryRequest.getTitle();\n        String content = postQueryRequest.getContent();\n        List<String> tagList = postQueryRequest.getTags();\n        Long userId = postQueryRequest.getUserId();\n        Long notId = postQueryRequest.getNotId();\n        // 拼接查询条件\n        if (StringUtils.isNotBlank(searchText)) {\n            queryWrapper.and(qw -> qw.like(\"title\", searchText).or().like(\"content\", searchText));\n        }\n        queryWrapper.like(StringUtils.isNotBlank(title), \"title\", title);\n        queryWrapper.like(StringUtils.isNotBlank(content), \"content\", content);\n        if (CollUtil.isNotEmpty(tagList)) {\n            for (String tag : tagList) {\n                queryWrapper.like(\"tags\", \"\\\"\" + tag + \"\\\"\");\n            }\n        }\n        queryWrapper.ne(ObjectUtils.isNotEmpty(notId), \"id\", notId);\n        queryWrapper.eq(ObjectUtils.isNotEmpty(id), \"id\", id);\n        queryWrapper.eq(ObjectUtils.isNotEmpty(userId), \"userId\", userId);\n        queryWrapper.orderBy(SqlUtils.validSortField(sortField), sortOrder.equals(CommonConstant.SORT_ORDER_ASC),\n                sortField);\n        return queryWrapper;\n    }\n\n    @Override\n    public PostVO getPostVO(Post post, HttpServletRequest request) {\n        PostVO postVO = PostVO.objToVo(post);\n        long postId = post.getId();\n        // 1. 关联查询用户信息\n        Long userId = post.getUserId();\n        User user = null;\n        if (userId != null && userId > 0) {\n            user = userService.getById(userId);\n        }\n        UserVO userVO = userService.getUserVO(user);\n        postVO.setUser(userVO);\n        // 2. 已登录，获取用户点赞、收藏状态\n        User loginUser = userService.getLoginUserPermitNull(request);\n        if (loginUser != null) {\n            // 获取点赞\n            QueryWrapper<PostThumb> postThumbQueryWrapper = new QueryWrapper<>();\n            postThumbQueryWrapper.in(\"postId\", postId);\n            postThumbQueryWrapper.eq(\"userId\", loginUser.getId());\n            PostThumb postThumb = postThumbMapper.selectOne(postThumbQueryWrapper);\n            postVO.setHasThumb(postThumb != null);\n            // 获取收藏\n            QueryWrapper<PostFavour> postFavourQueryWrapper = new QueryWrapper<>();\n            postFavourQueryWrapper.in(\"postId\", postId);\n            postFavourQueryWrapper.eq(\"userId\", loginUser.getId());\n            PostFavour postFavour = postFavourMapper.selectOne(postFavourQueryWrapper);\n            postVO.setHasFavour(postFavour != null);\n        }\n        return postVO;\n    }\n\n    @Override\n    public Page<PostVO> getPostVOPage(Page<Post> postPage, HttpServletRequest request) {\n        List<Post> postList = postPage.getRecords();\n        Page<PostVO> postVOPage = new Page<>(postPage.getCurrent(), postPage.getSize(), postPage.getTotal());\n        if (CollUtil.isEmpty(postList)) {\n            return postVOPage;\n        }\n        // 1. 关联查询用户信息\n        Set<Long> userIdSet = postList.stream().map(Post::getUserId).collect(Collectors.toSet());\n        Map<Long, List<User>> userIdUserListMap = userService.listByIds(userIdSet).stream()\n                .collect(Collectors.groupingBy(User::getId));\n        // 2. 已登录，获取用户点赞、收藏状态\n        Map<Long, Boolean> postIdHasThumbMap = new HashMap<>();\n        Map<Long, Boolean> postIdHasFavourMap = new HashMap<>();\n        User loginUser = userService.getLoginUserPermitNull(request);\n        if (loginUser != null) {\n            Set<Long> postIdSet = postList.stream().map(Post::getId).collect(Collectors.toSet());\n            loginUser = userService.getLoginUser(request);\n            // 获取点赞\n            QueryWrapper<PostThumb> postThumbQueryWrapper = new QueryWrapper<>();\n            postThumbQueryWrapper.in(\"postId\", postIdSet);\n            postThumbQueryWrapper.eq(\"userId\", loginUser.getId());\n            List<PostThumb> postPostThumbList = postThumbMapper.selectList(postThumbQueryWrapper);\n            postPostThumbList.forEach(postPostThumb -> postIdHasThumbMap.put(postPostThumb.getPostId(), true));\n            // 获取收藏\n            QueryWrapper<PostFavour> postFavourQueryWrapper = new QueryWrapper<>();\n            postFavourQueryWrapper.in(\"postId\", postIdSet);\n            postFavourQueryWrapper.eq(\"userId\", loginUser.getId());\n            List<PostFavour> postFavourList = postFavourMapper.selectList(postFavourQueryWrapper);\n            postFavourList.forEach(postFavour -> postIdHasFavourMap.put(postFavour.getPostId(), true));\n        }\n        // 填充信息\n        List<PostVO> postVOList = postList.stream().map(post -> {\n            PostVO postVO = PostVO.objToVo(post);\n            Long userId = post.getUserId();\n            User user = null;\n            if (userIdUserListMap.containsKey(userId)) {\n                user = userIdUserListMap.get(userId).get(0);\n            }\n            postVO.setUser(userService.getUserVO(user));\n            postVO.setHasThumb(postIdHasThumbMap.getOrDefault(post.getId(), false));\n            postVO.setHasFavour(postIdHasFavourMap.getOrDefault(post.getId(), false));\n            return postVO;\n        }).collect(Collectors.toList());\n        postVOPage.setRecords(postVOList);\n        return postVOPage;\n    }\n\n}\n\n\n\n\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/service/impl/PostThumbServiceImpl.java",
    "content": "package com.yupi.yudada.service.impl;\n\nimport com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;\nimport com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;\nimport com.yupi.yudada.common.ErrorCode;\nimport com.yupi.yudada.exception.BusinessException;\nimport com.yupi.yudada.mapper.PostThumbMapper;\nimport com.yupi.yudada.model.entity.Post;\nimport com.yupi.yudada.model.entity.PostThumb;\nimport com.yupi.yudada.model.entity.User;\nimport com.yupi.yudada.service.PostService;\nimport com.yupi.yudada.service.PostThumbService;\nimport javax.annotation.Resource;\nimport org.springframework.aop.framework.AopContext;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.annotation.Transactional;\n\n/**\n * 帖子点赞服务实现\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Service\npublic class PostThumbServiceImpl extends ServiceImpl<PostThumbMapper, PostThumb>\n        implements PostThumbService {\n\n    @Resource\n    private PostService postService;\n\n    /**\n     * 点赞\n     *\n     * @param postId\n     * @param loginUser\n     * @return\n     */\n    @Override\n    public int doPostThumb(long postId, User loginUser) {\n        // 判断实体是否存在，根据类别获取实体\n        Post post = postService.getById(postId);\n        if (post == null) {\n            throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);\n        }\n        // 是否已点赞\n        long userId = loginUser.getId();\n        // 每个用户串行点赞\n        // 锁必须要包裹住事务方法\n        PostThumbService postThumbService = (PostThumbService) AopContext.currentProxy();\n        synchronized (String.valueOf(userId).intern()) {\n            return postThumbService.doPostThumbInner(userId, postId);\n        }\n    }\n\n    /**\n     * 封装了事务的方法\n     *\n     * @param userId\n     * @param postId\n     * @return\n     */\n    @Override\n    @Transactional(rollbackFor = Exception.class)\n    public int doPostThumbInner(long userId, long postId) {\n        PostThumb postThumb = new PostThumb();\n        postThumb.setUserId(userId);\n        postThumb.setPostId(postId);\n        QueryWrapper<PostThumb> thumbQueryWrapper = new QueryWrapper<>(postThumb);\n        PostThumb oldPostThumb = this.getOne(thumbQueryWrapper);\n        boolean result;\n        // 已点赞\n        if (oldPostThumb != null) {\n            result = this.remove(thumbQueryWrapper);\n            if (result) {\n                // 点赞数 - 1\n                result = postService.update()\n                        .eq(\"id\", postId)\n                        .gt(\"thumbNum\", 0)\n                        .setSql(\"thumbNum = thumbNum - 1\")\n                        .update();\n                return result ? -1 : 0;\n            } else {\n                throw new BusinessException(ErrorCode.SYSTEM_ERROR);\n            }\n        } else {\n            // 未点赞\n            result = this.save(postThumb);\n            if (result) {\n                // 点赞数 + 1\n                result = postService.update()\n                        .eq(\"id\", postId)\n                        .setSql(\"thumbNum = thumbNum + 1\")\n                        .update();\n                return result ? 1 : 0;\n            } else {\n                throw new BusinessException(ErrorCode.SYSTEM_ERROR);\n            }\n        }\n    }\n\n}\n\n\n\n\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/service/impl/QuestionServiceImpl.java",
    "content": "package com.yupi.yudada.service.impl;\n\nimport cn.hutool.core.collection.CollUtil;\nimport com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;\nimport com.yupi.yudada.common.ErrorCode;\nimport com.yupi.yudada.constant.CommonConstant;\nimport com.yupi.yudada.exception.ThrowUtils;\nimport com.yupi.yudada.mapper.QuestionMapper;\nimport com.yupi.yudada.model.dto.question.QuestionQueryRequest;\nimport com.yupi.yudada.model.entity.App;\nimport com.yupi.yudada.model.entity.Question;\nimport com.yupi.yudada.model.entity.User;\nimport com.yupi.yudada.model.vo.QuestionVO;\nimport com.yupi.yudada.model.vo.UserVO;\nimport com.yupi.yudada.service.AppService;\nimport com.yupi.yudada.service.QuestionService;\nimport com.yupi.yudada.service.UserService;\nimport com.yupi.yudada.utils.SqlUtils;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.ObjectUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.stereotype.Service;\n\nimport javax.annotation.Resource;\nimport javax.servlet.http.HttpServletRequest;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\n/**\n * 题目服务实现\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Service\n@Slf4j\npublic class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements QuestionService {\n\n    @Resource\n    private UserService userService;\n\n    @Resource\n    private AppService appService;\n\n    /**\n     * 校验数据\n     *\n     * @param question\n     * @param add      对创建的数据进行校验\n     */\n    @Override\n    public void validQuestion(Question question, boolean add) {\n        ThrowUtils.throwIf(question == null, ErrorCode.PARAMS_ERROR);\n        // 从对象中取值\n        String questionContent = question.getQuestionContent();\n        Long appId = question.getAppId();\n        // 创建数据时，参数不能为空\n        if (add) {\n            // 补充校验规则\n            ThrowUtils.throwIf(StringUtils.isBlank(questionContent), ErrorCode.PARAMS_ERROR, \"题目内容不能为空\");\n            ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR, \"appId 非法\");\n        }\n        // 修改数据时，有参数则校验\n        // 补充校验规则\n        if (appId != null) {\n            App app = appService.getById(appId);\n            ThrowUtils.throwIf(app == null, ErrorCode.PARAMS_ERROR, \"应用不存在\");\n        }\n    }\n\n    /**\n     * 获取查询条件\n     *\n     * @param questionQueryRequest\n     * @return\n     */\n    @Override\n    public QueryWrapper<Question> getQueryWrapper(QuestionQueryRequest questionQueryRequest) {\n        QueryWrapper<Question> queryWrapper = new QueryWrapper<>();\n        if (questionQueryRequest == null) {\n            return queryWrapper;\n        }\n        // 从对象中取值\n        Long id = questionQueryRequest.getId();\n        String questionContent = questionQueryRequest.getQuestionContent();\n        Long appId = questionQueryRequest.getAppId();\n        Long userId = questionQueryRequest.getUserId();\n        Long notId = questionQueryRequest.getNotId();\n        String sortField = questionQueryRequest.getSortField();\n        String sortOrder = questionQueryRequest.getSortOrder();\n\n        // 补充需要的查询条件\n        // 模糊查询\n        queryWrapper.like(StringUtils.isNotBlank(questionContent), \"questionContent\", questionContent);\n        // 精确查询\n        queryWrapper.ne(ObjectUtils.isNotEmpty(notId), \"id\", notId);\n        queryWrapper.eq(ObjectUtils.isNotEmpty(id), \"id\", id);\n        queryWrapper.eq(ObjectUtils.isNotEmpty(appId), \"appId\", appId);\n        queryWrapper.eq(ObjectUtils.isNotEmpty(userId), \"userId\", userId);\n        // 排序规则\n        queryWrapper.orderBy(SqlUtils.validSortField(sortField),\n                sortOrder.equals(CommonConstant.SORT_ORDER_ASC),\n                sortField);\n        return queryWrapper;\n    }\n\n    /**\n     * 获取题目封装\n     *\n     * @param question\n     * @param request\n     * @return\n     */\n    @Override\n    public QuestionVO getQuestionVO(Question question, HttpServletRequest request) {\n        // 对象转封装类\n        QuestionVO questionVO = QuestionVO.objToVo(question);\n\n        // 可以根据需要为封装对象补充值，不需要的内容可以删除\n        // region 可选\n        // 1. 关联查询用户信息\n        Long userId = question.getUserId();\n        User user = null;\n        if (userId != null && userId > 0) {\n            user = userService.getById(userId);\n        }\n        UserVO userVO = userService.getUserVO(user);\n        questionVO.setUser(userVO);\n        // endregion\n\n        return questionVO;\n    }\n\n    /**\n     * 分页获取题目封装\n     *\n     * @param questionPage\n     * @param request\n     * @return\n     */\n    @Override\n    public Page<QuestionVO> getQuestionVOPage(Page<Question> questionPage, HttpServletRequest request) {\n        List<Question> questionList = questionPage.getRecords();\n        Page<QuestionVO> questionVOPage = new Page<>(questionPage.getCurrent(), questionPage.getSize(), questionPage.getTotal());\n        if (CollUtil.isEmpty(questionList)) {\n            return questionVOPage;\n        }\n        // 对象列表 => 封装对象列表\n        List<QuestionVO> questionVOList = questionList.stream().map(question -> {\n            return QuestionVO.objToVo(question);\n        }).collect(Collectors.toList());\n\n        // 可以根据需要为封装对象补充值，不需要的内容可以删除\n        // region 可选\n        // 1. 关联查询用户信息\n        Set<Long> userIdSet = questionList.stream().map(Question::getUserId).collect(Collectors.toSet());\n        Map<Long, List<User>> userIdUserListMap = userService.listByIds(userIdSet).stream()\n                .collect(Collectors.groupingBy(User::getId));\n        // 填充信息\n        questionVOList.forEach(questionVO -> {\n            Long userId = questionVO.getUserId();\n            User user = null;\n            if (userIdUserListMap.containsKey(userId)) {\n                user = userIdUserListMap.get(userId).get(0);\n            }\n            questionVO.setUser(userService.getUserVO(user));\n        });\n        // endregion\n\n        questionVOPage.setRecords(questionVOList);\n        return questionVOPage;\n    }\n\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/service/impl/ScoringResultServiceImpl.java",
    "content": "package com.yupi.yudada.service.impl;\n\nimport cn.hutool.core.collection.CollUtil;\nimport com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;\nimport com.yupi.yudada.common.ErrorCode;\nimport com.yupi.yudada.constant.CommonConstant;\nimport com.yupi.yudada.exception.ThrowUtils;\nimport com.yupi.yudada.mapper.ScoringResultMapper;\nimport com.yupi.yudada.model.dto.scoringResult.ScoringResultQueryRequest;\nimport com.yupi.yudada.model.entity.App;\nimport com.yupi.yudada.model.entity.ScoringResult;\nimport com.yupi.yudada.model.entity.User;\nimport com.yupi.yudada.model.vo.ScoringResultVO;\nimport com.yupi.yudada.model.vo.UserVO;\nimport com.yupi.yudada.service.AppService;\nimport com.yupi.yudada.service.ScoringResultService;\nimport com.yupi.yudada.service.UserService;\nimport com.yupi.yudada.utils.SqlUtils;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.ObjectUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.stereotype.Service;\n\nimport javax.annotation.Resource;\nimport javax.servlet.http.HttpServletRequest;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\n/**\n * 评分结果服务实现\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Service\n@Slf4j\npublic class ScoringResultServiceImpl extends ServiceImpl<ScoringResultMapper, ScoringResult> implements ScoringResultService {\n\n    @Resource\n    private UserService userService;\n\n    @Resource\n    private AppService appService;\n\n    /**\n     * 校验数据\n     *\n     * @param scoringResult\n     * @param add           对创建的数据进行校验\n     */\n    @Override\n    public void validScoringResult(ScoringResult scoringResult, boolean add) {\n        ThrowUtils.throwIf(scoringResult == null, ErrorCode.PARAMS_ERROR);\n        // 从对象中取值\n        String resultName = scoringResult.getResultName();\n        Long appId = scoringResult.getAppId();\n        // 创建数据时，参数不能为空\n        if (add) {\n            // 补充校验规则\n            ThrowUtils.throwIf(StringUtils.isBlank(resultName), ErrorCode.PARAMS_ERROR, \"结果名称不能为空\");\n            ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR, \"appId 非法\");\n        }\n        // 修改数据时，有参数则校验\n        // 补充校验规则\n        if (StringUtils.isNotBlank(resultName)) {\n            ThrowUtils.throwIf(resultName.length() > 128, ErrorCode.PARAMS_ERROR, \"结果名称不能超过 128\");\n        }\n        // 补充校验规则\n        if (appId != null) {\n            App app = appService.getById(appId);\n            ThrowUtils.throwIf(app == null, ErrorCode.PARAMS_ERROR, \"应用不存在\");\n        }\n    }\n\n    /**\n     * 获取查询条件\n     *\n     * @param scoringResultQueryRequest\n     * @return\n     */\n    @Override\n    public QueryWrapper<ScoringResult> getQueryWrapper(ScoringResultQueryRequest scoringResultQueryRequest) {\n        QueryWrapper<ScoringResult> queryWrapper = new QueryWrapper<>();\n        if (scoringResultQueryRequest == null) {\n            return queryWrapper;\n        }\n        // 从对象中取值\n        Long id = scoringResultQueryRequest.getId();\n        String resultName = scoringResultQueryRequest.getResultName();\n        String resultDesc = scoringResultQueryRequest.getResultDesc();\n        String resultPicture = scoringResultQueryRequest.getResultPicture();\n        String resultProp = scoringResultQueryRequest.getResultProp();\n        Integer resultScoreRange = scoringResultQueryRequest.getResultScoreRange();\n        Long appId = scoringResultQueryRequest.getAppId();\n        Long userId = scoringResultQueryRequest.getUserId();\n        Long notId = scoringResultQueryRequest.getNotId();\n        String searchText = scoringResultQueryRequest.getSearchText();\n        String sortField = scoringResultQueryRequest.getSortField();\n        String sortOrder = scoringResultQueryRequest.getSortOrder();\n\n        // 补充需要的查询条件\n        // 从多字段中搜索\n        if (StringUtils.isNotBlank(searchText)) {\n            // 需要拼接查询条件\n            queryWrapper.and(qw -> qw.like(\"resultName\", searchText).or().like(\"resultDesc\", searchText));\n        }\n        // 模糊查询\n        queryWrapper.like(StringUtils.isNotBlank(resultName), \"resultName\", resultName);\n        queryWrapper.like(StringUtils.isNotBlank(resultDesc), \"resultDesc\", resultDesc);\n        queryWrapper.like(StringUtils.isNotBlank(resultProp), \"resultProp\", resultProp);\n        // 精确查询\n        queryWrapper.ne(ObjectUtils.isNotEmpty(notId), \"id\", notId);\n        queryWrapper.eq(ObjectUtils.isNotEmpty(id), \"id\", id);\n        queryWrapper.eq(ObjectUtils.isNotEmpty(userId), \"userId\", userId);\n        queryWrapper.eq(ObjectUtils.isNotEmpty(appId), \"appId\", appId);\n        queryWrapper.eq(ObjectUtils.isNotEmpty(resultScoreRange), \"resultScoreRange\", resultScoreRange);\n        queryWrapper.eq(StringUtils.isNotBlank(resultPicture), \"resultPicture\", resultPicture);\n        // 排序规则\n        queryWrapper.orderBy(SqlUtils.validSortField(sortField),\n                sortOrder.equals(CommonConstant.SORT_ORDER_ASC),\n                sortField);\n        return queryWrapper;\n    }\n\n    /**\n     * 获取评分结果封装\n     *\n     * @param scoringResult\n     * @param request\n     * @return\n     */\n    @Override\n    public ScoringResultVO getScoringResultVO(ScoringResult scoringResult, HttpServletRequest request) {\n        // 对象转封装类\n        ScoringResultVO scoringResultVO = ScoringResultVO.objToVo(scoringResult);\n\n        // 可以根据需要为封装对象补充值，不需要的内容可以删除\n        // region 可选\n        // 1. 关联查询用户信息\n        Long userId = scoringResult.getUserId();\n        User user = null;\n        if (userId != null && userId > 0) {\n            user = userService.getById(userId);\n        }\n        UserVO userVO = userService.getUserVO(user);\n        scoringResultVO.setUser(userVO);\n        // endregion\n\n        return scoringResultVO;\n    }\n\n    /**\n     * 分页获取评分结果封装\n     *\n     * @param scoringResultPage\n     * @param request\n     * @return\n     */\n    @Override\n    public Page<ScoringResultVO> getScoringResultVOPage(Page<ScoringResult> scoringResultPage, HttpServletRequest request) {\n        List<ScoringResult> scoringResultList = scoringResultPage.getRecords();\n        Page<ScoringResultVO> scoringResultVOPage = new Page<>(scoringResultPage.getCurrent(), scoringResultPage.getSize(), scoringResultPage.getTotal());\n        if (CollUtil.isEmpty(scoringResultList)) {\n            return scoringResultVOPage;\n        }\n        // 对象列表 => 封装对象列表\n        List<ScoringResultVO> scoringResultVOList = scoringResultList.stream().map(scoringResult -> {\n            return ScoringResultVO.objToVo(scoringResult);\n        }).collect(Collectors.toList());\n\n        // 可以根据需要为封装对象补充值，不需要的内容可以删除\n        // region 可选\n        // 1. 关联查询用户信息\n        Set<Long> userIdSet = scoringResultList.stream().map(ScoringResult::getUserId).collect(Collectors.toSet());\n        Map<Long, List<User>> userIdUserListMap = userService.listByIds(userIdSet).stream()\n                .collect(Collectors.groupingBy(User::getId));\n        // 填充信息\n        scoringResultVOList.forEach(scoringResultVO -> {\n            Long userId = scoringResultVO.getUserId();\n            User user = null;\n            if (userIdUserListMap.containsKey(userId)) {\n                user = userIdUserListMap.get(userId).get(0);\n            }\n            scoringResultVO.setUser(userService.getUserVO(user));\n        });\n        // endregion\n\n        scoringResultVOPage.setRecords(scoringResultVOList);\n        return scoringResultVOPage;\n    }\n\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/service/impl/UserAnswerServiceImpl.java",
    "content": "package com.yupi.yudada.service.impl;\n\nimport cn.hutool.core.collection.CollUtil;\nimport com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;\nimport com.yupi.yudada.common.ErrorCode;\nimport com.yupi.yudada.constant.CommonConstant;\nimport com.yupi.yudada.exception.ThrowUtils;\nimport com.yupi.yudada.mapper.UserAnswerMapper;\nimport com.yupi.yudada.model.dto.userAnswer.UserAnswerQueryRequest;\nimport com.yupi.yudada.model.entity.App;\nimport com.yupi.yudada.model.entity.User;\nimport com.yupi.yudada.model.entity.UserAnswer;\nimport com.yupi.yudada.model.vo.UserAnswerVO;\nimport com.yupi.yudada.model.vo.UserVO;\nimport com.yupi.yudada.service.AppService;\nimport com.yupi.yudada.service.UserAnswerService;\nimport com.yupi.yudada.service.UserService;\nimport com.yupi.yudada.utils.SqlUtils;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.ObjectUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.stereotype.Service;\n\nimport javax.annotation.Resource;\nimport javax.servlet.http.HttpServletRequest;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\n/**\n * 用户答案服务实现\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Service\n@Slf4j\npublic class UserAnswerServiceImpl extends ServiceImpl<UserAnswerMapper, UserAnswer> implements UserAnswerService {\n\n    @Resource\n    private UserService userService;\n\n    @Resource\n    private AppService appService;\n\n    /**\n     * 校验数据\n     *\n     * @param userAnswer\n     * @param add        对创建的数据进行校验\n     */\n    @Override\n    public void validUserAnswer(UserAnswer userAnswer, boolean add) {\n        ThrowUtils.throwIf(userAnswer == null, ErrorCode.PARAMS_ERROR);\n        // 从对象中取值\n        Long appId = userAnswer.getAppId();\n        Long id = userAnswer.getId();\n        // 创建数据时，参数不能为空\n        if (add) {\n            // 补充校验规则\n            ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR, \"appId 非法\");\n            ThrowUtils.throwIf(id == null || id <= 0, ErrorCode.PARAMS_ERROR, \"id 非法\");\n        }\n        // 修改数据时，有参数则校验\n        // 补充校验规则\n        if (appId != null) {\n            App app = appService.getById(appId);\n            ThrowUtils.throwIf(app == null, ErrorCode.PARAMS_ERROR, \"应用不存在\");\n        }\n    }\n\n    /**\n     * 获取查询条件\n     *\n     * @param userAnswerQueryRequest\n     * @return\n     */\n    @Override\n    public QueryWrapper<UserAnswer> getQueryWrapper(UserAnswerQueryRequest userAnswerQueryRequest) {\n        QueryWrapper<UserAnswer> queryWrapper = new QueryWrapper<>();\n        if (userAnswerQueryRequest == null) {\n            return queryWrapper;\n        }\n        // 从对象中取值\n        Long id = userAnswerQueryRequest.getId();\n        Long appId = userAnswerQueryRequest.getAppId();\n        Integer appType = userAnswerQueryRequest.getAppType();\n        Integer scoringStrategy = userAnswerQueryRequest.getScoringStrategy();\n        String choices = userAnswerQueryRequest.getChoices();\n        Long resultId = userAnswerQueryRequest.getResultId();\n        String resultName = userAnswerQueryRequest.getResultName();\n        String resultDesc = userAnswerQueryRequest.getResultDesc();\n        String resultPicture = userAnswerQueryRequest.getResultPicture();\n        Integer resultScore = userAnswerQueryRequest.getResultScore();\n        Long userId = userAnswerQueryRequest.getUserId();\n        Long notId = userAnswerQueryRequest.getNotId();\n        String searchText = userAnswerQueryRequest.getSearchText();\n        String sortField = userAnswerQueryRequest.getSortField();\n        String sortOrder = userAnswerQueryRequest.getSortOrder();\n\n        // 补充需要的查询条件\n        // 从多字段中搜索\n        if (StringUtils.isNotBlank(searchText)) {\n            // 需要拼接查询条件\n            queryWrapper.and(qw -> qw.like(\"resultName\", searchText).or().like(\"resultDesc\", searchText));\n        }\n        // 模糊查询\n        queryWrapper.like(StringUtils.isNotBlank(choices), \"choices\", choices);\n        queryWrapper.like(StringUtils.isNotBlank(resultName), \"resultName\", resultName);\n        queryWrapper.like(StringUtils.isNotBlank(resultDesc), \"resultDesc\", resultDesc);\n        queryWrapper.like(StringUtils.isNotBlank(resultPicture), \"resultPicture\", resultPicture);\n        // 精确查询\n        queryWrapper.ne(ObjectUtils.isNotEmpty(notId), \"id\", notId);\n        queryWrapper.eq(ObjectUtils.isNotEmpty(id), \"id\", id);\n        queryWrapper.eq(ObjectUtils.isNotEmpty(userId), \"userId\", userId);\n        queryWrapper.eq(ObjectUtils.isNotEmpty(resultId), \"resultId\", resultId);\n        queryWrapper.eq(ObjectUtils.isNotEmpty(appId), \"appId\", appId);\n        queryWrapper.eq(ObjectUtils.isNotEmpty(appType), \"appType\", appType);\n        queryWrapper.eq(ObjectUtils.isNotEmpty(resultScore), \"resultScore\", resultScore);\n        queryWrapper.eq(ObjectUtils.isNotEmpty(scoringStrategy), \"scoringStrategy\", scoringStrategy);\n        // 排序规则\n        queryWrapper.orderBy(SqlUtils.validSortField(sortField),\n                sortOrder.equals(CommonConstant.SORT_ORDER_ASC),\n                sortField);\n        return queryWrapper;\n    }\n\n    /**\n     * 获取用户答案封装\n     *\n     * @param userAnswer\n     * @param request\n     * @return\n     */\n    @Override\n    public UserAnswerVO getUserAnswerVO(UserAnswer userAnswer, HttpServletRequest request) {\n        // 对象转封装类\n        UserAnswerVO userAnswerVO = UserAnswerVO.objToVo(userAnswer);\n\n        // 可以根据需要为封装对象补充值，不需要的内容可以删除\n        // region 可选\n        // 1. 关联查询用户信息\n        Long userId = userAnswer.getUserId();\n        User user = null;\n        if (userId != null && userId > 0) {\n            user = userService.getById(userId);\n        }\n        UserVO userVO = userService.getUserVO(user);\n        userAnswerVO.setUser(userVO);\n        // endregion\n\n        return userAnswerVO;\n    }\n\n    /**\n     * 分页获取用户答案封装\n     *\n     * @param userAnswerPage\n     * @param request\n     * @return\n     */\n    @Override\n    public Page<UserAnswerVO> getUserAnswerVOPage(Page<UserAnswer> userAnswerPage, HttpServletRequest request) {\n        List<UserAnswer> userAnswerList = userAnswerPage.getRecords();\n        Page<UserAnswerVO> userAnswerVOPage = new Page<>(userAnswerPage.getCurrent(), userAnswerPage.getSize(), userAnswerPage.getTotal());\n        if (CollUtil.isEmpty(userAnswerList)) {\n            return userAnswerVOPage;\n        }\n        // 对象列表 => 封装对象列表\n        List<UserAnswerVO> userAnswerVOList = userAnswerList.stream().map(userAnswer -> {\n            return UserAnswerVO.objToVo(userAnswer);\n        }).collect(Collectors.toList());\n\n        // 可以根据需要为封装对象补充值，不需要的内容可以删除\n        // region 可选\n        // 1. 关联查询用户信息\n        Set<Long> userIdSet = userAnswerList.stream().map(UserAnswer::getUserId).collect(Collectors.toSet());\n        Map<Long, List<User>> userIdUserListMap = userService.listByIds(userIdSet).stream()\n                .collect(Collectors.groupingBy(User::getId));\n        // 填充信息\n        userAnswerVOList.forEach(userAnswerVO -> {\n            Long userId = userAnswerVO.getUserId();\n            User user = null;\n            if (userIdUserListMap.containsKey(userId)) {\n                user = userIdUserListMap.get(userId).get(0);\n            }\n            userAnswerVO.setUser(userService.getUserVO(user));\n        });\n        // endregion\n\n        userAnswerVOPage.setRecords(userAnswerVOList);\n        return userAnswerVOPage;\n    }\n\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/service/impl/UserServiceImpl.java",
    "content": "package com.yupi.yudada.service.impl;\n\nimport static com.yupi.yudada.constant.UserConstant.USER_LOGIN_STATE;\n\nimport cn.hutool.core.collection.CollUtil;\nimport com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;\nimport com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;\nimport com.yupi.yudada.common.ErrorCode;\nimport com.yupi.yudada.constant.CommonConstant;\nimport com.yupi.yudada.exception.BusinessException;\nimport com.yupi.yudada.mapper.UserMapper;\nimport com.yupi.yudada.model.dto.user.UserQueryRequest;\nimport com.yupi.yudada.model.entity.User;\nimport com.yupi.yudada.model.enums.UserRoleEnum;\nimport com.yupi.yudada.model.vo.LoginUserVO;\nimport com.yupi.yudada.model.vo.UserVO;\nimport com.yupi.yudada.service.UserService;\nimport com.yupi.yudada.utils.SqlUtils;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport javax.servlet.http.HttpServletRequest;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.DigestUtils;\n\n/**\n * 用户服务实现\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Service\n@Slf4j\npublic class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {\n\n    /**\n     * 盐值，混淆密码\n     */\n    public static final String SALT = \"yupi\";\n\n    @Override\n    public long userRegister(String userAccount, String userPassword, String checkPassword) {\n        // 1. 校验\n        if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR, \"参数为空\");\n        }\n        if (userAccount.length() < 4) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR, \"用户账号过短\");\n        }\n        if (userPassword.length() < 8 || checkPassword.length() < 8) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR, \"用户密码过短\");\n        }\n        // 密码和校验密码相同\n        if (!userPassword.equals(checkPassword)) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR, \"两次输入的密码不一致\");\n        }\n        synchronized (userAccount.intern()) {\n            // 账户不能重复\n            QueryWrapper<User> queryWrapper = new QueryWrapper<>();\n            queryWrapper.eq(\"userAccount\", userAccount);\n            long count = this.baseMapper.selectCount(queryWrapper);\n            if (count > 0) {\n                throw new BusinessException(ErrorCode.PARAMS_ERROR, \"账号重复\");\n            }\n            // 2. 加密\n            String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());\n            // 3. 插入数据\n            User user = new User();\n            user.setUserAccount(userAccount);\n            user.setUserPassword(encryptPassword);\n            boolean saveResult = this.save(user);\n            if (!saveResult) {\n                throw new BusinessException(ErrorCode.SYSTEM_ERROR, \"注册失败，数据库错误\");\n            }\n            return user.getId();\n        }\n    }\n\n    @Override\n    public LoginUserVO userLogin(String userAccount, String userPassword, HttpServletRequest request) {\n        // 1. 校验\n        if (StringUtils.isAnyBlank(userAccount, userPassword)) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR, \"参数为空\");\n        }\n        if (userAccount.length() < 4) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR, \"账号错误\");\n        }\n        if (userPassword.length() < 8) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR, \"密码错误\");\n        }\n        // 2. 加密\n        String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());\n        // 查询用户是否存在\n        QueryWrapper<User> queryWrapper = new QueryWrapper<>();\n        queryWrapper.eq(\"userAccount\", userAccount);\n        queryWrapper.eq(\"userPassword\", encryptPassword);\n        User user = this.baseMapper.selectOne(queryWrapper);\n        // 用户不存在\n        if (user == null) {\n            log.info(\"user login failed, userAccount cannot match userPassword\");\n            throw new BusinessException(ErrorCode.PARAMS_ERROR, \"用户不存在或密码错误\");\n        }\n        // 3. 记录用户的登录态\n        request.getSession().setAttribute(USER_LOGIN_STATE, user);\n        return this.getLoginUserVO(user);\n    }\n\n    /**\n     * 获取当前登录用户\n     *\n     * @param request\n     * @return\n     */\n    @Override\n    public User getLoginUser(HttpServletRequest request) {\n        // 先判断是否已登录\n        Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);\n        User currentUser = (User) userObj;\n        if (currentUser == null || currentUser.getId() == null) {\n            throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);\n        }\n        // 从数据库查询（追求性能的话可以注释，直接走缓存）\n        long userId = currentUser.getId();\n        currentUser = this.getById(userId);\n        if (currentUser == null) {\n            throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);\n        }\n        return currentUser;\n    }\n\n    /**\n     * 获取当前登录用户（允许未登录）\n     *\n     * @param request\n     * @return\n     */\n    @Override\n    public User getLoginUserPermitNull(HttpServletRequest request) {\n        // 先判断是否已登录\n        Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);\n        User currentUser = (User) userObj;\n        if (currentUser == null || currentUser.getId() == null) {\n            return null;\n        }\n        // 从数据库查询（追求性能的话可以注释，直接走缓存）\n        long userId = currentUser.getId();\n        return this.getById(userId);\n    }\n\n    /**\n     * 是否为管理员\n     *\n     * @param request\n     * @return\n     */\n    @Override\n    public boolean isAdmin(HttpServletRequest request) {\n        // 仅管理员可查询\n        Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);\n        User user = (User) userObj;\n        return isAdmin(user);\n    }\n\n    @Override\n    public boolean isAdmin(User user) {\n        return user != null && UserRoleEnum.ADMIN.getValue().equals(user.getUserRole());\n    }\n\n    /**\n     * 用户注销\n     *\n     * @param request\n     */\n    @Override\n    public boolean userLogout(HttpServletRequest request) {\n        if (request.getSession().getAttribute(USER_LOGIN_STATE) == null) {\n            throw new BusinessException(ErrorCode.OPERATION_ERROR, \"未登录\");\n        }\n        // 移除登录态\n        request.getSession().removeAttribute(USER_LOGIN_STATE);\n        return true;\n    }\n\n    @Override\n    public LoginUserVO getLoginUserVO(User user) {\n        if (user == null) {\n            return null;\n        }\n        LoginUserVO loginUserVO = new LoginUserVO();\n        BeanUtils.copyProperties(user, loginUserVO);\n        return loginUserVO;\n    }\n\n    @Override\n    public UserVO getUserVO(User user) {\n        if (user == null) {\n            return null;\n        }\n        UserVO userVO = new UserVO();\n        BeanUtils.copyProperties(user, userVO);\n        return userVO;\n    }\n\n    @Override\n    public List<UserVO> getUserVO(List<User> userList) {\n        if (CollUtil.isEmpty(userList)) {\n            return new ArrayList<>();\n        }\n        return userList.stream().map(this::getUserVO).collect(Collectors.toList());\n    }\n\n    @Override\n    public QueryWrapper<User> getQueryWrapper(UserQueryRequest userQueryRequest) {\n        if (userQueryRequest == null) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR, \"请求参数为空\");\n        }\n        Long id = userQueryRequest.getId();\n        String unionId = userQueryRequest.getUnionId();\n        String mpOpenId = userQueryRequest.getMpOpenId();\n        String userName = userQueryRequest.getUserName();\n        String userProfile = userQueryRequest.getUserProfile();\n        String userRole = userQueryRequest.getUserRole();\n        String sortField = userQueryRequest.getSortField();\n        String sortOrder = userQueryRequest.getSortOrder();\n        QueryWrapper<User> queryWrapper = new QueryWrapper<>();\n        queryWrapper.eq(id != null, \"id\", id);\n        queryWrapper.eq(StringUtils.isNotBlank(unionId), \"unionId\", unionId);\n        queryWrapper.eq(StringUtils.isNotBlank(mpOpenId), \"mpOpenId\", mpOpenId);\n        queryWrapper.eq(StringUtils.isNotBlank(userRole), \"userRole\", userRole);\n        queryWrapper.like(StringUtils.isNotBlank(userProfile), \"userProfile\", userProfile);\n        queryWrapper.like(StringUtils.isNotBlank(userName), \"userName\", userName);\n        queryWrapper.orderBy(SqlUtils.validSortField(sortField), sortOrder.equals(CommonConstant.SORT_ORDER_ASC),\n                sortField);\n        return queryWrapper;\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/utils/NetUtils.java",
    "content": "package com.yupi.yudada.utils;\n\nimport java.net.InetAddress;\nimport javax.servlet.http.HttpServletRequest;\n\n/**\n * 网络工具类\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\npublic class NetUtils {\n\n    /**\n     * 获取客户端 IP 地址\n     *\n     * @param request\n     * @return\n     */\n    public static String getIpAddress(HttpServletRequest request) {\n        String ip = request.getHeader(\"x-forwarded-for\");\n        if (ip == null || ip.length() == 0 || \"unknown\".equalsIgnoreCase(ip)) {\n            ip = request.getHeader(\"Proxy-Client-IP\");\n        }\n        if (ip == null || ip.length() == 0 || \"unknown\".equalsIgnoreCase(ip)) {\n            ip = request.getHeader(\"WL-Proxy-Client-IP\");\n        }\n        if (ip == null || ip.length() == 0 || \"unknown\".equalsIgnoreCase(ip)) {\n            ip = request.getRemoteAddr();\n            if (ip.equals(\"127.0.0.1\")) {\n                // 根据网卡取本机配置的 IP\n                InetAddress inet = null;\n                try {\n                    inet = InetAddress.getLocalHost();\n                } catch (Exception e) {\n                    e.printStackTrace();\n                }\n                if (inet != null) {\n                    ip = inet.getHostAddress();\n                }\n            }\n        }\n        // 多个代理的情况，第一个IP为客户端真实IP,多个IP按照','分割\n        if (ip != null && ip.length() > 15) {\n            if (ip.indexOf(\",\") > 0) {\n                ip = ip.substring(0, ip.indexOf(\",\"));\n            }\n        }\n        if (ip == null) {\n            return \"127.0.0.1\";\n        }\n        return ip;\n    }\n\n}\n"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/utils/SpringContextUtils.java",
    "content": "package com.yupi.yudada.utils;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.springframework.beans.BeansException;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.ApplicationContextAware;\nimport org.springframework.stereotype.Component;\n\n/**\n * Spring 上下文获取工具\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@Component\npublic class SpringContextUtils implements ApplicationContextAware {\n\n    private static ApplicationContext applicationContext;\n\n    @Override\n    public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {\n        SpringContextUtils.applicationContext = applicationContext;\n    }\n\n    /**\n     * 通过名称获取 Bean\n     *\n     * @param beanName\n     * @return\n     */\n    public static Object getBean(String beanName) {\n        return applicationContext.getBean(beanName);\n    }\n\n    /**\n     * 通过 class 获取 Bean\n     *\n     * @param beanClass\n     * @param <T>\n     * @return\n     */\n    public static <T> T getBean(Class<T> beanClass) {\n        return applicationContext.getBean(beanClass);\n    }\n\n    /**\n     * 通过名称和类型获取 Bean\n     *\n     * @param beanName\n     * @param beanClass\n     * @param <T>\n     * @return\n     */\n    public static <T> T getBean(String beanName, Class<T> beanClass) {\n        return applicationContext.getBean(beanName, beanClass);\n    }\n}"
  },
  {
    "path": "yudada-backend/src/main/java/com/yupi/yudada/utils/SqlUtils.java",
    "content": "package com.yupi.yudada.utils;\n\nimport org.apache.commons.lang3.StringUtils;\n\n/**\n * SQL 工具\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\npublic class SqlUtils {\n\n    /**\n     * 校验排序字段是否合法（防止 SQL 注入）\n     *\n     * @param sortField\n     * @return\n     */\n    public static boolean validSortField(String sortField) {\n        if (StringUtils.isBlank(sortField)) {\n            return false;\n        }\n        return !StringUtils.containsAny(sortField, \"=\", \"(\", \")\", \" \");\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json",
    "content": "{\n  \"properties\": [\n    {\n      \"name\": \"cos.client.accessKey\",\n      \"type\": \"java.lang.String\",\n      \"description\": \"Description for cos.client.accessKey.\"\n    },\n    {\n      \"name\": \"cos.client.secretKey\",\n      \"type\": \"java.lang.String\",\n      \"description\": \"Description for cos.client.secretKey.\"\n    },\n    {\n      \"name\": \"cos.client.region\",\n      \"type\": \"java.lang.String\",\n      \"description\": \"Description for cos.client.region.\"\n    },\n    {\n      \"name\": \"cos.client.bucket\",\n      \"type\": \"java.lang.String\",\n      \"description\": \"Description for cos.client.bucket.\"\n    },\n    {\n      \"name\": \"wx.open.appId\",\n      \"type\": \"java.lang.String\",\n      \"description\": \"Description for wx.open.appId.\"\n    },\n    {\n      \"name\": \"wx.open.appSecret\",\n      \"type\": \"java.lang.String\",\n      \"description\": \"Description for wx.open.appSecret.\"\n    }\n  ]\n}"
  },
  {
    "path": "yudada-backend/src/main/resources/application-prod.yml",
    "content": "# 线上配置文件\n# @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n# @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\nserver:\n  port: 8101\nspring:\n  # 数据库配置\n  # todo 需替换配置\n  datasource:\n    driver-class-name: com.mysql.cj.jdbc.Driver\n    url: jdbc:mysql://localhost:3306/yudada\n    username: root\n    password: 123456\n  # Redis 配置\n  # todo 需替换配置\n  redis:\n    database: 1\n    host: localhost\n    port: 6379\n    timeout: 5000\n    password: 123456\nmybatis-plus:\n  configuration:\n    # 生产环境关闭日志\n    log-impl: ''\n# 接口文档配置\nknife4j:\n  basic:\n    enable: true\n    username: root\n    password: 123456\n"
  },
  {
    "path": "yudada-backend/src/main/resources/application-test.yml",
    "content": "# 测试配置文件\n# @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n# @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\nserver:\n  port: 8101\nspring:\n  # 数据库配置\n  # todo 需替换配置\n  datasource:\n    driver-class-name: com.mysql.cj.jdbc.Driver\n    url: jdbc:mysql://localhost:3306/yudada\n    username: root\n    password: 123456\n  # Redis 配置\n  # todo 需替换配置\n  redis:\n    database: 1\n    host: localhost\n    port: 6379\n    timeout: 5000\n    password: 123456\n"
  },
  {
    "path": "yudada-backend/src/main/resources/application.yml",
    "content": "# 公共配置文件\n# @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n# @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\nspring:\n  application:\n    name: yudada-backend\n  # 默认 dev 环境\n  profiles:\n    active: dev\n  # 支持 swagger3\n  mvc:\n    pathmatch:\n      matching-strategy: ant_path_matcher\n  # session 配置\n  session:\n    # todo 取消注释开启分布式 session（须先配置 Redis）\n    # store-type: redis\n    # 30 天过期\n    timeout: 2592000\n  # 数据库配置\n  # todo 需替换配置\n  datasource:\n    driver-class-name: com.mysql.cj.jdbc.Driver\n    url: jdbc:mysql://localhost:3306/yudada\n    username: root\n    password: 123456\n  # Redis 配置\n  # todo 需替换配置，然后取消注释\n  redis:\n    database: 1\n    host: localhost\n    port: 6379\n    timeout: 5000\n  #    password: 123456\n  # 文件上传\n  servlet:\n    multipart:\n      # 大小限制\n      max-file-size: 10MB\n  # 分库分表配置\n  shardingsphere:\n    #数据源配置\n    datasource:\n      # 多数据源以逗号隔开即可\n      names: yudada\n      yudada:\n        type: com.zaxxer.hikari.HikariDataSource\n        driver-class-name: com.mysql.cj.jdbc.Driver\n        jdbc-url: jdbc:mysql://localhost:3306/yudada\n        username: root\n        password: 123456\n    # 规则配置\n    rules:\n      sharding:\n        # 分片算法配置\n        sharding-algorithms:\n          # 自定义分片规则名\n          answer-table-inline:\n            ## inline 类型是简单的配置文件里面就能写的类型，其他还有自定义类等等\n            type: INLINE\n            props:\n              algorithm-expression: user_answer_$->{appId % 2}\n        tables:\n          user_answer:\n            actual-data-nodes: yudada.user_answer_$->{0..1}\n            # 分表策略\n            table-strategy:\n              standard:\n                sharding-column: appId\n                sharding-algorithm-name: answer-table-inline\nserver:\n  address: 0.0.0.0\n  port: 8101\n  servlet:\n    context-path: /api\n    # cookie 30 天过期\n    session:\n      cookie:\n        max-age: 2592000\nmybatis-plus:\n  configuration:\n    map-underscore-to-camel-case: false\n    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\n  global-config:\n    db-config:\n      logic-delete-field: isDelete # 全局逻辑删除的实体字段名\n      logic-delete-value: 1 # 逻辑已删除值（默认为 1）\n      logic-not-delete-value: 0 # 逻辑未删除值（默认为 0）\n# 对象存储\n# todo 需替换配置\ncos:\n  client:\n    accessKey: xxx\n    secretKey: xxx\n    region: xxx\n    bucket: xxx\n# 接口文档配置\nknife4j:\n  enable: true\n  openapi:\n    title: \"接口文档\"\n    version: 1.0\n    group:\n      default:\n        api-rule: package\n        api-rule-resources:\n          - com.yupi.yudada.controller\n# AI 配置\nai:\n  apiKey: 改为自己的 key"
  },
  {
    "path": "yudada-backend/src/main/resources/banner.txt",
    "content": "by 程序员鱼皮：https://github.com/liyupi\n可能是最好的编程学习圈子：https://yupi.icu\n"
  },
  {
    "path": "yudada-backend/src/main/resources/mapper/AppMapper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE mapper\n        PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\"\n        \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n<mapper namespace=\"com.yupi.yudada.mapper.AppMapper\">\n\n    <resultMap id=\"BaseResultMap\" type=\"com.yupi.yudada.model.entity.App\">\n            <id property=\"id\" column=\"id\" jdbcType=\"BIGINT\"/>\n            <result property=\"appName\" column=\"appName\" jdbcType=\"VARCHAR\"/>\n            <result property=\"appDesc\" column=\"appDesc\" jdbcType=\"VARCHAR\"/>\n            <result property=\"appIcon\" column=\"appIcon\" jdbcType=\"VARCHAR\"/>\n            <result property=\"appType\" column=\"appType\" jdbcType=\"TINYINT\"/>\n            <result property=\"scoringStrategy\" column=\"scoringStrategy\" jdbcType=\"TINYINT\"/>\n            <result property=\"reviewStatus\" column=\"reviewStatus\" jdbcType=\"INTEGER\"/>\n            <result property=\"reviewMessage\" column=\"reviewMessage\" jdbcType=\"VARCHAR\"/>\n            <result property=\"reviewerId\" column=\"reviewerId\" jdbcType=\"BIGINT\"/>\n            <result property=\"reviewTime\" column=\"reviewTime\" jdbcType=\"TIMESTAMP\"/>\n            <result property=\"userId\" column=\"userId\" jdbcType=\"BIGINT\"/>\n            <result property=\"createTime\" column=\"createTime\" jdbcType=\"TIMESTAMP\"/>\n            <result property=\"updateTime\" column=\"updateTime\" jdbcType=\"TIMESTAMP\"/>\n            <result property=\"isDelete\" column=\"isDelete\" jdbcType=\"TINYINT\"/>\n    </resultMap>\n\n    <sql id=\"Base_Column_List\">\n        id,appName,appDesc,\n        appIcon,appType,scoringStrategy,\n        reviewStatus,reviewMessage,reviewerId,\n        reviewTime,userId,createTime,\n        updateTime,isDelete\n    </sql>\n</mapper>\n"
  },
  {
    "path": "yudada-backend/src/main/resources/mapper/PostFavourMapper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a> -->\n<!-- @from <a href=\"https://yupi.icu\">编程导航知识星球</a> -->\n<!DOCTYPE mapper\n        PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\"\n        \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n<mapper namespace=\"com.yupi.yudada.mapper.PostFavourMapper\">\n\n    <resultMap id=\"BaseResultMap\" type=\"com.yupi.yudada.model.entity.PostFavour\">\n        <id property=\"id\" column=\"id\" jdbcType=\"BIGINT\"/>\n        <result property=\"postId\" column=\"postId\" jdbcType=\"BIGINT\"/>\n        <result property=\"userId\" column=\"userId\" jdbcType=\"BIGINT\"/>\n        <result property=\"createTime\" column=\"createTime\" jdbcType=\"TIMESTAMP\"/>\n        <result property=\"updateTime\" column=\"updateTime\" jdbcType=\"TIMESTAMP\"/>\n    </resultMap>\n\n    <sql id=\"Base_Column_List\">\n        id,postId,userId,\n        createTime,updateTime\n    </sql>\n\n    <select id=\"listFavourPostByPage\"\n            resultType=\"com.yupi.yudada.model.entity.Post\">\n        select p.*\n        from post p\n                 join (select postId from post_favour where userId = #{favourUserId}) pf\n                      on p.id = pf.postId ${ew.customSqlSegment}\n    </select>\n</mapper>\n"
  },
  {
    "path": "yudada-backend/src/main/resources/mapper/PostMapper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a> -->\n<!-- @from <a href=\"https://yupi.icu\">编程导航知识星球</a> -->\n<!DOCTYPE mapper\n        PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\"\n        \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n<mapper namespace=\"com.yupi.yudada.mapper.PostMapper\">\n\n    <resultMap id=\"BaseResultMap\" type=\"com.yupi.yudada.model.entity.Post\">\n        <id property=\"id\" column=\"id\" jdbcType=\"BIGINT\"/>\n        <result property=\"title\" column=\"title\" jdbcType=\"VARCHAR\"/>\n        <result property=\"content\" column=\"content\" jdbcType=\"VARCHAR\"/>\n        <result property=\"tags\" column=\"tags\" jdbcType=\"VARCHAR\"/>\n        <result property=\"thumbNum\" column=\"thumbNum\" jdbcType=\"BIGINT\"/>\n        <result property=\"favourNum\" column=\"favourNum\" jdbcType=\"BIGINT\"/>\n        <result property=\"userId\" column=\"userId\" jdbcType=\"BIGINT\"/>\n        <result property=\"createTime\" column=\"createTime\" jdbcType=\"TIMESTAMP\"/>\n        <result property=\"updateTime\" column=\"updateTime\" jdbcType=\"TIMESTAMP\"/>\n        <result property=\"isDelete\" column=\"isDelete\" jdbcType=\"TINYINT\"/>\n    </resultMap>\n\n    <sql id=\"Base_Column_List\">\n        id,title,content,tags,\n        thumbNum,favourNum,userId,\n        createTime,updateTime,isDelete\n    </sql>\n\n    <select id=\"listPostWithDelete\" resultType=\"com.yupi.yudada.model.entity.Post\">\n        select *\n        from post\n        where updateTime >= #{minUpdateTime}\n    </select>\n</mapper>\n"
  },
  {
    "path": "yudada-backend/src/main/resources/mapper/PostThumbMapper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a> -->\n<!-- @from <a href=\"https://yupi.icu\">编程导航知识星球</a> -->\n<!DOCTYPE mapper\n        PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\"\n        \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n<mapper namespace=\"com.yupi.yudada.mapper.PostThumbMapper\">\n\n    <resultMap id=\"BaseResultMap\" type=\"com.yupi.yudada.model.entity.PostThumb\">\n            <id property=\"id\" column=\"id\" jdbcType=\"BIGINT\"/>\n            <result property=\"postId\" column=\"postId\" jdbcType=\"BIGINT\"/>\n            <result property=\"userId\" column=\"userId\" jdbcType=\"BIGINT\"/>\n            <result property=\"createTime\" column=\"createTime\" jdbcType=\"TIMESTAMP\"/>\n            <result property=\"updateTime\" column=\"updateTime\" jdbcType=\"TIMESTAMP\"/>\n    </resultMap>\n\n    <sql id=\"Base_Column_List\">\n        id,postId,\n        userId,createTime,updateTime\n    </sql>\n</mapper>\n"
  },
  {
    "path": "yudada-backend/src/main/resources/mapper/QuestionMapper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE mapper\n        PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\"\n        \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n<mapper namespace=\"com.yupi.yudada.mapper.QuestionMapper\">\n\n    <resultMap id=\"BaseResultMap\" type=\"com.yupi.yudada.model.entity.Question\">\n            <id property=\"id\" column=\"id\" jdbcType=\"BIGINT\"/>\n            <result property=\"questionContent\" column=\"questionContent\" jdbcType=\"VARCHAR\"/>\n            <result property=\"appId\" column=\"appId\" jdbcType=\"BIGINT\"/>\n            <result property=\"userId\" column=\"userId\" jdbcType=\"BIGINT\"/>\n            <result property=\"createTime\" column=\"createTime\" jdbcType=\"TIMESTAMP\"/>\n            <result property=\"updateTime\" column=\"updateTime\" jdbcType=\"TIMESTAMP\"/>\n            <result property=\"isDelete\" column=\"isDelete\" jdbcType=\"TINYINT\"/>\n    </resultMap>\n\n    <sql id=\"Base_Column_List\">\n        id,questionContent,appId,\n        userId,createTime,updateTime,\n        isDelete\n    </sql>\n</mapper>\n"
  },
  {
    "path": "yudada-backend/src/main/resources/mapper/ScoringResultMapper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE mapper\n        PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\"\n        \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n<mapper namespace=\"com.yupi.yudada.mapper.ScoringResultMapper\">\n\n    <resultMap id=\"BaseResultMap\" type=\"com.yupi.yudada.model.entity.ScoringResult\">\n            <id property=\"id\" column=\"id\" jdbcType=\"BIGINT\"/>\n            <result property=\"resultName\" column=\"resultName\" jdbcType=\"VARCHAR\"/>\n            <result property=\"resultDesc\" column=\"resultDesc\" jdbcType=\"VARCHAR\"/>\n            <result property=\"resultPicture\" column=\"resultPicture\" jdbcType=\"VARCHAR\"/>\n            <result property=\"resultProp\" column=\"resultProp\" jdbcType=\"VARCHAR\"/>\n            <result property=\"resultScoreRange\" column=\"resultScoreRange\" jdbcType=\"INTEGER\"/>\n            <result property=\"appId\" column=\"appId\" jdbcType=\"BIGINT\"/>\n            <result property=\"userId\" column=\"userId\" jdbcType=\"BIGINT\"/>\n            <result property=\"createTime\" column=\"createTime\" jdbcType=\"TIMESTAMP\"/>\n            <result property=\"updateTime\" column=\"updateTime\" jdbcType=\"TIMESTAMP\"/>\n            <result property=\"isDelete\" column=\"isDelete\" jdbcType=\"TINYINT\"/>\n    </resultMap>\n\n    <sql id=\"Base_Column_List\">\n        id,resultName,resultDesc,\n        resultPicture,resultProp,resultScoreRange,\n        appId,userId,createTime,\n        updateTime,isDelete\n    </sql>\n</mapper>\n"
  },
  {
    "path": "yudada-backend/src/main/resources/mapper/UserAnswerMapper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE mapper\n        PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\"\n        \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n<mapper namespace=\"com.yupi.yudada.mapper.UserAnswerMapper\">\n\n    <resultMap id=\"BaseResultMap\" type=\"com.yupi.yudada.model.entity.UserAnswer\">\n            <id property=\"id\" column=\"id\" jdbcType=\"BIGINT\"/>\n            <result property=\"appId\" column=\"appId\" jdbcType=\"BIGINT\"/>\n            <result property=\"appType\" column=\"appType\" jdbcType=\"TINYINT\"/>\n            <result property=\"scoringStrategy\" column=\"scoringStrategy\" jdbcType=\"TINYINT\"/>\n            <result property=\"choices\" column=\"choices\" jdbcType=\"VARCHAR\"/>\n            <result property=\"resultId\" column=\"resultId\" jdbcType=\"BIGINT\"/>\n            <result property=\"resultName\" column=\"resultName\" jdbcType=\"VARCHAR\"/>\n            <result property=\"resultDesc\" column=\"resultDesc\" jdbcType=\"VARCHAR\"/>\n            <result property=\"resultPicture\" column=\"resultPicture\" jdbcType=\"VARCHAR\"/>\n            <result property=\"resultScore\" column=\"resultScore\" jdbcType=\"INTEGER\"/>\n            <result property=\"userId\" column=\"userId\" jdbcType=\"BIGINT\"/>\n            <result property=\"createTime\" column=\"createTime\" jdbcType=\"TIMESTAMP\"/>\n            <result property=\"updateTime\" column=\"updateTime\" jdbcType=\"TIMESTAMP\"/>\n            <result property=\"isDelete\" column=\"isDelete\" jdbcType=\"TINYINT\"/>\n    </resultMap>\n\n    <sql id=\"Base_Column_List\">\n        id,appId,appType,\n        scoringStrategy,choices,resultId,\n        resultName,resultDesc,resultPicture,\n        resultScore,userId,createTime,\n        updateTime,isDelete\n    </sql>\n</mapper>\n"
  },
  {
    "path": "yudada-backend/src/main/resources/mapper/UserMapper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a> -->\n<!-- @from <a href=\"https://yupi.icu\">编程导航知识星球</a> -->\n<!DOCTYPE mapper\n        PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\"\n        \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n<mapper namespace=\"com.yupi.yudada.mapper.UserMapper\">\n    <resultMap id=\"BaseResultMap\" type=\"com.yupi.yudada.model.entity.User\">\n        <id property=\"id\" column=\"id\" jdbcType=\"BIGINT\"/>\n        <result property=\"unionId\" column=\"unionId\" jdbcType=\"VARCHAR\"/>\n        <result property=\"mpOpenId\" column=\"mpOpenId\" jdbcType=\"VARCHAR\"/>\n        <result property=\"userName\" column=\"userName\" jdbcType=\"VARCHAR\"/>\n        <result property=\"userAvatar\" column=\"userAvatar\" jdbcType=\"VARCHAR\"/>\n        <result property=\"userProfile\" column=\"userProfile\" jdbcType=\"VARCHAR\"/>\n        <result property=\"userRole\" column=\"userRole\" jdbcType=\"VARCHAR\"/>\n        <result property=\"createTime\" column=\"createTime\" jdbcType=\"TIMESTAMP\"/>\n        <result property=\"updateTime\" column=\"updateTime\" jdbcType=\"TIMESTAMP\"/>\n        <result property=\"isDelete\" column=\"isDelete\" jdbcType=\"TINYINT\"/>\n    </resultMap>\n\n    <sql id=\"Base_Column_List\">\n        id,unionId,mpOpenId,\n        userName,userAvatar,userProfile,\n        userRole,createTime,updateTime,isDelete\n    </sql>\n</mapper>\n"
  },
  {
    "path": "yudada-backend/src/main/resources/templates/TemplateController.java.ftl",
    "content": "package ${packageName}.controller;\n\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport ${packageName}.annotation.AuthCheck;\nimport ${packageName}.common.BaseResponse;\nimport ${packageName}.common.DeleteRequest;\nimport ${packageName}.common.ErrorCode;\nimport ${packageName}.common.ResultUtils;\nimport ${packageName}.constant.UserConstant;\nimport ${packageName}.exception.BusinessException;\nimport ${packageName}.exception.ThrowUtils;\nimport ${packageName}.model.dto.${dataKey}.${upperDataKey}AddRequest;\nimport ${packageName}.model.dto.${dataKey}.${upperDataKey}EditRequest;\nimport ${packageName}.model.dto.${dataKey}.${upperDataKey}QueryRequest;\nimport ${packageName}.model.dto.${dataKey}.${upperDataKey}UpdateRequest;\nimport ${packageName}.model.entity.${upperDataKey};\nimport ${packageName}.model.entity.User;\nimport ${packageName}.model.vo.${upperDataKey}VO;\nimport ${packageName}.service.${upperDataKey}Service;\nimport ${packageName}.service.UserService;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.web.bind.annotation.*;\n\nimport javax.annotation.Resource;\nimport javax.servlet.http.HttpServletRequest;\n\n/**\n * ${dataName}接口\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@RestController\n@RequestMapping(\"/${dataKey}\")\n@Slf4j\npublic class ${upperDataKey}Controller {\n\n    @Resource\n    private ${upperDataKey}Service ${dataKey}Service;\n\n    @Resource\n    private UserService userService;\n\n    // region 增删改查\n\n    /**\n     * 创建${dataName}\n     *\n     * @param ${dataKey}AddRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/add\")\n    public BaseResponse<Long> add${upperDataKey}(@RequestBody ${upperDataKey}AddRequest ${dataKey}AddRequest, HttpServletRequest request) {\n        ThrowUtils.throwIf(${dataKey}AddRequest == null, ErrorCode.PARAMS_ERROR);\n        // todo 在此处将实体类和 DTO 进行转换\n        ${upperDataKey} ${dataKey} = new ${upperDataKey}();\n        BeanUtils.copyProperties(${dataKey}AddRequest, ${dataKey});\n        // 数据校验\n        ${dataKey}Service.valid${upperDataKey}(${dataKey}, true);\n        // todo 填充默认值\n        User loginUser = userService.getLoginUser(request);\n        ${dataKey}.setUserId(loginUser.getId());\n        // 写入数据库\n        boolean result = ${dataKey}Service.save(${dataKey});\n        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);\n        // 返回新写入的数据 id\n        long new${upperDataKey}Id = ${dataKey}.getId();\n        return ResultUtils.success(new${upperDataKey}Id);\n    }\n\n    /**\n     * 删除${dataName}\n     *\n     * @param deleteRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/delete\")\n    public BaseResponse<Boolean> delete${upperDataKey}(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {\n        if (deleteRequest == null || deleteRequest.getId() <= 0) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        User user = userService.getLoginUser(request);\n        long id = deleteRequest.getId();\n        // 判断是否存在\n        ${upperDataKey} old${upperDataKey} = ${dataKey}Service.getById(id);\n        ThrowUtils.throwIf(old${upperDataKey} == null, ErrorCode.NOT_FOUND_ERROR);\n        // 仅本人或管理员可删除\n        if (!old${upperDataKey}.getUserId().equals(user.getId()) && !userService.isAdmin(request)) {\n            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);\n        }\n        // 操作数据库\n        boolean result = ${dataKey}Service.removeById(id);\n        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);\n        return ResultUtils.success(true);\n    }\n\n    /**\n     * 更新${dataName}（仅管理员可用）\n     *\n     * @param ${dataKey}UpdateRequest\n     * @return\n     */\n    @PostMapping(\"/update\")\n    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)\n    public BaseResponse<Boolean> update${upperDataKey}(@RequestBody ${upperDataKey}UpdateRequest ${dataKey}UpdateRequest) {\n        if (${dataKey}UpdateRequest == null || ${dataKey}UpdateRequest.getId() <= 0) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        // todo 在此处将实体类和 DTO 进行转换\n        ${upperDataKey} ${dataKey} = new ${upperDataKey}();\n        BeanUtils.copyProperties(${dataKey}UpdateRequest, ${dataKey});\n        // 数据校验\n        ${dataKey}Service.valid${upperDataKey}(${dataKey}, false);\n        // 判断是否存在\n        long id = ${dataKey}UpdateRequest.getId();\n        ${upperDataKey} old${upperDataKey} = ${dataKey}Service.getById(id);\n        ThrowUtils.throwIf(old${upperDataKey} == null, ErrorCode.NOT_FOUND_ERROR);\n        // 操作数据库\n        boolean result = ${dataKey}Service.updateById(${dataKey});\n        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);\n        return ResultUtils.success(true);\n    }\n\n    /**\n     * 根据 id 获取${dataName}（封装类）\n     *\n     * @param id\n     * @return\n     */\n    @GetMapping(\"/get/vo\")\n    public BaseResponse<${upperDataKey}VO> get${upperDataKey}VOById(long id, HttpServletRequest request) {\n        ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR);\n        // 查询数据库\n        ${upperDataKey} ${dataKey} = ${dataKey}Service.getById(id);\n        ThrowUtils.throwIf(${dataKey} == null, ErrorCode.NOT_FOUND_ERROR);\n        // 获取封装类\n        return ResultUtils.success(${dataKey}Service.get${upperDataKey}VO(${dataKey}, request));\n    }\n\n    /**\n     * 分页获取${dataName}列表（仅管理员可用）\n     *\n     * @param ${dataKey}QueryRequest\n     * @return\n     */\n    @PostMapping(\"/list/page\")\n    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)\n    public BaseResponse<Page<${upperDataKey}>> list${upperDataKey}ByPage(@RequestBody ${upperDataKey}QueryRequest ${dataKey}QueryRequest) {\n        long current = ${dataKey}QueryRequest.getCurrent();\n        long size = ${dataKey}QueryRequest.getPageSize();\n        // 查询数据库\n        Page<${upperDataKey}> ${dataKey}Page = ${dataKey}Service.page(new Page<>(current, size),\n                ${dataKey}Service.getQueryWrapper(${dataKey}QueryRequest));\n        return ResultUtils.success(${dataKey}Page);\n    }\n\n    /**\n     * 分页获取${dataName}列表（封装类）\n     *\n     * @param ${dataKey}QueryRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/list/page/vo\")\n    public BaseResponse<Page<${upperDataKey}VO>> list${upperDataKey}VOByPage(@RequestBody ${upperDataKey}QueryRequest ${dataKey}QueryRequest,\n                                                               HttpServletRequest request) {\n        long current = ${dataKey}QueryRequest.getCurrent();\n        long size = ${dataKey}QueryRequest.getPageSize();\n        // 限制爬虫\n        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);\n        // 查询数据库\n        Page<${upperDataKey}> ${dataKey}Page = ${dataKey}Service.page(new Page<>(current, size),\n                ${dataKey}Service.getQueryWrapper(${dataKey}QueryRequest));\n        // 获取封装类\n        return ResultUtils.success(${dataKey}Service.get${upperDataKey}VOPage(${dataKey}Page, request));\n    }\n\n    /**\n     * 分页获取当前登录用户创建的${dataName}列表\n     *\n     * @param ${dataKey}QueryRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/my/list/page/vo\")\n    public BaseResponse<Page<${upperDataKey}VO>> listMy${upperDataKey}VOByPage(@RequestBody ${upperDataKey}QueryRequest ${dataKey}QueryRequest,\n                                                                 HttpServletRequest request) {\n        ThrowUtils.throwIf(${dataKey}QueryRequest == null, ErrorCode.PARAMS_ERROR);\n        // 补充查询条件，只查询当前登录用户的数据\n        User loginUser = userService.getLoginUser(request);\n        ${dataKey}QueryRequest.setUserId(loginUser.getId());\n        long current = ${dataKey}QueryRequest.getCurrent();\n        long size = ${dataKey}QueryRequest.getPageSize();\n        // 限制爬虫\n        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);\n        // 查询数据库\n        Page<${upperDataKey}> ${dataKey}Page = ${dataKey}Service.page(new Page<>(current, size),\n                ${dataKey}Service.getQueryWrapper(${dataKey}QueryRequest));\n        // 获取封装类\n        return ResultUtils.success(${dataKey}Service.get${upperDataKey}VOPage(${dataKey}Page, request));\n    }\n\n    /**\n     * 编辑${dataName}（给用户使用）\n     *\n     * @param ${dataKey}EditRequest\n     * @param request\n     * @return\n     */\n    @PostMapping(\"/edit\")\n    public BaseResponse<Boolean> edit${upperDataKey}(@RequestBody ${upperDataKey}EditRequest ${dataKey}EditRequest, HttpServletRequest request) {\n        if (${dataKey}EditRequest == null || ${dataKey}EditRequest.getId() <= 0) {\n            throw new BusinessException(ErrorCode.PARAMS_ERROR);\n        }\n        // todo 在此处将实体类和 DTO 进行转换\n        ${upperDataKey} ${dataKey} = new ${upperDataKey}();\n        BeanUtils.copyProperties(${dataKey}EditRequest, ${dataKey});\n        // 数据校验\n        ${dataKey}Service.valid${upperDataKey}(${dataKey}, false);\n        User loginUser = userService.getLoginUser(request);\n        // 判断是否存在\n        long id = ${dataKey}EditRequest.getId();\n        ${upperDataKey} old${upperDataKey} = ${dataKey}Service.getById(id);\n        ThrowUtils.throwIf(old${upperDataKey} == null, ErrorCode.NOT_FOUND_ERROR);\n        // 仅本人或管理员可编辑\n        if (!old${upperDataKey}.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {\n            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);\n        }\n        // 操作数据库\n        boolean result = ${dataKey}Service.updateById(${dataKey});\n        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);\n        return ResultUtils.success(true);\n    }\n\n    // endregion\n}\n"
  },
  {
    "path": "yudada-backend/src/main/resources/templates/TemplateService.java.ftl",
    "content": "package ${packageName}.service;\n\nimport com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.baomidou.mybatisplus.extension.service.IService;\nimport ${packageName}.model.dto.${dataKey}.${upperDataKey}QueryRequest;\nimport ${packageName}.model.entity.${upperDataKey};\nimport ${packageName}.model.vo.${upperDataKey}VO;\n\nimport javax.servlet.http.HttpServletRequest;\n\n/**\n * ${dataName}服务\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\npublic interface ${upperDataKey}Service extends IService<${upperDataKey}> {\n\n    /**\n     * 校验数据\n     *\n     * @param ${dataKey}\n     * @param add 对创建的数据进行校验\n     */\n    void valid${upperDataKey}(${upperDataKey} ${dataKey}, boolean add);\n\n    /**\n     * 获取查询条件\n     *\n     * @param ${dataKey}QueryRequest\n     * @return\n     */\n    QueryWrapper<${upperDataKey}> getQueryWrapper(${upperDataKey}QueryRequest ${dataKey}QueryRequest);\n    \n    /**\n     * 获取${dataName}封装\n     *\n     * @param ${dataKey}\n     * @param request\n     * @return\n     */\n    ${upperDataKey}VO get${upperDataKey}VO(${upperDataKey} ${dataKey}, HttpServletRequest request);\n\n    /**\n     * 分页获取${dataName}封装\n     *\n     * @param ${dataKey}Page\n     * @param request\n     * @return\n     */\n    Page<${upperDataKey}VO> get${upperDataKey}VOPage(Page<${upperDataKey}> ${dataKey}Page, HttpServletRequest request);\n}\n"
  },
  {
    "path": "yudada-backend/src/main/resources/templates/TemplateServiceImpl.java.ftl",
    "content": "package ${packageName}.service.impl;\n\nimport cn.hutool.core.collection.CollUtil;\nimport com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;\nimport ${packageName}.common.ErrorCode;\nimport ${packageName}.constant.CommonConstant;\nimport ${packageName}.exception.ThrowUtils;\nimport ${packageName}.mapper.${upperDataKey}Mapper;\nimport ${packageName}.model.dto.${dataKey}.${upperDataKey}QueryRequest;\nimport ${packageName}.model.entity.${upperDataKey};\nimport ${packageName}.model.entity.${upperDataKey}Favour;\nimport ${packageName}.model.entity.${upperDataKey}Thumb;\nimport ${packageName}.model.entity.User;\nimport ${packageName}.model.vo.${upperDataKey}VO;\nimport ${packageName}.model.vo.UserVO;\nimport ${packageName}.service.${upperDataKey}Service;\nimport ${packageName}.service.UserService;\nimport ${packageName}.utils.SqlUtils;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.ObjectUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.stereotype.Service;\n\nimport javax.annotation.Resource;\nimport javax.servlet.http.HttpServletRequest;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\n/**\n * ${dataName}服务实现\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Service\n@Slf4j\npublic class ${upperDataKey}ServiceImpl extends ServiceImpl<${upperDataKey}Mapper, ${upperDataKey}> implements ${upperDataKey}Service {\n\n    @Resource\n    private UserService userService;\n\n    /**\n     * 校验数据\n     *\n     * @param ${dataKey}\n     * @param add      对创建的数据进行校验\n     */\n    @Override\n    public void valid${upperDataKey}(${upperDataKey} ${dataKey}, boolean add) {\n        ThrowUtils.throwIf(${dataKey} == null, ErrorCode.PARAMS_ERROR);\n        // todo 从对象中取值\n        String title = ${dataKey}.getTitle();\n        // 创建数据时，参数不能为空\n        if (add) {\n            // todo 补充校验规则\n            ThrowUtils.throwIf(StringUtils.isBlank(title), ErrorCode.PARAMS_ERROR);\n        }\n        // 修改数据时，有参数则校验\n        // todo 补充校验规则\n        if (StringUtils.isNotBlank(title)) {\n            ThrowUtils.throwIf(title.length() > 80, ErrorCode.PARAMS_ERROR, \"标题过长\");\n        }\n    }\n\n    /**\n     * 获取查询条件\n     *\n     * @param ${dataKey}QueryRequest\n     * @return\n     */\n    @Override\n    public QueryWrapper<${upperDataKey}> getQueryWrapper(${upperDataKey}QueryRequest ${dataKey}QueryRequest) {\n        QueryWrapper<${upperDataKey}> queryWrapper = new QueryWrapper<>();\n        if (${dataKey}QueryRequest == null) {\n            return queryWrapper;\n        }\n        // todo 从对象中取值\n        Long id = ${dataKey}QueryRequest.getId();\n        Long notId = ${dataKey}QueryRequest.getNotId();\n        String title = ${dataKey}QueryRequest.getTitle();\n        String content = ${dataKey}QueryRequest.getContent();\n        String searchText = ${dataKey}QueryRequest.getSearchText();\n        String sortField = ${dataKey}QueryRequest.getSortField();\n        String sortOrder = ${dataKey}QueryRequest.getSortOrder();\n        List<String> tagList = ${dataKey}QueryRequest.getTags();\n        Long userId = ${dataKey}QueryRequest.getUserId();\n        // todo 补充需要的查询条件\n        // 从多字段中搜索\n        if (StringUtils.isNotBlank(searchText)) {\n            // 需要拼接查询条件\n            queryWrapper.and(qw -> qw.like(\"title\", searchText).or().like(\"content\", searchText));\n        }\n        // 模糊查询\n        queryWrapper.like(StringUtils.isNotBlank(title), \"title\", title);\n        queryWrapper.like(StringUtils.isNotBlank(content), \"content\", content);\n        // JSON 数组查询\n        if (CollUtil.isNotEmpty(tagList)) {\n            for (String tag : tagList) {\n                queryWrapper.like(\"tags\", \"\\\"\" + tag + \"\\\"\");\n            }\n        }\n        // 精确查询\n        queryWrapper.ne(ObjectUtils.isNotEmpty(notId), \"id\", notId);\n        queryWrapper.eq(ObjectUtils.isNotEmpty(id), \"id\", id);\n        queryWrapper.eq(ObjectUtils.isNotEmpty(userId), \"userId\", userId);\n        // 排序规则\n        queryWrapper.orderBy(SqlUtils.validSortField(sortField),\n                sortOrder.equals(CommonConstant.SORT_ORDER_ASC),\n                sortField);\n        return queryWrapper;\n    }\n\n    /**\n     * 获取${dataName}封装\n     *\n     * @param ${dataKey}\n     * @param request\n     * @return\n     */\n    @Override\n    public ${upperDataKey}VO get${upperDataKey}VO(${upperDataKey} ${dataKey}, HttpServletRequest request) {\n        // 对象转封装类\n        ${upperDataKey}VO ${dataKey}VO = ${upperDataKey}VO.objToVo(${dataKey});\n\n        // todo 可以根据需要为封装对象补充值，不需要的内容可以删除\n        // region 可选\n        // 1. 关联查询用户信息\n        Long userId = ${dataKey}.getUserId();\n        User user = null;\n        if (userId != null && userId > 0) {\n            user = userService.getById(userId);\n        }\n        UserVO userVO = userService.getUserVO(user);\n        ${dataKey}VO.setUser(userVO);\n        // 2. 已登录，获取用户点赞、收藏状态\n        long ${dataKey}Id = ${dataKey}.getId();\n        User loginUser = userService.getLoginUserPermitNull(request);\n        if (loginUser != null) {\n            // 获取点赞\n            QueryWrapper<${upperDataKey}Thumb> ${dataKey}ThumbQueryWrapper = new QueryWrapper<>();\n            ${dataKey}ThumbQueryWrapper.in(\"${dataKey}Id\", ${dataKey}Id);\n            ${dataKey}ThumbQueryWrapper.eq(\"userId\", loginUser.getId());\n            ${upperDataKey}Thumb ${dataKey}Thumb = ${dataKey}ThumbMapper.selectOne(${dataKey}ThumbQueryWrapper);\n            ${dataKey}VO.setHasThumb(${dataKey}Thumb != null);\n            // 获取收藏\n            QueryWrapper<${upperDataKey}Favour> ${dataKey}FavourQueryWrapper = new QueryWrapper<>();\n            ${dataKey}FavourQueryWrapper.in(\"${dataKey}Id\", ${dataKey}Id);\n            ${dataKey}FavourQueryWrapper.eq(\"userId\", loginUser.getId());\n            ${upperDataKey}Favour ${dataKey}Favour = ${dataKey}FavourMapper.selectOne(${dataKey}FavourQueryWrapper);\n            ${dataKey}VO.setHasFavour(${dataKey}Favour != null);\n        }\n        // endregion\n\n        return ${dataKey}VO;\n    }\n\n    /**\n     * 分页获取${dataName}封装\n     *\n     * @param ${dataKey}Page\n     * @param request\n     * @return\n     */\n    @Override\n    public Page<${upperDataKey}VO> get${upperDataKey}VOPage(Page<${upperDataKey}> ${dataKey}Page, HttpServletRequest request) {\n        List<${upperDataKey}> ${dataKey}List = ${dataKey}Page.getRecords();\n        Page<${upperDataKey}VO> ${dataKey}VOPage = new Page<>(${dataKey}Page.getCurrent(), ${dataKey}Page.getSize(), ${dataKey}Page.getTotal());\n        if (CollUtil.isEmpty(${dataKey}List)) {\n            return ${dataKey}VOPage;\n        }\n        // 对象列表 => 封装对象列表\n        List<${upperDataKey}VO> ${dataKey}VOList = ${dataKey}List.stream().map(${dataKey} -> {\n            return ${upperDataKey}VO.objToVo(${dataKey});\n        }).collect(Collectors.toList());\n\n        // todo 可以根据需要为封装对象补充值，不需要的内容可以删除\n        // region 可选\n        // 1. 关联查询用户信息\n        Set<Long> userIdSet = ${dataKey}List.stream().map(${upperDataKey}::getUserId).collect(Collectors.toSet());\n        Map<Long, List<User>> userIdUserListMap = userService.listByIds(userIdSet).stream()\n                .collect(Collectors.groupingBy(User::getId));\n        // 2. 已登录，获取用户点赞、收藏状态\n        Map<Long, Boolean> ${dataKey}IdHasThumbMap = new HashMap<>();\n        Map<Long, Boolean> ${dataKey}IdHasFavourMap = new HashMap<>();\n        User loginUser = userService.getLoginUserPermitNull(request);\n        if (loginUser != null) {\n            Set<Long> ${dataKey}IdSet = ${dataKey}List.stream().map(${upperDataKey}::getId).collect(Collectors.toSet());\n            loginUser = userService.getLoginUser(request);\n            // 获取点赞\n            QueryWrapper<${upperDataKey}Thumb> ${dataKey}ThumbQueryWrapper = new QueryWrapper<>();\n            ${dataKey}ThumbQueryWrapper.in(\"${dataKey}Id\", ${dataKey}IdSet);\n            ${dataKey}ThumbQueryWrapper.eq(\"userId\", loginUser.getId());\n            List<${upperDataKey}Thumb> ${dataKey}${upperDataKey}ThumbList = ${dataKey}ThumbMapper.selectList(${dataKey}ThumbQueryWrapper);\n            ${dataKey}${upperDataKey}ThumbList.forEach(${dataKey}${upperDataKey}Thumb -> ${dataKey}IdHasThumbMap.put(${dataKey}${upperDataKey}Thumb.get${upperDataKey}Id(), true));\n            // 获取收藏\n            QueryWrapper<${upperDataKey}Favour> ${dataKey}FavourQueryWrapper = new QueryWrapper<>();\n            ${dataKey}FavourQueryWrapper.in(\"${dataKey}Id\", ${dataKey}IdSet);\n            ${dataKey}FavourQueryWrapper.eq(\"userId\", loginUser.getId());\n            List<${upperDataKey}Favour> ${dataKey}FavourList = ${dataKey}FavourMapper.selectList(${dataKey}FavourQueryWrapper);\n            ${dataKey}FavourList.forEach(${dataKey}Favour -> ${dataKey}IdHasFavourMap.put(${dataKey}Favour.get${upperDataKey}Id(), true));\n        }\n        // 填充信息\n        ${dataKey}VOList.forEach(${dataKey}VO -> {\n            Long userId = ${dataKey}VO.getUserId();\n            User user = null;\n            if (userIdUserListMap.containsKey(userId)) {\n                user = userIdUserListMap.get(userId).get(0);\n            }\n            ${dataKey}VO.setUser(userService.getUserVO(user));\n            ${dataKey}VO.setHasThumb(${dataKey}IdHasThumbMap.getOrDefault(${dataKey}VO.getId(), false));\n            ${dataKey}VO.setHasFavour(${dataKey}IdHasFavourMap.getOrDefault(${dataKey}VO.getId(), false));\n        });\n        // endregion\n\n        ${dataKey}VOPage.setRecords(${dataKey}VOList);\n        return ${dataKey}VOPage;\n    }\n\n}\n"
  },
  {
    "path": "yudada-backend/src/main/resources/templates/model/TemplateAddRequest.java.ftl",
    "content": "package ${packageName}.model.dto.${dataKey};\n\nimport lombok.Data;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n/**\n * 创建${dataName}请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Data\npublic class ${upperDataKey}AddRequest implements Serializable {\n\n    /**\n     * 标题\n     */\n    private String title;\n\n    /**\n     * 内容\n     */\n    private String content;\n\n    /**\n     * 标签列表\n     */\n    private List<String> tags;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/resources/templates/model/TemplateEditRequest.java.ftl",
    "content": "package ${packageName}.model.dto.${dataKey};\n\nimport lombok.Data;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n/**\n * 编辑${dataName}请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Data\npublic class ${upperDataKey}EditRequest implements Serializable {\n\n    /**\n     * id\n     */\n    private Long id;\n\n    /**\n     * 标题\n     */\n    private String title;\n\n    /**\n     * 内容\n     */\n    private String content;\n\n    /**\n     * 标签列表\n     */\n    private List<String> tags;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/resources/templates/model/TemplateQueryRequest.java.ftl",
    "content": "package ${packageName}.model.dto.${dataKey};\n\nimport ${packageName}.common.PageRequest;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n/**\n * 查询${dataName}请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@EqualsAndHashCode(callSuper = true)\n@Data\npublic class ${upperDataKey}QueryRequest extends PageRequest implements Serializable {\n\n    /**\n     * id\n     */\n    private Long id;\n\n    /**\n     * id\n     */\n    private Long notId;\n\n    /**\n     * 搜索词\n     */\n    private String searchText;\n\n    /**\n     * 标题\n     */\n    private String title;\n\n    /**\n     * 内容\n     */\n    private String content;\n\n    /**\n     * 标签列表\n     */\n    private List<String> tags;\n\n    /**\n     * 创建用户 id\n     */\n    private Long userId;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/resources/templates/model/TemplateUpdateRequest.java.ftl",
    "content": "package ${packageName}.model.dto.${dataKey};\n\nimport lombok.Data;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n/**\n * 更新${dataName}请求\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Data\npublic class ${upperDataKey}UpdateRequest implements Serializable {\n\n    /**\n     * id\n     */\n    private Long id;\n\n    /**\n     * 标题\n     */\n    private String title;\n\n    /**\n     * 内容\n     */\n    private String content;\n\n    /**\n     * 标签列表\n     */\n    private List<String> tags;\n\n    private static final long serialVersionUID = 1L;\n}"
  },
  {
    "path": "yudada-backend/src/main/resources/templates/model/TemplateVO.java.ftl",
    "content": "package ${packageName}.model.vo;\n\nimport cn.hutool.json.JSONUtil;\nimport ${packageName}.model.entity.${upperDataKey};\nimport lombok.Data;\nimport org.springframework.beans.BeanUtils;\n\nimport java.io.Serializable;\nimport java.util.Date;\nimport java.util.List;\n\n/**\n * ${dataName}视图\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://www.code-nav.cn\">编程导航学习圈</a>\n */\n@Data\npublic class ${upperDataKey}VO implements Serializable {\n\n    /**\n     * id\n     */\n    private Long id;\n\n    /**\n     * 标题\n     */\n    private String title;\n\n    /**\n     * 内容\n     */\n    private String content;\n\n    /**\n     * 创建用户 id\n     */\n    private Long userId;\n\n    /**\n     * 创建时间\n     */\n    private Date createTime;\n\n    /**\n     * 更新时间\n     */\n    private Date updateTime;\n\n    /**\n     * 标签列表\n     */\n    private List<String> tagList;\n\n    /**\n     * 创建用户信息\n     */\n    private UserVO user;\n\n    /**\n     * 封装类转对象\n     *\n     * @param ${dataKey}VO\n     * @return\n     */\n    public static ${upperDataKey} voToObj(${upperDataKey}VO ${dataKey}VO) {\n        if (${dataKey}VO == null) {\n            return null;\n        }\n        ${upperDataKey} ${dataKey} = new ${upperDataKey}();\n        BeanUtils.copyProperties(${dataKey}VO, ${dataKey});\n        List<String> tagList = ${dataKey}VO.getTagList();\n        ${dataKey}.setTags(JSONUtil.toJsonStr(tagList));\n        return ${dataKey};\n    }\n\n    /**\n     * 对象转封装类\n     *\n     * @param ${dataKey}\n     * @return\n     */\n    public static ${upperDataKey}VO objToVo(${upperDataKey} ${dataKey}) {\n        if (${dataKey} == null) {\n            return null;\n        }\n        ${upperDataKey}VO ${dataKey}VO = new ${upperDataKey}VO();\n        BeanUtils.copyProperties(${dataKey}, ${dataKey}VO);\n        ${dataKey}VO.setTagList(JSONUtil.toList(${dataKey}.getTags(), String.class));\n        return ${dataKey}VO;\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/test/java/com/yupi/yudada/MainApplicationTests.java",
    "content": "package com.yupi.yudada;\n\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.test.context.SpringBootTest;\n\n/**\n * 主类测试\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@SpringBootTest\nclass MainApplicationTests {\n\n    @Test\n    void contextLoads() {\n    }\n\n}\n"
  },
  {
    "path": "yudada-backend/src/test/java/com/yupi/yudada/QuestionControllerTest.java",
    "content": "package com.yupi.yudada;\n\nimport com.yupi.yudada.controller.QuestionController;\nimport com.yupi.yudada.model.dto.question.AiGenerateQuestionRequest;\nimport org.checkerframework.checker.units.qual.A;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.test.context.SpringBootTest;\n\nimport javax.annotation.Resource;\n\n@SpringBootTest\npublic class QuestionControllerTest {\n\n    @Resource\n    private QuestionController questionController;\n\n    @Test\n    void aiGenerateQuestionSSETest() throws InterruptedException {\n        // 模拟调用\n        AiGenerateQuestionRequest aiGenerateQuestionRequest = new AiGenerateQuestionRequest();\n        aiGenerateQuestionRequest.setAppId(3L);\n        aiGenerateQuestionRequest.setQuestionNumber(10);\n        aiGenerateQuestionRequest.setOptionNumber(2);\n\n        // 模拟普通用户\n        questionController.aiGenerateQuestionSSETest(aiGenerateQuestionRequest, false);\n        // 模拟普通用户\n        questionController.aiGenerateQuestionSSETest(aiGenerateQuestionRequest, false);\n        // 模拟会员用户\n        questionController.aiGenerateQuestionSSETest(aiGenerateQuestionRequest, true);\n\n        // 模拟主线程一直启动\n        Thread.sleep(1000000L);\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/test/java/com/yupi/yudada/RxJavaTest.java",
    "content": "package com.yupi.yudada;\n\nimport io.reactivex.Flowable;\nimport io.reactivex.schedulers.Schedulers;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.test.context.SpringBootTest;\n\nimport java.util.concurrent.TimeUnit;\n\n@SpringBootTest\npublic class RxJavaTest {\n\n    @Test\n    public void test() throws InterruptedException {\n        // 创建数据流\n        Flowable<Long> flowable = Flowable.interval(1, TimeUnit.SECONDS)\n                .map(i -> i + 1)\n                .subscribeOn(Schedulers.io()); // 指定执行操作用的线程池\n\n        // 订阅 Flowable 流，并且打印出每个接受到的数字\n        flowable\n                .observeOn(Schedulers.io())\n                .doOnNext(item -> System.out.println(item.toString()))\n                .subscribe();\n\n        // 主线程睡眠，以便观察到结果\n        Thread.sleep(10000L);\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/test/java/com/yupi/yudada/UserAnswerShardingTest.java",
    "content": "package com.yupi.yudada;\n\nimport cn.hutool.json.JSONUtil;\nimport com.baomidou.mybatisplus.core.toolkit.Wrappers;\nimport com.yupi.yudada.model.entity.UserAnswer;\nimport com.yupi.yudada.service.UserAnswerService;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.test.context.SpringBootTest;\n\nimport javax.annotation.Resource;\n\n@SpringBootTest\npublic class UserAnswerShardingTest {\n\n    @Resource\n    private UserAnswerService userAnswerService;\n\n    @Test\n    void test() {\n\n        UserAnswer userAnswer1 = new UserAnswer();\n        userAnswer1.setAppId(1L);\n        userAnswer1.setUserId(1L);\n        userAnswer1.setChoices(\"1\");\n        userAnswerService.save(userAnswer1);\n\n        UserAnswer userAnswer2 = new UserAnswer();\n        userAnswer2.setAppId(2L);\n        userAnswer2.setUserId(1L);\n        userAnswer2.setChoices(\"2\");\n        userAnswerService.save(userAnswer2);\n\n        UserAnswer userAnswerOne = userAnswerService.getOne(Wrappers.lambdaQuery(UserAnswer.class).eq(UserAnswer::getAppId, 1L));\n        System.out.println(JSONUtil.toJsonStr(userAnswerOne));\n\n        UserAnswer userAnswerTwo = userAnswerService.getOne(Wrappers.lambdaQuery(UserAnswer.class).eq(UserAnswer::getAppId, 2L));\n        System.out.println(JSONUtil.toJsonStr(userAnswerTwo));\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/test/java/com/yupi/yudada/ZhiPuAiTest.java",
    "content": "package com.yupi.yudada;\n\nimport com.zhipu.oapi.ClientV4;\nimport com.zhipu.oapi.Constants;\nimport com.zhipu.oapi.service.v4.model.ChatCompletionRequest;\nimport com.zhipu.oapi.service.v4.model.ChatMessage;\nimport com.zhipu.oapi.service.v4.model.ChatMessageRole;\nimport com.zhipu.oapi.service.v4.model.ModelApiResponse;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.test.context.SpringBootTest;\n\nimport javax.annotation.Resource;\nimport java.util.ArrayList;\nimport java.util.List;\n\n@SpringBootTest\npublic class ZhiPuAiTest {\n\n    @Resource\n    private ClientV4 clientV4;\n\n    @Test\n    public void test() {\n        // 初始化客户端\n//        ClientV4 client = new ClientV4.Builder(KeyConstant.KEY).build();\n        // 构建请求\n        List<ChatMessage> messages = new ArrayList<>();\n        ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), \"作为一名营销专家，请为智谱开放平台创作一个吸引人的slogan\");\n        messages.add(chatMessage);\n//        String requestId = String.format(requestIdTemplate, System.currentTimeMillis());\n        ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()\n                .model(Constants.ModelChatGLM4)\n                .stream(Boolean.FALSE)\n                .invokeMethod(Constants.invokeMethod)\n                .messages(messages)\n                .build();\n        ModelApiResponse invokeModelApiResp = clientV4.invokeModelApi(chatCompletionRequest);\n        System.out.println(\"model output:\" + invokeModelApiResp.getData().getChoices().get(0));\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/test/java/com/yupi/yudada/manager/CosManagerTest.java",
    "content": "package com.yupi.yudada.manager;\n\nimport javax.annotation.Resource;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.test.context.SpringBootTest;\n\n/**\n * Cos 操作测试\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@SpringBootTest\nclass CosManagerTest {\n\n    @Resource\n    private CosManager cosManager;\n\n    @Test\n    void putObject() {\n        cosManager.putObject(\"test\", \"test.json\");\n    }\n}"
  },
  {
    "path": "yudada-backend/src/test/java/com/yupi/yudada/mapper/PostFavourMapperTest.java",
    "content": "package com.yupi.yudada.mapper;\n\nimport com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;\nimport com.baomidou.mybatisplus.core.metadata.IPage;\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.yupi.yudada.model.entity.Post;\nimport javax.annotation.Resource;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.test.context.SpringBootTest;\n\n/**\n * 帖子收藏数据库操作测试\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@SpringBootTest\nclass PostFavourMapperTest {\n\n    @Resource\n    private PostFavourMapper postFavourMapper;\n\n    @Test\n    void listUserFavourPostByPage() {\n        IPage<Post> page = new Page<>(2, 1);\n        QueryWrapper<Post> queryWrapper = new QueryWrapper<>();\n        queryWrapper.eq(\"id\", 1);\n        queryWrapper.like(\"content\", \"a\");\n        IPage<Post> result = postFavourMapper.listFavourPostByPage(page, queryWrapper, 1);\n        Assertions.assertNotNull(result);\n    }\n}"
  },
  {
    "path": "yudada-backend/src/test/java/com/yupi/yudada/mapper/PostMapperTest.java",
    "content": "package com.yupi.yudada.mapper;\n\nimport com.yupi.yudada.model.entity.Post;\nimport java.util.Date;\nimport java.util.List;\nimport javax.annotation.Resource;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.test.context.SpringBootTest;\n\n/**\n * 帖子数据库操作测试\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@SpringBootTest\nclass PostMapperTest {\n\n    @Resource\n    private PostMapper postMapper;\n\n    @Test\n    void listPostWithDelete() {\n        List<Post> postList = postMapper.listPostWithDelete(new Date());\n        Assertions.assertNotNull(postList);\n    }\n}"
  },
  {
    "path": "yudada-backend/src/test/java/com/yupi/yudada/service/PostFavourServiceTest.java",
    "content": "package com.yupi.yudada.service;\n\nimport com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;\nimport com.baomidou.mybatisplus.extension.plugins.pagination.Page;\nimport com.yupi.yudada.model.entity.Post;\nimport com.yupi.yudada.model.entity.User;\nimport javax.annotation.Resource;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.test.context.SpringBootTest;\n\n/**\n * 帖子收藏服务测试\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@SpringBootTest\nclass PostFavourServiceTest {\n\n    @Resource\n    private PostFavourService postFavourService;\n\n    private static final User loginUser = new User();\n\n    @BeforeAll\n    static void setUp() {\n        loginUser.setId(1L);\n    }\n\n    @Test\n    void doPostFavour() {\n        int i = postFavourService.doPostFavour(1L, loginUser);\n        Assertions.assertTrue(i >= 0);\n    }\n\n    @Test\n    void listFavourPostByPage() {\n        QueryWrapper<Post> postQueryWrapper = new QueryWrapper<>();\n        postQueryWrapper.eq(\"id\", 1L);\n        postFavourService.listFavourPostByPage(Page.of(0, 1), postQueryWrapper, loginUser.getId());\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/test/java/com/yupi/yudada/service/PostThumbServiceTest.java",
    "content": "package com.yupi.yudada.service;\n\nimport com.yupi.yudada.model.entity.User;\nimport javax.annotation.Resource;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.test.context.SpringBootTest;\n\n/**\n * 帖子点赞服务测试\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@SpringBootTest\nclass PostThumbServiceTest {\n\n    @Resource\n    private PostThumbService postThumbService;\n\n    private static final User loginUser = new User();\n\n    @BeforeAll\n    static void setUp() {\n        loginUser.setId(1L);\n    }\n\n    @Test\n    void doPostThumb() {\n        int i = postThumbService.doPostThumb(1L, loginUser);\n        Assertions.assertTrue(i >= 0);\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/test/java/com/yupi/yudada/service/UserServiceTest.java",
    "content": "package com.yupi.yudada.service;\n\nimport javax.annotation.Resource;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.test.context.SpringBootTest;\n\n/**\n * 用户服务测试\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@SpringBootTest\npublic class UserServiceTest {\n\n    @Resource\n    private UserService userService;\n\n    @Test\n    void userRegister() {\n        String userAccount = \"yupi\";\n        String userPassword = \"\";\n        String checkPassword = \"123456\";\n        try {\n            long result = userService.userRegister(userAccount, userPassword, checkPassword);\n            Assertions.assertEquals(-1, result);\n            userAccount = \"yu\";\n            result = userService.userRegister(userAccount, userPassword, checkPassword);\n            Assertions.assertEquals(-1, result);\n        } catch (Exception e) {\n\n        }\n    }\n}\n"
  },
  {
    "path": "yudada-backend/src/test/java/com/yupi/yudada/utils/EasyExcelTest.java",
    "content": "package com.yupi.yudada.utils;\n\nimport com.alibaba.excel.EasyExcel;\nimport com.alibaba.excel.support.ExcelTypeEnum;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.util.ResourceUtils;\n\nimport java.io.File;\nimport java.io.FileNotFoundException;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * EasyExcel 测试\n *\n * @author <a href=\"https://github.com/liyupi\">程序员鱼皮</a>\n * @from <a href=\"https://yupi.icu\">编程导航知识星球</a>\n */\n@SpringBootTest\npublic class EasyExcelTest {\n\n    @Test\n    public void doImport() throws FileNotFoundException {\n        File file = ResourceUtils.getFile(\"classpath:test_excel.xlsx\");\n        List<Map<Integer, String>> list = EasyExcel.read(file)\n                .excelType(ExcelTypeEnum.XLSX)\n                .sheet()\n                .headRowNumber(0)\n                .doReadSync();\n        System.out.println(list);\n    }\n\n}"
  },
  {
    "path": "yudada-frontend/.browserslistrc",
    "content": "> 1%\nlast 2 versions\nnot dead\nnot ie 11\n"
  },
  {
    "path": "yudada-frontend/.eslintrc.js",
    "content": "module.exports = {\n  root: true,\n  env: {\n    node: true,\n  },\n  extends: [\n    \"plugin:vue/vue3-essential\",\n    \"eslint:recommended\",\n    \"@vue/typescript/recommended\",\n    \"plugin:prettier/recommended\",\n  ],\n  parserOptions: {\n    ecmaVersion: 2020,\n  },\n  rules: {\n    \"no-console\": process.env.NODE_ENV === \"production\" ? \"warn\" : \"off\",\n    \"no-debugger\": process.env.NODE_ENV === \"production\" ? \"warn\" : \"off\",\n    \"@typescript-eslint/ban-ts-comment\": \"off\",\n  },\n};\n"
  },
  {
    "path": "yudada-frontend/.gitignore",
    "content": ".DS_Store\nnode_modules\n/dist\n\n\n# local env files\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\n\n# Editor directories and files\n.idea\n.vscode\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "yudada-frontend/Dockerfile",
    "content": "# 第一个阶段：构建阶段，使用 Node.js 16 的 Alpine 镜像作为基础镜像，并命名为 build-stage\nFROM node:16-alpine as build-stage\n\n# 设置工作目录为 /app\nWORKDIR /app\n\n# 将项目源代码（包括 package.json）复制到工作目录\nCOPY . .\n\n# 设置 npm registry 地址为国内源\nRUN npm config set registry https://registry.npmmirror.com\n\n# 使用 npm 安装项目依赖\nRUN npm install --force\n\n# 运行构建命令\nRUN npm run build\n\n# 第二个阶段：运行 Nginx 提供 web 服务\nFROM nginx:1.22.1 as server-stage\n\nUSER root\n\nCOPY ./docker/nginx.conf /etc/nginx/conf.d/default.conf\n\nCOPY --from=build-stage /app/dist /usr/share/nginx/html\n\nEXPOSE 80\n\nCMD [\"nginx\", \"-g\", \"daemon off;\"]"
  },
  {
    "path": "yudada-frontend/README.md",
    "content": "# yudada-frontend\n\n## Project setup\n```\nnpm install\n```\n\n### Compiles and hot-reloads for development\n```\nnpm run serve\n```\n\n### Compiles and minifies for production\n```\nnpm run build\n```\n\n### Lints and fixes files\n```\nnpm run lint\n```\n\n### Customize configuration\nSee [Configuration Reference](https://cli.vuejs.org/config/).\n"
  },
  {
    "path": "yudada-frontend/babel.config.js",
    "content": "module.exports = {\n  presets: [\"@vue/cli-plugin-babel/preset\"],\n};\n"
  },
  {
    "path": "yudada-frontend/docker/nginx.conf",
    "content": "server {\n    listen 80;\n\n    # gzip config\n    gzip on;\n    gzip_min_length 1k;\n    gzip_comp_level 9;\n    gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml;\n    gzip_vary on;\n    gzip_disable \"MSIE [1-6]\\.\";\n\n    root /usr/share/nginx/html;\n    include /etc/nginx/mime.types;\n\n    location / {\n        try_files $uri index.html;\n    }\n}"
  },
  {
    "path": "yudada-frontend/openapi.config.ts",
    "content": "const { generateService } = require(\"@umijs/openapi\");\n\ngenerateService({\n  requestLibPath: \"import request from '@/request'\",\n  schemaPath: \"http://localhost:8101/api/v2/api-docs\",\n  serversPath: \"./src\",\n});\n"
  },
  {
    "path": "yudada-frontend/package.json",
    "content": "{\n  \"name\": \"yudada-frontend\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"serve\": \"vue-cli-service serve\",\n    \"build\": \"vue-cli-service build\",\n    \"lint\": \"vue-cli-service lint\",\n    \"openapi\": \"node openapi.config.ts\"\n  },\n  \"dependencies\": {\n    \"@bytemd/plugin-gfm\": \"^1.21.0\",\n    \"@bytemd/plugin-highlight\": \"^1.21.0\",\n    \"@bytemd/vue-next\": \"^1.21.0\",\n    \"axios\": \"^1.6.8\",\n    \"core-js\": \"^3.8.3\",\n    \"echarts\": \"^5.5.0\",\n    \"pinia\": \"^2.1.7\",\n    \"qrcode\": \"^1.5.3\",\n    \"vue\": \"^3.2.13\",\n    \"vue-echarts\": \"^6.7.2\",\n    \"vue-router\": \"^4.0.3\"\n  },\n  \"devDependencies\": {\n    \"@arco-design/web-vue\": \"^2.55.2\",\n    \"@types/qrcode\": \"^1.5.5\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.4.0\",\n    \"@typescript-eslint/parser\": \"^5.4.0\",\n    \"@umijs/openapi\": \"^1.12.1\",\n    \"@vue/cli-plugin-babel\": \"~5.0.0\",\n    \"@vue/cli-plugin-eslint\": \"~5.0.0\",\n    \"@vue/cli-plugin-router\": \"~5.0.0\",\n    \"@vue/cli-plugin-typescript\": \"~5.0.0\",\n    \"@vue/cli-service\": \"~5.0.0\",\n    \"@vue/eslint-config-typescript\": \"^9.1.0\",\n    \"eslint\": \"^7.32.0\",\n    \"eslint-config-prettier\": \"^8.3.0\",\n    \"eslint-plugin-prettier\": \"^4.0.0\",\n    \"eslint-plugin-vue\": \"^8.0.3\",\n    \"prettier\": \"^2.4.1\",\n    \"typescript\": \"~4.5.5\"\n  }\n}\n"
  },
  {
    "path": "yudada-frontend/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\n    <link rel=\"icon\" href=\"<%= BASE_URL %>favicon.ico\">\n    <title><%= htmlWebpackPlugin.options.title %></title>\n  </head>\n  <body>\n    <noscript>\n      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>\n    </noscript>\n    <div id=\"app\"></div>\n    <!-- built files will be auto injected -->\n  </body>\n</html>\n"
  },
  {
    "path": "yudada-frontend/src/App.vue",
    "content": "<template>\n  <div id=\"app\">\n    <template v-if=\"route.path.startsWith('/user')\">\n      <router-view />\n    </template>\n    <template v-else>\n      <BasicLayout />\n    </template>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport BasicLayout from \"@/layouts/BasicLayout.vue\";\nimport { useRoute } from \"vue-router\";\nimport { onMounted } from \"vue\";\n\nconst route = useRoute();\n\n/**\n * 全局初始化函数，有全局单次调用的代码，都可以写到这里\n */\nconst doInit = () => {\n  console.log(\"hello 欢迎来到我的项目\");\n};\n\nonMounted(() => {\n  doInit();\n});\n</script>\n\n<style scoped></style>\n"
  },
  {
    "path": "yudada-frontend/src/access/accessEnum.ts",
    "content": "/**\n * 权限定义\n */\nconst ACCESS_ENUM = {\n  NOT_LOGIN: \"notLogin\",\n  USER: \"user\",\n  ADMIN: \"admin\",\n};\n\nexport default ACCESS_ENUM;\n"
  },
  {
    "path": "yudada-frontend/src/access/checkAccess.ts",
    "content": "import ACCESS_ENUM from \"@/access/accessEnum\";\n\n/**\n * 检查权限（判断当前登录用户是否具有某个权限）\n * @param loginUser 当前登录用户\n * @param needAccess 需要有的权限\n * @return boolean 有无权限\n */\nconst checkAccess = (\n  loginUser: API.LoginUserVO,\n  needAccess = ACCESS_ENUM.NOT_LOGIN\n) => {\n  // 获取当前登录用户具有的权限（如果没有 loginUser，则表示未登录）\n  const loginUserAccess = loginUser?.userRole ?? ACCESS_ENUM.NOT_LOGIN;\n  if (needAccess === ACCESS_ENUM.NOT_LOGIN) {\n    return true;\n  }\n  // 如果用户要登录才能访问\n  if (needAccess === ACCESS_ENUM.USER) {\n    // 如果用户没登录，那么表示无权限\n    if (loginUserAccess === ACCESS_ENUM.NOT_LOGIN) {\n      return false;\n    }\n  }\n  // 如果管理员才能访问\n  if (needAccess === ACCESS_ENUM.ADMIN) {\n    // 如果不是管理员，表示无权限\n    if (loginUserAccess !== ACCESS_ENUM.ADMIN) {\n      return false;\n    }\n  }\n  return true;\n};\n\nexport default checkAccess;\n"
  },
  {
    "path": "yudada-frontend/src/access/index.ts",
    "content": "import router from \"@/router\";\nimport { useLoginUserStore } from \"@/store/userStore\";\nimport ACCESS_ENUM from \"@/access/accessEnum\";\nimport checkAccess from \"@/access/checkAccess\";\n\n// 进入页面前，进行权限校验\nrouter.beforeEach(async (to, from, next) => {\n  // 获取当前登录用户\n  const loginUserStore = useLoginUserStore();\n  let loginUser = loginUserStore.loginUser;\n\n  // 如果之前没有尝试获取过登录用户信息，才自动登录\n  if (!loginUser || !loginUser.userRole) {\n    // 加 await 是为了等待用户登录成功并获取到值后，再执行后续操作\n    await loginUserStore.fetchLoginUser();\n    loginUser = loginUserStore.loginUser;\n  }\n\n  // 当前页面需要的权限\n  const needAccess = (to.meta?.access as string) ?? ACCESS_ENUM.NOT_LOGIN;\n  // 要跳转的页面必须登录\n  if (needAccess !== ACCESS_ENUM.NOT_LOGIN) {\n    // 如果没登录，跳转到登录页面\n    if (\n      !loginUser ||\n      !loginUser.userRole ||\n      loginUser.userRole === ACCESS_ENUM.NOT_LOGIN\n    ) {\n      next(`/user/login?redirect=${to.fullPath}`);\n    }\n    // 如果已经登录了，判断权限是否足够，如果不足，跳转到无权限页面\n    if (!checkAccess(loginUser, needAccess)) {\n      next(\"/noAuth\");\n      return;\n    }\n  }\n  next();\n});\n"
  },
  {
    "path": "yudada-frontend/src/api/appController.ts",
    "content": "// @ts-ignore\n/* eslint-disable */\nimport request from '@/request';\n\n/** addApp POST /api/app/add */\nexport async function addAppUsingPost(body: API.AppAddRequest, options?: { [key: string]: any }) {\n  return request<API.BaseResponseLong_>('/api/app/add', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** deleteApp POST /api/app/delete */\nexport async function deleteAppUsingPost(\n  body: API.DeleteRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseBoolean_>('/api/app/delete', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** editApp POST /api/app/edit */\nexport async function editAppUsingPost(body: API.AppEditRequest, options?: { [key: string]: any }) {\n  return request<API.BaseResponseBoolean_>('/api/app/edit', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** getAppVOById GET /api/app/get/vo */\nexport async function getAppVoByIdUsingGet(\n  // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)\n  params: API.getAppVOByIdUsingGETParams,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseAppVO_>('/api/app/get/vo', {\n    method: 'GET',\n    params: {\n      ...params,\n    },\n    ...(options || {}),\n  });\n}\n\n/** listAppByPage POST /api/app/list/page */\nexport async function listAppByPageUsingPost(\n  body: API.AppQueryRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponsePageApp_>('/api/app/list/page', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** listAppVOByPage POST /api/app/list/page/vo */\nexport async function listAppVoByPageUsingPost(\n  body: API.AppQueryRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponsePageAppVO_>('/api/app/list/page/vo', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** listMyAppVOByPage POST /api/app/my/list/page/vo */\nexport async function listMyAppVoByPageUsingPost(\n  body: API.AppQueryRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponsePageAppVO_>('/api/app/my/list/page/vo', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** doAppReview POST /api/app/review */\nexport async function doAppReviewUsingPost(\n  body: API.ReviewRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseBoolean_>('/api/app/review', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** updateApp POST /api/app/update */\nexport async function updateAppUsingPost(\n  body: API.AppUpdateRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseBoolean_>('/api/app/update', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n"
  },
  {
    "path": "yudada-frontend/src/api/appStatisticController.ts",
    "content": "// @ts-ignore\n/* eslint-disable */\nimport request from '@/request';\n\n/** getAppAnswerCount GET /api/app/statistic/answer_count */\nexport async function getAppAnswerCountUsingGet(options?: { [key: string]: any }) {\n  return request<API.BaseResponseListAppAnswerCountDTO_>('/api/app/statistic/answer_count', {\n    method: 'GET',\n    ...(options || {}),\n  });\n}\n\n/** getAppAnswerResultCount GET /api/app/statistic/answer_result_count */\nexport async function getAppAnswerResultCountUsingGet(\n  // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)\n  params: API.getAppAnswerResultCountUsingGETParams,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseListAppAnswerResultCountDTO_>(\n    '/api/app/statistic/answer_result_count',\n    {\n      method: 'GET',\n      params: {\n        ...params,\n      },\n      ...(options || {}),\n    },\n  );\n}\n"
  },
  {
    "path": "yudada-frontend/src/api/fileController.ts",
    "content": "// @ts-ignore\n/* eslint-disable */\nimport request from '@/request';\n\n/** uploadFile POST /api/file/upload */\nexport async function uploadFileUsingPost(\n  // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)\n  params: API.uploadFileUsingPOSTParams,\n  body: {},\n  file?: File,\n  options?: { [key: string]: any },\n) {\n  const formData = new FormData();\n\n  if (file) {\n    formData.append('file', file);\n  }\n\n  Object.keys(body).forEach((ele) => {\n    const item = (body as any)[ele];\n\n    if (item !== undefined && item !== null) {\n      if (typeof item === 'object' && !(item instanceof File)) {\n        if (item instanceof Array) {\n          item.forEach((f) => formData.append(ele, f || ''));\n        } else {\n          formData.append(ele, JSON.stringify(item));\n        }\n      } else {\n        formData.append(ele, item);\n      }\n    }\n  });\n\n  return request<API.BaseResponseString_>('/api/file/upload', {\n    method: 'POST',\n    params: {\n      ...params,\n    },\n    data: formData,\n    // @ts-ignore\n    requestType: 'form',\n    ...(options || {}),\n  });\n}\n"
  },
  {
    "path": "yudada-frontend/src/api/index.ts",
    "content": "// @ts-ignore\n/* eslint-disable */\n// API 更新时间：\n// API 唯一标识：\nimport * as appController from './appController';\nimport * as appStatisticController from './appStatisticController';\nimport * as fileController from './fileController';\nimport * as postController from './postController';\nimport * as postFavourController from './postFavourController';\nimport * as postThumbController from './postThumbController';\nimport * as questionController from './questionController';\nimport * as scoringResultController from './scoringResultController';\nimport * as userController from './userController';\nimport * as userAnswerController from './userAnswerController';\nexport default {\n  appController,\n  appStatisticController,\n  fileController,\n  postController,\n  postFavourController,\n  postThumbController,\n  questionController,\n  scoringResultController,\n  userController,\n  userAnswerController,\n};\n"
  },
  {
    "path": "yudada-frontend/src/api/postController.ts",
    "content": "// @ts-ignore\n/* eslint-disable */\nimport request from '@/request';\n\n/** addPost POST /api/post/add */\nexport async function addPostUsingPost(body: API.PostAddRequest, options?: { [key: string]: any }) {\n  return request<API.BaseResponseLong_>('/api/post/add', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** deletePost POST /api/post/delete */\nexport async function deletePostUsingPost(\n  body: API.DeleteRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseBoolean_>('/api/post/delete', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** editPost POST /api/post/edit */\nexport async function editPostUsingPost(\n  body: API.PostEditRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseBoolean_>('/api/post/edit', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** getPostVOById GET /api/post/get/vo */\nexport async function getPostVoByIdUsingGet(\n  // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)\n  params: API.getPostVOByIdUsingGETParams,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponsePostVO_>('/api/post/get/vo', {\n    method: 'GET',\n    params: {\n      ...params,\n    },\n    ...(options || {}),\n  });\n}\n\n/** listPostByPage POST /api/post/list/page */\nexport async function listPostByPageUsingPost(\n  body: API.PostQueryRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponsePagePost_>('/api/post/list/page', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** listPostVOByPage POST /api/post/list/page/vo */\nexport async function listPostVoByPageUsingPost(\n  body: API.PostQueryRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponsePagePostVO_>('/api/post/list/page/vo', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** listMyPostVOByPage POST /api/post/my/list/page/vo */\nexport async function listMyPostVoByPageUsingPost(\n  body: API.PostQueryRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponsePagePostVO_>('/api/post/my/list/page/vo', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** updatePost POST /api/post/update */\nexport async function updatePostUsingPost(\n  body: API.PostUpdateRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseBoolean_>('/api/post/update', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n"
  },
  {
    "path": "yudada-frontend/src/api/postFavourController.ts",
    "content": "// @ts-ignore\n/* eslint-disable */\nimport request from '@/request';\n\n/** doPostFavour POST /api/post_favour/ */\nexport async function doPostFavourUsingPost(\n  body: API.PostFavourAddRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseInt_>('/api/post_favour/', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** listFavourPostByPage POST /api/post_favour/list/page */\nexport async function listFavourPostByPageUsingPost(\n  body: API.PostFavourQueryRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponsePagePostVO_>('/api/post_favour/list/page', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** listMyFavourPostByPage POST /api/post_favour/my/list/page */\nexport async function listMyFavourPostByPageUsingPost(\n  body: API.PostQueryRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponsePagePostVO_>('/api/post_favour/my/list/page', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n"
  },
  {
    "path": "yudada-frontend/src/api/postThumbController.ts",
    "content": "// @ts-ignore\n/* eslint-disable */\nimport request from '@/request';\n\n/** doThumb POST /api/post_thumb/ */\nexport async function doThumbUsingPost(\n  body: API.PostThumbAddRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseInt_>('/api/post_thumb/', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n"
  },
  {
    "path": "yudada-frontend/src/api/questionController.ts",
    "content": "// @ts-ignore\n/* eslint-disable */\nimport request from '@/request';\n\n/** addQuestion POST /api/question/add */\nexport async function addQuestionUsingPost(\n  body: API.QuestionAddRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseLong_>('/api/question/add', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** aiGenerateQuestion POST /api/question/ai_generate */\nexport async function aiGenerateQuestionUsingPost(\n  body: API.AiGenerateQuestionRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseListQuestionContentDTO_>('/api/question/ai_generate', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** aiGenerateQuestionSSE GET /api/question/ai_generate/sse */\nexport async function aiGenerateQuestionSseUsingGet(\n  // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)\n  params: API.aiGenerateQuestionSSEUsingGETParams,\n  options?: { [key: string]: any },\n) {\n  return request<API.SseEmitter>('/api/question/ai_generate/sse', {\n    method: 'GET',\n    params: {\n      ...params,\n    },\n    ...(options || {}),\n  });\n}\n\n/** aiGenerateQuestionSSETest GET /api/question/ai_generate/sse/test */\nexport async function aiGenerateQuestionSseTestUsingGet(\n  // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)\n  params: API.aiGenerateQuestionSSETestUsingGETParams,\n  options?: { [key: string]: any },\n) {\n  return request<API.SseEmitter>('/api/question/ai_generate/sse/test', {\n    method: 'GET',\n    params: {\n      ...params,\n    },\n    ...(options || {}),\n  });\n}\n\n/** deleteQuestion POST /api/question/delete */\nexport async function deleteQuestionUsingPost(\n  body: API.DeleteRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseBoolean_>('/api/question/delete', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** editQuestion POST /api/question/edit */\nexport async function editQuestionUsingPost(\n  body: API.QuestionEditRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseBoolean_>('/api/question/edit', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** getQuestionVOById GET /api/question/get/vo */\nexport async function getQuestionVoByIdUsingGet(\n  // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)\n  params: API.getQuestionVOByIdUsingGETParams,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseQuestionVO_>('/api/question/get/vo', {\n    method: 'GET',\n    params: {\n      ...params,\n    },\n    ...(options || {}),\n  });\n}\n\n/** listQuestionByPage POST /api/question/list/page */\nexport async function listQuestionByPageUsingPost(\n  body: API.QuestionQueryRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponsePageQuestion_>('/api/question/list/page', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** listQuestionVOByPage POST /api/question/list/page/vo */\nexport async function listQuestionVoByPageUsingPost(\n  body: API.QuestionQueryRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponsePageQuestionVO_>('/api/question/list/page/vo', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** listMyQuestionVOByPage POST /api/question/my/list/page/vo */\nexport async function listMyQuestionVoByPageUsingPost(\n  body: API.QuestionQueryRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponsePageQuestionVO_>('/api/question/my/list/page/vo', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** updateQuestion POST /api/question/update */\nexport async function updateQuestionUsingPost(\n  body: API.QuestionUpdateRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseBoolean_>('/api/question/update', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n"
  },
  {
    "path": "yudada-frontend/src/api/scoringResultController.ts",
    "content": "// @ts-ignore\n/* eslint-disable */\nimport request from '@/request';\n\n/** addScoringResult POST /api/scoringResult/add */\nexport async function addScoringResultUsingPost(\n  body: API.ScoringResultAddRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseLong_>('/api/scoringResult/add', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** deleteScoringResult POST /api/scoringResult/delete */\nexport async function deleteScoringResultUsingPost(\n  body: API.DeleteRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseBoolean_>('/api/scoringResult/delete', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** editScoringResult POST /api/scoringResult/edit */\nexport async function editScoringResultUsingPost(\n  body: API.ScoringResultEditRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseBoolean_>('/api/scoringResult/edit', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** getScoringResultVOById GET /api/scoringResult/get/vo */\nexport async function getScoringResultVoByIdUsingGet(\n  // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)\n  params: API.getScoringResultVOByIdUsingGETParams,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseScoringResultVO_>('/api/scoringResult/get/vo', {\n    method: 'GET',\n    params: {\n      ...params,\n    },\n    ...(options || {}),\n  });\n}\n\n/** listScoringResultByPage POST /api/scoringResult/list/page */\nexport async function listScoringResultByPageUsingPost(\n  body: API.ScoringResultQueryRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponsePageScoringResult_>('/api/scoringResult/list/page', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** listScoringResultVOByPage POST /api/scoringResult/list/page/vo */\nexport async function listScoringResultVoByPageUsingPost(\n  body: API.ScoringResultQueryRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponsePageScoringResultVO_>('/api/scoringResult/list/page/vo', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** listMyScoringResultVOByPage POST /api/scoringResult/my/list/page/vo */\nexport async function listMyScoringResultVoByPageUsingPost(\n  body: API.ScoringResultQueryRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponsePageScoringResultVO_>('/api/scoringResult/my/list/page/vo', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** updateScoringResult POST /api/scoringResult/update */\nexport async function updateScoringResultUsingPost(\n  body: API.ScoringResultUpdateRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseBoolean_>('/api/scoringResult/update', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n"
  },
  {
    "path": "yudada-frontend/src/api/typings.d.ts",
    "content": "declare namespace API {\n  type AiGenerateQuestionRequest = {\n    appId?: number;\n    optionNumber?: number;\n    questionNumber?: number;\n  };\n\n  type aiGenerateQuestionSSETestUsingGETParams = {\n    appId?: number;\n    optionNumber?: number;\n    questionNumber?: number;\n    /** isVip */\n    isVip?: boolean;\n  };\n\n  type aiGenerateQuestionSSEUsingGETParams = {\n    appId?: number;\n    optionNumber?: number;\n    questionNumber?: number;\n  };\n\n  type App = {\n    appDesc?: string;\n    appIcon?: string;\n    appName?: string;\n    appType?: number;\n    createTime?: string;\n    id?: number;\n    isDelete?: number;\n    reviewMessage?: string;\n    reviewStatus?: number;\n    reviewTime?: string;\n    reviewerId?: number;\n    scoringStrategy?: number;\n    updateTime?: string;\n    userId?: number;\n  };\n\n  type AppAddRequest = {\n    appDesc?: string;\n    appIcon?: string;\n    appName?: string;\n    appType?: number;\n    scoringStrategy?: number;\n  };\n\n  type AppAnswerCountDTO = {\n    answerCount?: number;\n    appId?: number;\n  };\n\n  type AppAnswerResultCountDTO = {\n    resultCount?: string;\n    resultName?: string;\n  };\n\n  type AppEditRequest = {\n    appDesc?: string;\n    appIcon?: string;\n    appName?: string;\n    appType?: number;\n    id?: number;\n    scoringStrategy?: number;\n  };\n\n  type AppQueryRequest = {\n    appDesc?: string;\n    appIcon?: string;\n    appName?: string;\n    appType?: number;\n    current?: number;\n    id?: number;\n    notId?: number;\n    pageSize?: number;\n    reviewMessage?: string;\n    reviewStatus?: number;\n    reviewerId?: number;\n    scoringStrategy?: number;\n    searchText?: string;\n    sortField?: string;\n    sortOrder?: string;\n    userId?: number;\n  };\n\n  type AppUpdateRequest = {\n    appDesc?: string;\n    appIcon?: string;\n    appName?: string;\n    appType?: number;\n    id?: number;\n    reviewMessage?: string;\n    reviewStatus?: number;\n    reviewTime?: string;\n    reviewerId?: number;\n    scoringStrategy?: number;\n  };\n\n  type AppVO = {\n    appDesc?: string;\n    appIcon?: string;\n    appName?: string;\n    appType?: number;\n    createTime?: string;\n    id?: number;\n    reviewMessage?: string;\n    reviewStatus?: number;\n    reviewTime?: string;\n    reviewerId?: number;\n    scoringStrategy?: number;\n    updateTime?: string;\n    user?: UserVO;\n    userId?: number;\n  };\n\n  type BaseResponseAppVO_ = {\n    code?: number;\n    data?: AppVO;\n    message?: string;\n  };\n\n  type BaseResponseBoolean_ = {\n    code?: number;\n    data?: boolean;\n    message?: string;\n  };\n\n  type BaseResponseInt_ = {\n    code?: number;\n    data?: number;\n    message?: string;\n  };\n\n  type BaseResponseListAppAnswerCountDTO_ = {\n    code?: number;\n    data?: AppAnswerCountDTO[];\n    message?: string;\n  };\n\n  type BaseResponseListAppAnswerResultCountDTO_ = {\n    code?: number;\n    data?: AppAnswerResultCountDTO[];\n    message?: string;\n  };\n\n  type BaseResponseListQuestionContentDTO_ = {\n    code?: number;\n    data?: QuestionContentDTO[];\n    message?: string;\n  };\n\n  type BaseResponseLoginUserVO_ = {\n    code?: number;\n    data?: LoginUserVO;\n    message?: string;\n  };\n\n  type BaseResponseLong_ = {\n    code?: number;\n    data?: number;\n    message?: string;\n  };\n\n  type BaseResponsePageApp_ = {\n    code?: number;\n    data?: PageApp_;\n    message?: string;\n  };\n\n  type BaseResponsePageAppVO_ = {\n    code?: number;\n    data?: PageAppVO_;\n    message?: string;\n  };\n\n  type BaseResponsePagePost_ = {\n    code?: number;\n    data?: PagePost_;\n    message?: string;\n  };\n\n  type BaseResponsePagePostVO_ = {\n    code?: number;\n    data?: PagePostVO_;\n    message?: string;\n  };\n\n  type BaseResponsePageQuestion_ = {\n    code?: number;\n    data?: PageQuestion_;\n    message?: string;\n  };\n\n  type BaseResponsePageQuestionVO_ = {\n    code?: number;\n    data?: PageQuestionVO_;\n    message?: string;\n  };\n\n  type BaseResponsePageScoringResult_ = {\n    code?: number;\n    data?: PageScoringResult_;\n    message?: string;\n  };\n\n  type BaseResponsePageScoringResultVO_ = {\n    code?: number;\n    data?: PageScoringResultVO_;\n    message?: string;\n  };\n\n  type BaseResponsePageUser_ = {\n    code?: number;\n    data?: PageUser_;\n    message?: string;\n  };\n\n  type BaseResponsePageUserAnswer_ = {\n    code?: number;\n    data?: PageUserAnswer_;\n    message?: string;\n  };\n\n  type BaseResponsePageUserAnswerVO_ = {\n    code?: number;\n    data?: PageUserAnswerVO_;\n    message?: string;\n  };\n\n  type BaseResponsePageUserVO_ = {\n    code?: number;\n    data?: PageUserVO_;\n    message?: string;\n  };\n\n  type BaseResponsePostVO_ = {\n    code?: number;\n    data?: PostVO;\n    message?: string;\n  };\n\n  type BaseResponseQuestionVO_ = {\n    code?: number;\n    data?: QuestionVO;\n    message?: string;\n  };\n\n  type BaseResponseScoringResultVO_ = {\n    code?: number;\n    data?: ScoringResultVO;\n    message?: string;\n  };\n\n  type BaseResponseString_ = {\n    code?: number;\n    data?: string;\n    message?: string;\n  };\n\n  type BaseResponseUser_ = {\n    code?: number;\n    data?: User;\n    message?: string;\n  };\n\n  type BaseResponseUserAnswerVO_ = {\n    code?: number;\n    data?: UserAnswerVO;\n    message?: string;\n  };\n\n  type BaseResponseUserVO_ = {\n    code?: number;\n    data?: UserVO;\n    message?: string;\n  };\n\n  type DeleteRequest = {\n    id?: number;\n  };\n\n  type getAppAnswerResultCountUsingGETParams = {\n    /** appId */\n    appId?: number;\n  };\n\n  type getAppVOByIdUsingGETParams = {\n    /** id */\n    id?: number;\n  };\n\n  type getPostVOByIdUsingGETParams = {\n    /** id */\n    id?: number;\n  };\n\n  type getQuestionVOByIdUsingGETParams = {\n    /** id */\n    id?: number;\n  };\n\n  type getScoringResultVOByIdUsingGETParams = {\n    /** id */\n    id?: number;\n  };\n\n  type getUserAnswerVOByIdUsingGETParams = {\n    /** id */\n    id?: number;\n  };\n\n  type getUserByIdUsingGETParams = {\n    /** id */\n    id?: number;\n  };\n\n  type getUserVOByIdUsingGETParams = {\n    /** id */\n    id?: number;\n  };\n\n  type LoginUserVO = {\n    createTime?: string;\n    id?: number;\n    updateTime?: string;\n    userAvatar?: string;\n    userName?: string;\n    userProfile?: string;\n    userRole?: string;\n  };\n\n  type Option = {\n    key?: string;\n    result?: string;\n    score?: number;\n    value?: string;\n  };\n\n  type OrderItem = {\n    asc?: boolean;\n    column?: string;\n  };\n\n  type PageApp_ = {\n    countId?: string;\n    current?: number;\n    maxLimit?: number;\n    optimizeCountSql?: boolean;\n    orders?: OrderItem[];\n    pages?: number;\n    records?: App[];\n    searchCount?: boolean;\n    size?: number;\n    total?: number;\n  };\n\n  type PageAppVO_ = {\n    countId?: string;\n    current?: number;\n    maxLimit?: number;\n    optimizeCountSql?: boolean;\n    orders?: OrderItem[];\n    pages?: number;\n    records?: AppVO[];\n    searchCount?: boolean;\n    size?: number;\n    total?: number;\n  };\n\n  type PagePost_ = {\n    countId?: string;\n    current?: number;\n    maxLimit?: number;\n    optimizeCountSql?: boolean;\n    orders?: OrderItem[];\n    pages?: number;\n    records?: Post[];\n    searchCount?: boolean;\n    size?: number;\n    total?: number;\n  };\n\n  type PagePostVO_ = {\n    countId?: string;\n    current?: number;\n    maxLimit?: number;\n    optimizeCountSql?: boolean;\n    orders?: OrderItem[];\n    pages?: number;\n    records?: PostVO[];\n    searchCount?: boolean;\n    size?: number;\n    total?: number;\n  };\n\n  type PageQuestion_ = {\n    countId?: string;\n    current?: number;\n    maxLimit?: number;\n    optimizeCountSql?: boolean;\n    orders?: OrderItem[];\n    pages?: number;\n    records?: Question[];\n    searchCount?: boolean;\n    size?: number;\n    total?: number;\n  };\n\n  type PageQuestionVO_ = {\n    countId?: string;\n    current?: number;\n    maxLimit?: number;\n    optimizeCountSql?: boolean;\n    orders?: OrderItem[];\n    pages?: number;\n    records?: QuestionVO[];\n    searchCount?: boolean;\n    size?: number;\n    total?: number;\n  };\n\n  type PageScoringResult_ = {\n    countId?: string;\n    current?: number;\n    maxLimit?: number;\n    optimizeCountSql?: boolean;\n    orders?: OrderItem[];\n    pages?: number;\n    records?: ScoringResult[];\n    searchCount?: boolean;\n    size?: number;\n    total?: number;\n  };\n\n  type PageScoringResultVO_ = {\n    countId?: string;\n    current?: number;\n    maxLimit?: number;\n    optimizeCountSql?: boolean;\n    orders?: OrderItem[];\n    pages?: number;\n    records?: ScoringResultVO[];\n    searchCount?: boolean;\n    size?: number;\n    total?: number;\n  };\n\n  type PageUser_ = {\n    countId?: string;\n    current?: number;\n    maxLimit?: number;\n    optimizeCountSql?: boolean;\n    orders?: OrderItem[];\n    pages?: number;\n    records?: User[];\n    searchCount?: boolean;\n    size?: number;\n    total?: number;\n  };\n\n  type PageUserAnswer_ = {\n    countId?: string;\n    current?: number;\n    maxLimit?: number;\n    optimizeCountSql?: boolean;\n    orders?: OrderItem[];\n    pages?: number;\n    records?: UserAnswer[];\n    searchCount?: boolean;\n    size?: number;\n    total?: number;\n  };\n\n  type PageUserAnswerVO_ = {\n    countId?: string;\n    current?: number;\n    maxLimit?: number;\n    optimizeCountSql?: boolean;\n    orders?: OrderItem[];\n    pages?: number;\n    records?: UserAnswerVO[];\n    searchCount?: boolean;\n    size?: number;\n    total?: number;\n  };\n\n  type PageUserVO_ = {\n    countId?: string;\n    current?: number;\n    maxLimit?: number;\n    optimizeCountSql?: boolean;\n    orders?: OrderItem[];\n    pages?: number;\n    records?: UserVO[];\n    searchCount?: boolean;\n    size?: number;\n    total?: number;\n  };\n\n  type Post = {\n    content?: string;\n    createTime?: string;\n    favourNum?: number;\n    id?: number;\n    isDelete?: number;\n    tags?: string;\n    thumbNum?: number;\n    title?: string;\n    updateTime?: string;\n    userId?: number;\n  };\n\n  type PostAddRequest = {\n    content?: string;\n    tags?: string[];\n    title?: string;\n  };\n\n  type PostEditRequest = {\n    content?: string;\n    id?: number;\n    tags?: string[];\n    title?: string;\n  };\n\n  type PostFavourAddRequest = {\n    postId?: number;\n  };\n\n  type PostFavourQueryRequest = {\n    current?: number;\n    pageSize?: number;\n    postQueryRequest?: PostQueryRequest;\n    sortField?: string;\n    sortOrder?: string;\n    userId?: number;\n  };\n\n  type PostQueryRequest = {\n    content?: string;\n    current?: number;\n    favourUserId?: number;\n    id?: number;\n    notId?: number;\n    orTags?: string[];\n    pageSize?: number;\n    searchText?: string;\n    sortField?: string;\n    sortOrder?: string;\n    tags?: string[];\n    title?: string;\n    userId?: number;\n  };\n\n  type PostThumbAddRequest = {\n    postId?: number;\n  };\n\n  type PostUpdateRequest = {\n    content?: string;\n    id?: number;\n    tags?: string[];\n    title?: string;\n  };\n\n  type PostVO = {\n    content?: string;\n    createTime?: string;\n    favourNum?: number;\n    hasFavour?: boolean;\n    hasThumb?: boolean;\n    id?: number;\n    tagList?: string[];\n    thumbNum?: number;\n    title?: string;\n    updateTime?: string;\n    user?: UserVO;\n    userId?: number;\n  };\n\n  type Question = {\n    appId?: number;\n    createTime?: string;\n    id?: number;\n    isDelete?: number;\n    questionContent?: string;\n    updateTime?: string;\n    userId?: number;\n  };\n\n  type QuestionAddRequest = {\n    appId?: number;\n    questionContent?: QuestionContentDTO[];\n  };\n\n  type QuestionContentDTO = {\n    options?: Option[];\n    title?: string;\n  };\n\n  type QuestionEditRequest = {\n    id?: number;\n    questionContent?: QuestionContentDTO[];\n  };\n\n  type QuestionQueryRequest = {\n    appId?: number;\n    current?: number;\n    id?: number;\n    notId?: number;\n    pageSize?: number;\n    questionContent?: string;\n    sortField?: string;\n    sortOrder?: string;\n    userId?: number;\n  };\n\n  type QuestionUpdateRequest = {\n    id?: number;\n    questionContent?: QuestionContentDTO[];\n  };\n\n  type QuestionVO = {\n    appId?: number;\n    createTime?: string;\n    id?: number;\n    questionContent?: QuestionContentDTO[];\n    updateTime?: string;\n    user?: UserVO;\n    userId?: number;\n  };\n\n  type ReviewRequest = {\n    id?: number;\n    reviewMessage?: string;\n    reviewStatus?: number;\n  };\n\n  type ScoringResult = {\n    appId?: number;\n    createTime?: string;\n    id?: number;\n    isDelete?: number;\n    resultDesc?: string;\n    resultName?: string;\n    resultPicture?: string;\n    resultProp?: string;\n    resultScoreRange?: number;\n    updateTime?: string;\n    userId?: number;\n  };\n\n  type ScoringResultAddRequest = {\n    appId?: number;\n    resultDesc?: string;\n    resultName?: string;\n    resultPicture?: string;\n    resultProp?: string[];\n    resultScoreRange?: number;\n  };\n\n  type ScoringResultEditRequest = {\n    id?: number;\n    resultDesc?: string;\n    resultName?: string;\n    resultPicture?: string;\n    resultProp?: string[];\n    resultScoreRange?: number;\n  };\n\n  type ScoringResultQueryRequest = {\n    appId?: number;\n    current?: number;\n    id?: number;\n    notId?: number;\n    pageSize?: number;\n    resultDesc?: string;\n    resultName?: string;\n    resultPicture?: string;\n    resultProp?: string;\n    resultScoreRange?: number;\n    searchText?: string;\n    sortField?: string;\n    sortOrder?: string;\n    userId?: number;\n  };\n\n  type ScoringResultUpdateRequest = {\n    id?: number;\n    resultDesc?: string;\n    resultName?: string;\n    resultPicture?: string;\n    resultProp?: string[];\n    resultScoreRange?: number;\n  };\n\n  type ScoringResultVO = {\n    appId?: number;\n    createTime?: string;\n    id?: number;\n    resultDesc?: string;\n    resultName?: string;\n    resultPicture?: string;\n    resultProp?: string[];\n    resultScoreRange?: number;\n    updateTime?: string;\n    user?: UserVO;\n    userId?: number;\n  };\n\n  type SseEmitter = {\n    timeout?: number;\n  };\n\n  type uploadFileUsingPOSTParams = {\n    biz?: string;\n  };\n\n  type User = {\n    createTime?: string;\n    id?: number;\n    isDelete?: number;\n    mpOpenId?: string;\n    unionId?: string;\n    updateTime?: string;\n    userAccount?: string;\n    userAvatar?: string;\n    userName?: string;\n    userPassword?: string;\n    userProfile?: string;\n    userRole?: string;\n  };\n\n  type UserAddRequest = {\n    userAccount?: string;\n    userAvatar?: string;\n    userName?: string;\n    userRole?: string;\n  };\n\n  type UserAnswer = {\n    appId?: number;\n    appType?: number;\n    choices?: string;\n    createTime?: string;\n    id?: number;\n    isDelete?: number;\n    resultDesc?: string;\n    resultId?: number;\n    resultName?: string;\n    resultPicture?: string;\n    resultScore?: number;\n    scoringStrategy?: number;\n    updateTime?: string;\n    userId?: number;\n  };\n\n  type UserAnswerAddRequest = {\n    appId?: number;\n    choices?: string[];\n    id?: number;\n  };\n\n  type UserAnswerEditRequest = {\n    appId?: number;\n    choices?: string[];\n    id?: number;\n  };\n\n  type UserAnswerQueryRequest = {\n    appId?: number;\n    appType?: number;\n    choices?: string;\n    current?: number;\n    id?: number;\n    notId?: number;\n    pageSize?: number;\n    resultDesc?: string;\n    resultId?: number;\n    resultName?: string;\n    resultPicture?: string;\n    resultScore?: number;\n    scoringStrategy?: number;\n    searchText?: string;\n    sortField?: string;\n    sortOrder?: string;\n    userId?: number;\n  };\n\n  type UserAnswerUpdateRequest = {\n    appId?: number;\n    choices?: string[];\n    id?: number;\n  };\n\n  type UserAnswerVO = {\n    appId?: number;\n    appType?: number;\n    choices?: string[];\n    createTime?: string;\n    id?: number;\n    resultDesc?: string;\n    resultId?: number;\n    resultName?: string;\n    resultPicture?: string;\n    resultScore?: number;\n    scoringStrategy?: number;\n    updateTime?: string;\n    user?: UserVO;\n    userId?: number;\n  };\n\n  type UserLoginRequest = {\n    userAccount?: string;\n    userPassword?: string;\n  };\n\n  type UserQueryRequest = {\n    current?: number;\n    id?: number;\n    mpOpenId?: string;\n    pageSize?: number;\n    sortField?: string;\n    sortOrder?: string;\n    unionId?: string;\n    userName?: string;\n    userProfile?: string;\n    userRole?: string;\n  };\n\n  type UserRegisterRequest = {\n    checkPassword?: string;\n    userAccount?: string;\n    userPassword?: string;\n  };\n\n  type UserUpdateMyRequest = {\n    userAvatar?: string;\n    userName?: string;\n    userProfile?: string;\n  };\n\n  type UserUpdateRequest = {\n    id?: number;\n    userAvatar?: string;\n    userName?: string;\n    userProfile?: string;\n    userRole?: string;\n  };\n\n  type UserVO = {\n    createTime?: string;\n    id?: number;\n    userAvatar?: string;\n    userName?: string;\n    userProfile?: string;\n    userRole?: string;\n  };\n}\n"
  },
  {
    "path": "yudada-frontend/src/api/userAnswerController.ts",
    "content": "// @ts-ignore\n/* eslint-disable */\nimport request from '@/request';\n\n/** addUserAnswer POST /api/userAnswer/add */\nexport async function addUserAnswerUsingPost(\n  body: API.UserAnswerAddRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseLong_>('/api/userAnswer/add', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** deleteUserAnswer POST /api/userAnswer/delete */\nexport async function deleteUserAnswerUsingPost(\n  body: API.DeleteRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseBoolean_>('/api/userAnswer/delete', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** editUserAnswer POST /api/userAnswer/edit */\nexport async function editUserAnswerUsingPost(\n  body: API.UserAnswerEditRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseBoolean_>('/api/userAnswer/edit', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** generateUserAnswerId GET /api/userAnswer/generate/id */\nexport async function generateUserAnswerIdUsingGet(options?: { [key: string]: any }) {\n  return request<API.BaseResponseLong_>('/api/userAnswer/generate/id', {\n    method: 'GET',\n    ...(options || {}),\n  });\n}\n\n/** getUserAnswerVOById GET /api/userAnswer/get/vo */\nexport async function getUserAnswerVoByIdUsingGet(\n  // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)\n  params: API.getUserAnswerVOByIdUsingGETParams,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseUserAnswerVO_>('/api/userAnswer/get/vo', {\n    method: 'GET',\n    params: {\n      ...params,\n    },\n    ...(options || {}),\n  });\n}\n\n/** listUserAnswerByPage POST /api/userAnswer/list/page */\nexport async function listUserAnswerByPageUsingPost(\n  body: API.UserAnswerQueryRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponsePageUserAnswer_>('/api/userAnswer/list/page', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** listUserAnswerVOByPage POST /api/userAnswer/list/page/vo */\nexport async function listUserAnswerVoByPageUsingPost(\n  body: API.UserAnswerQueryRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponsePageUserAnswerVO_>('/api/userAnswer/list/page/vo', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** listMyUserAnswerVOByPage POST /api/userAnswer/my/list/page/vo */\nexport async function listMyUserAnswerVoByPageUsingPost(\n  body: API.UserAnswerQueryRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponsePageUserAnswerVO_>('/api/userAnswer/my/list/page/vo', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** updateUserAnswer POST /api/userAnswer/update */\nexport async function updateUserAnswerUsingPost(\n  body: API.UserAnswerUpdateRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseBoolean_>('/api/userAnswer/update', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n"
  },
  {
    "path": "yudada-frontend/src/api/userController.ts",
    "content": "// @ts-ignore\n/* eslint-disable */\nimport request from '@/request';\n\n/** addUser POST /api/user/add */\nexport async function addUserUsingPost(body: API.UserAddRequest, options?: { [key: string]: any }) {\n  return request<API.BaseResponseLong_>('/api/user/add', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** deleteUser POST /api/user/delete */\nexport async function deleteUserUsingPost(\n  body: API.DeleteRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseBoolean_>('/api/user/delete', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** getUserById GET /api/user/get */\nexport async function getUserByIdUsingGet(\n  // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)\n  params: API.getUserByIdUsingGETParams,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseUser_>('/api/user/get', {\n    method: 'GET',\n    params: {\n      ...params,\n    },\n    ...(options || {}),\n  });\n}\n\n/** getLoginUser GET /api/user/get/login */\nexport async function getLoginUserUsingGet(options?: { [key: string]: any }) {\n  return request<API.BaseResponseLoginUserVO_>('/api/user/get/login', {\n    method: 'GET',\n    ...(options || {}),\n  });\n}\n\n/** getUserVOById GET /api/user/get/vo */\nexport async function getUserVoByIdUsingGet(\n  // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)\n  params: API.getUserVOByIdUsingGETParams,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseUserVO_>('/api/user/get/vo', {\n    method: 'GET',\n    params: {\n      ...params,\n    },\n    ...(options || {}),\n  });\n}\n\n/** listUserByPage POST /api/user/list/page */\nexport async function listUserByPageUsingPost(\n  body: API.UserQueryRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponsePageUser_>('/api/user/list/page', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** listUserVOByPage POST /api/user/list/page/vo */\nexport async function listUserVoByPageUsingPost(\n  body: API.UserQueryRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponsePageUserVO_>('/api/user/list/page/vo', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** userLogin POST /api/user/login */\nexport async function userLoginUsingPost(\n  body: API.UserLoginRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseLoginUserVO_>('/api/user/login', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** userLogout POST /api/user/logout */\nexport async function userLogoutUsingPost(options?: { [key: string]: any }) {\n  return request<API.BaseResponseBoolean_>('/api/user/logout', {\n    method: 'POST',\n    ...(options || {}),\n  });\n}\n\n/** userRegister POST /api/user/register */\nexport async function userRegisterUsingPost(\n  body: API.UserRegisterRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseLong_>('/api/user/register', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** updateUser POST /api/user/update */\nexport async function updateUserUsingPost(\n  body: API.UserUpdateRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseBoolean_>('/api/user/update', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n\n/** updateMyUser POST /api/user/update/my */\nexport async function updateMyUserUsingPost(\n  body: API.UserUpdateMyRequest,\n  options?: { [key: string]: any },\n) {\n  return request<API.BaseResponseBoolean_>('/api/user/update/my', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    data: body,\n    ...(options || {}),\n  });\n}\n"
  },
  {
    "path": "yudada-frontend/src/components/AppCard.vue",
    "content": "<template>\n  <a-card class=\"appCard\" hoverable @click=\"doCardClick\">\n    <template #actions>\n      <!--      <span class=\"icon-hover\"> <IconThumbUp /> </span>-->\n      <span class=\"icon-hover\" @click=\"doShare\"> <IconShareInternal /> </span>\n    </template>\n    <template #cover>\n      <div\n        :style=\"{\n          height: '184px',\n          overflow: 'hidden',\n        }\"\n      >\n        <img\n          :style=\"{ width: '100%', height: '100%' }\"\n          :alt=\"app.appName\"\n          :src=\"app.appIcon\"\n        />\n      </div>\n    </template>\n    <a-card-meta :title=\"app.appName\" :description=\"app.appDesc\">\n      <template #avatar>\n        <div\n          :style=\"{ display: 'flex', alignItems: 'center', color: '#1D2129' }\"\n        >\n          <a-avatar\n            :size=\"24\"\n            :image-url=\"app.user?.userAvatar\"\n            :style=\"{ marginRight: '8px' }\"\n          />\n          <a-typography-text\n            >{{ app.user?.userName ?? \"无名\" }}\n          </a-typography-text>\n        </div>\n      </template>\n    </a-card-meta>\n  </a-card>\n  <ShareModal :link=\"shareLink\" title=\"应用分享\" ref=\"shareModalRef\" />\n</template>\n\n<script setup lang=\"ts\">\nimport { IconShareInternal } from \"@arco-design/web-vue/es/icon\";\nimport API from \"@/api\";\nimport { defineProps, ref, withDefaults } from \"vue\";\nimport { useRouter } from \"vue-router\";\nimport ShareModal from \"@/components/ShareModal.vue\";\n\ninterface Props {\n  app: API.AppVO;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  app: () => {\n    return {};\n  },\n});\n\nconst router = useRouter();\nconst doCardClick = () => {\n  router.push(`/app/detail/${props.app.id}`);\n};\n\n// 分享弹窗的引用\nconst shareModalRef = ref();\n\n// 分享链接\nconst shareLink = `${window.location.protocol}//${window.location.host}/app/detail/${props.app.id}`;\n\n// 分享\nconst doShare = (e: Event) => {\n  if (shareModalRef.value) {\n    shareModalRef.value.openModal();\n  }\n  // 阻止冒泡，防止跳转到详情页\n  e.stopPropagation();\n};\n</script>\n<style scoped>\n.appCard {\n  cursor: pointer;\n}\n\n.icon-hover {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 24px;\n  height: 24px;\n  border-radius: 50%;\n  transition: all 0.1s;\n}\n\n.icon-hover:hover {\n  background-color: rgb(var(--gray-2));\n}\n</style>\n"
  },
  {
    "path": "yudada-frontend/src/components/GlobalHeader.vue",
    "content": "<template>\n  <a-row id=\"globalHeader\" align=\"center\" :wrap=\"false\">\n    <a-col flex=\"auto\">\n      <a-menu\n        mode=\"horizontal\"\n        :selected-keys=\"selectedKeys\"\n        @menu-item-click=\"doMenuClick\"\n      >\n        <a-menu-item\n          key=\"0\"\n          :style=\"{ padding: 0, marginRight: '38px' }\"\n          disabled\n        >\n          <div class=\"titleBar\">\n            <img class=\"logo\" src=\"../assets/logo.png\" />\n            <div class=\"title\">鱼答答</div>\n          </div>\n        </a-menu-item>\n        <a-menu-item v-for=\"item in visibleRoutes\" :key=\"item.path\">\n          {{ item.name }}\n        </a-menu-item>\n      </a-menu>\n    </a-col>\n    <a-col flex=\"100px\">\n      <div v-if=\"loginUserStore.loginUser.id\">\n        {{ loginUserStore.loginUser.userName ?? \"无名\" }}\n      </div>\n      <div v-else>\n        <a-button type=\"primary\" href=\"/user/login\">登录</a-button>\n      </div>\n    </a-col>\n  </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { routes } from \"@/router/routes\";\nimport { useRouter } from \"vue-router\";\nimport { computed, ref } from \"vue\";\nimport { useLoginUserStore } from \"@/store/userStore\";\nimport checkAccess from \"@/access/checkAccess\";\n\nconst loginUserStore = useLoginUserStore();\n\nconst router = useRouter();\n// 当前选中的菜单项\nconst selectedKeys = ref([\"/\"]);\n// 路由跳转时，自动更新选中的菜单项\nrouter.afterEach((to, from, failure) => {\n  selectedKeys.value = [to.path];\n});\n\n// 展示在菜单栏的路由数组\nconst visibleRoutes = computed(() => {\n  return routes.filter((item) => {\n    if (item.meta?.hideInMenu) {\n      return false;\n    }\n    // 根据权限过滤菜单\n    if (!checkAccess(loginUserStore.loginUser, item.meta?.access as string)) {\n      return false;\n    }\n    return true;\n  });\n});\n\n// 点击菜单跳转到对应页面\nconst doMenuClick = (key: string) => {\n  router.push({\n    path: key,\n  });\n};\n</script>\n\n<style scoped>\n#globalHeader {\n}\n\n.titleBar {\n  display: flex;\n  align-items: center;\n}\n\n.title {\n  margin-left: 16px;\n  color: black;\n}\n\n.logo {\n  height: 48px;\n}\n</style>\n"
  },
  {
    "path": "yudada-frontend/src/components/MdEditor.vue",
    "content": "<template>\n  <Editor :value=\"value\" :plugins=\"plugins\" @change=\"handleChange\" />\n</template>\n\n<script setup lang=\"ts\">\nimport gfm from \"@bytemd/plugin-gfm\";\nimport highlight from \"@bytemd/plugin-highlight\";\nimport { Editor } from \"@bytemd/vue-next\";\nimport { ref } from \"vue\";\n\nconst plugins = [\n  gfm(),\n  highlight(),\n  // Add more plugins here\n];\n\nconst value = ref(\"\");\n\nconst handleChange = (v: string) => {\n  value.value = v;\n};\n</script>\n\n<style scoped></style>"
  },
  {
    "path": "yudada-frontend/src/components/MdViewer.vue",
    "content": "<template>\n  <Viewer :value=\"value\" :plugins=\"plugins\" />\n</template>\n\n<script setup lang=\"ts\">\nimport gfm from \"@bytemd/plugin-gfm\";\nimport highlight from \"@bytemd/plugin-highlight\";\nimport { Viewer } from \"@bytemd/vue-next\";\nimport { withDefaults, defineProps } from \"vue\";\n\n/**\n * 定义组件属性类型\n */\ninterface Props {\n  value: string;\n}\n\nconst plugins = [\n  gfm(),\n  highlight(),\n  // Add more plugins here\n];\n\n/**\n * 给组件指定初始值\n */\nconst props = withDefaults(defineProps<Props>(), {\n  value: () => \"\",\n});\n</script>\n"
  },
  {
    "path": "yudada-frontend/src/components/PictureUploader.vue",
    "content": "<template>\n  <a-space direction=\"vertical\" :style=\"{ width: '100%' }\">\n    <a-upload\n      :fileList=\"file ? [file] : []\"\n      :show-file-list=\"false\"\n      :custom-request=\"customRequest\"\n    >\n      <template #upload-button>\n        <div\n          :class=\"`arco-upload-list-item${\n            file && file.status === 'error'\n              ? ' arco-upload-list-item-error'\n              : ''\n          }`\"\n        >\n          <div\n            class=\"arco-upload-list-picture custom-upload-avatar\"\n            v-if=\"file && file.url\"\n          >\n            <img :src=\"file.url\" />\n            <div class=\"arco-upload-list-picture-mask\">\n              <IconEdit />\n            </div>\n            <a-progress\n              v-if=\"file.status === 'uploading' && file.percent < 100\"\n              :percent=\"file.percent\"\n              type=\"circle\"\n              size=\"mini\"\n              :style=\"{\n                position: 'absolute',\n                left: '50%',\n                top: '50%',\n                transform: 'translateX(-50%) translateY(-50%)',\n              }\"\n            />\n          </div>\n          <div class=\"arco-upload-picture-card\" v-else>\n            <div class=\"arco-upload-picture-card-text\">\n              <IconPlus />\n              <div style=\"margin-top: 10px; font-weight: 600\">上传</div>\n            </div>\n          </div>\n        </div>\n      </template>\n    </a-upload>\n  </a-space>\n</template>\n\n<script setup lang=\"ts\">\nimport { IconEdit, IconPlus } from \"@arco-design/web-vue/es/icon\";\nimport { ref, withDefaults, defineProps } from \"vue\";\nimport { uploadFileUsingPost } from \"@/api/fileController\";\nimport { Message } from \"@arco-design/web-vue\";\n\n/**\n * 定义组件属性类型\n */\ninterface Props {\n  biz: string;\n  onChange?: (url: string) => void;\n  value?: string;\n}\n\n/**\n * 给组件指定初始值\n */\nconst props = withDefaults(defineProps<Props>(), {\n  value: () => \"\",\n});\n\nconst file = ref();\nif (props.value) {\n  file.value = {\n    url: props.value,\n    percent: 100,\n    status: \"done\",\n  };\n}\n\n// 自定义请求\nconst customRequest = async (option: any) => {\n  const { onError, onSuccess, fileItem } = option;\n\n  const res: any = await uploadFileUsingPost(\n    { biz: props.biz },\n    {},\n    fileItem.file\n  );\n  if (res.data.code === 0 && res.data.data) {\n    const url = res.data.data;\n    file.value = {\n      name: fileItem.name,\n      file: fileItem.file,\n      url,\n    };\n    props.onChange?.(url);\n    onSuccess();\n    console.log(file.value);\n  } else {\n    Message.error(\"上传失败，\" + res.data.message || \"\");\n    onError(new Error(res.data.message));\n  }\n};\n</script>\n"
  },
  {
    "path": "yudada-frontend/src/components/ShareModal.vue",
    "content": "<template>\n  <a-modal v-model:visible=\"visible\" :footer=\"false\" @cancel=\"closeModal\">\n    <template #title>\n      {{ title }}\n    </template>\n    <h4 style=\"margin-top: 0\">复制分享链接</h4>\n    <a-typography-paragraph copyable>{{ link }}</a-typography-paragraph>\n    <h4>手机扫码查看</h4>\n    <img :src=\"codeImg\" />\n  </a-modal>\n</template>\n\n<script setup lang=\"ts\">\nimport { defineExpose, defineProps, ref, withDefaults } from \"vue\";\n// @ts-ignore\nimport QRCode from \"qrcode\";\nimport message from \"@arco-design/web-vue/es/message\";\n\n/**\n * 定义组件属性类型\n */\ninterface Props {\n  // 分享链接\n  link: string;\n  // 弹窗标题\n  title: string;\n}\n\n/**\n * 给组件指定初始值\n */\nconst props = withDefaults(defineProps<Props>(), {\n  link: () => \"https://laoyujianli.com/share/yupi\",\n  title: () => \"分享\",\n});\n\n// 要展示的图片\nconst codeImg = ref();\n\n// 是否可见\nconst visible = ref(false);\n\n// 打开弹窗\nconst openModal = () => {\n  visible.value = true;\n};\n\n// 暴露函数给父组件\ndefineExpose({\n  openModal,\n});\n\n// 关闭弹窗\nconst closeModal = () => {\n  visible.value = false;\n};\n\n// 二维码生成\nQRCode.toDataURL(props.link)\n  .then((url: any) => {\n    codeImg.value = url;\n  })\n  .catch((err: any) => {\n    console.error(err);\n    message.error(\"生成二维码失败，\" + err.message);\n  });\n</script>\n\n<style scoped></style>"
  },
  {
    "path": "yudada-frontend/src/constant/app.ts",
    "content": "// 审核状态枚举\nexport const REVIEW_STATUS_ENUM = {\n  // 待审核\n  REVIEWING: 0,\n  // 通过\n  PASS: 1,\n  // 拒绝\n  REJECT: 2,\n};\n\n// 审核状态映射\nexport const REVIEW_STATUS_MAP = {\n  0: \"待审核\",\n  1: \"通过\",\n  2: \"拒绝\",\n};\n\n// 应用类型枚举\nexport const APP_TYPE_ENUM = {\n  // 得分类\n  SCORE: 0,\n  // 测评类\n  TEST: 1,\n};\n\n// 应用类型映射\nexport const APP_TYPE_MAP = {\n  0: \"得分类\",\n  1: \"测评类\",\n};\n\n// 应用评分策略枚举\nexport const APP_SCORING_STRATEGY_ENUM = {\n  // 自定义\n  CUSTOM: 0,\n  // AI\n  AI: 1,\n};\n\n// 应用评分策略映射\nexport const APP_SCORING_STRATEGY_MAP = {\n  0: \"自定义\",\n  1: \"AI\",\n};\n"
  },
  {
    "path": "yudada-frontend/src/layouts/BasicLayout.vue",
    "content": "<template>\n  <div id=\"basicLayout\">\n    <a-layout style=\"height: 100vh\">\n      <a-layout-header class=\"header\">\n        <GlobalHeader />\n      </a-layout-header>\n      <a-layout-content class=\"content\">\n        <router-view />\n      </a-layout-content>\n      <a-layout-footer class=\"footer\">\n        <a href=\"https://www.code-nav.cn\" target=\"_blank\">\n          编程导航 by 程序员鱼皮\n        </a>\n      </a-layout-footer>\n    </a-layout>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport GlobalHeader from \"@/components/GlobalHeader.vue\";\n</script>\n\n<style scoped>\n#basicLayout {\n}\n\n#basicLayout .header {\n  margin-bottom: 16px;\n  box-shadow: #eee 1px 1px 5px;\n}\n\n#basicLayout .content {\n  max-width: 1200px;\n  width: 100%;\n  box-sizing: border-box;\n  margin: 0 auto 28px;\n  padding: 20px;\n  background: linear-gradient(to right, #fefefe, #fff);\n}\n\n.footer {\n  padding: 16px;\n  text-align: center;\n  background: #efefef;\n}\n</style>\n"
  },
  {
    "path": "yudada-frontend/src/layouts/UserLayout.vue",
    "content": "<template>\n  <div id=\"userLayout\">\n    <a-layout style=\"height: 100vh\">\n      <a-layout-header class=\"header\">\n        <a-space>\n          <img class=\"logo\" src=\"../assets/logo.png\" />\n          <div>鱼答答 AI 答题应用平台</div>\n        </a-space>\n      </a-layout-header>\n      <a-layout-content class=\"content\">\n        <router-view />\n      </a-layout-content>\n      <a-layout-footer class=\"footer\">\n        <a href=\"https://www.code-nav.cn\" target=\"_blank\">\n          编程导航 by 程序员鱼皮\n        </a>\n      </a-layout-footer>\n    </a-layout>\n  </div>\n</template>\n\n<script setup lang=\"ts\"></script>\n\n<style scoped>\n#userLayout {\n  text-align: center;\n  background: url(\"https://gw.alipayobjects.com/zos/rmsportal/FfdJeJRQWjEeGTpqgBKj.png\")\n    0% 0% / 100% 100%;\n}\n\n#userLayout .logo {\n  width: 48px;\n  height: 48px;\n}\n\n#userLayout .header {\n  margin-top: 16px;\n  font-size: 21px;\n}\n\n#userLayout .content {\n  margin-bottom: 16px;\n  padding: 20px;\n}\n\n.footer {\n  padding: 16px;\n  text-align: center;\n  background: #efefef;\n}\n</style>\n"
  },
  {
    "path": "yudada-frontend/src/main.ts",
    "content": "import { createApp } from \"vue\";\nimport App from \"./App.vue\";\nimport ArcoVue from \"@arco-design/web-vue\";\nimport { createPinia } from \"pinia\";\nimport \"@arco-design/web-vue/dist/arco.css\";\nimport router from \"./router\";\nimport \"@/access\";\n\nconst pinia = createPinia();\n\ncreateApp(App).use(ArcoVue).use(pinia).use(router).mount(\"#app\");\n"
  },
  {
    "path": "yudada-frontend/src/request.ts",
    "content": "import axios from \"axios\";\nimport { Message } from \"@arco-design/web-vue\";\n\nconst myAxios = axios.create({\n  baseURL: \"http://localhost:8101\",\n  timeout: 60000,\n  withCredentials: true,\n});\n\n// 全局请求拦截器\nmyAxios.interceptors.request.use(\n  function (config) {\n    // Do something before request is sent\n    return config;\n  },\n  function (error) {\n    // Do something with request error\n    return Promise.reject(error);\n  }\n);\n\n// 全局响应拦截器\nmyAxios.interceptors.response.use(\n  function (response) {\n    console.log(response);\n    // Any status code that lie within the range of 2xx cause this function to trigger\n    // Do something with response data\n    const { data } = response;\n\n    // 未登录\n    if (data.code === 40100) {\n      // 不是获取用户信息的请求，并且用户目前不是已经在用户登录页面，则跳转到登录页面\n      if (\n        !response.request.responseURL.includes(\"user/get/login\") &&\n        !window.location.pathname.includes(\"/user/login\")\n      ) {\n        Message.warning(\"请先登录\");\n        window.location.href = `/user/login?redirect=${window.location.href}`;\n      }\n    }\n\n    return response;\n  },\n  function (error) {\n    // Any status codes that falls outside the range of 2xx cause this function to trigger\n    // Do something with response error\n    return Promise.reject(error);\n  }\n);\n\nexport default myAxios;\n"
  },
  {
    "path": "yudada-frontend/src/router/index.ts",
    "content": "import { createRouter, createWebHistory } from \"vue-router\";\nimport { routes } from \"@/router/routes\";\n\nconst router = createRouter({\n  history: createWebHistory(process.env.BASE_URL),\n  routes,\n});\n\nexport default router;\n"
  },
  {
    "path": "yudada-frontend/src/router/routes.ts",
    "content": "import { RouteRecordRaw } from \"vue-router\";\nimport HomePage from \"@/views/HomePage.vue\";\nimport UserLayout from \"@/layouts/UserLayout.vue\";\nimport ACCESS_ENUM from \"@/access/accessEnum\";\nimport NoAuthPage from \"@/views/NoAuthPage.vue\";\nimport UserLoginPage from \"@/views/user/UserLoginPage.vue\";\nimport UserRegisterPage from \"@/views/user/UserRegisterPage.vue\";\nimport AdminUserPage from \"@/views/admin/AdminUserPage.vue\";\nimport AdminAppPage from \"@/views/admin/AdminAppPage.vue\";\nimport AdminQuestionPage from \"@/views/admin/AdminQuestionPage.vue\";\nimport AdminScoringResultPage from \"@/views/admin/AdminScoringResultPage.vue\";\nimport AdminUserAnswerPage from \"@/views/admin/AdminUserAnswerPage.vue\";\nimport AppDetailPage from \"@/views/app/AppDetailPage.vue\";\nimport AddAppPage from \"@/views/add/AddAppPage.vue\";\nimport AddQuestionPage from \"@/views/add/AddQuestionPage.vue\";\nimport AddScoringResultPage from \"@/views/add/AddScoringResultPage.vue\";\nimport DoAnswerPage from \"@/views/answer/DoAnswerPage.vue\";\nimport AnswerResultPage from \"@/views/answer/AnswerResultPage.vue\";\nimport MyAnswerPage from \"@/views/answer/MyAnswerPage.vue\";\nimport AppStatisticPage from \"@/views/statistic/AppStatisticPage.vue\";\n\nexport const routes: Array<RouteRecordRaw> = [\n  {\n    path: \"/\",\n    name: \"主页\",\n    component: HomePage,\n  },\n  {\n    path: \"/add/app\",\n    name: \"创建应用\",\n    component: AddAppPage,\n  },\n  {\n    path: \"/add/app/:id\",\n    name: \"修改应用\",\n    props: true,\n    component: AddAppPage,\n    meta: {\n      hideInMenu: true,\n    },\n  },\n  {\n    path: \"/add/question/:appId\",\n    name: \"创建题目\",\n    component: AddQuestionPage,\n    props: true,\n    meta: {\n      hideInMenu: true,\n    },\n  },\n  {\n    path: \"/add/scoring_result/:appId\",\n    name: \"创建评分\",\n    component: AddScoringResultPage,\n    props: true,\n    meta: {\n      hideInMenu: true,\n    },\n  },\n  {\n    path: \"/app/detail/:id\",\n    name: \"应用详情页\",\n    props: true,\n    component: AppDetailPage,\n    meta: {\n      hideInMenu: true,\n    },\n  },\n  {\n    path: \"/answer/do/:appId\",\n    name: \"答题\",\n    component: DoAnswerPage,\n    props: true,\n    meta: {\n      hideInMenu: true,\n      access: ACCESS_ENUM.USER,\n    },\n  },\n  {\n    path: \"/answer/result/:id\",\n    name: \"答题结果\",\n    component: AnswerResultPage,\n    props: true,\n    meta: {\n      hideInMenu: true,\n      access: ACCESS_ENUM.USER,\n    },\n  },\n  {\n    path: \"/answer/my\",\n    name: \"我的答题\",\n    component: MyAnswerPage,\n    meta: {\n      access: ACCESS_ENUM.USER,\n    },\n  },\n  {\n    path: \"/admin/user\",\n    name: \"用户管理\",\n    component: AdminUserPage,\n    meta: {\n      access: ACCESS_ENUM.ADMIN,\n    },\n  },\n  {\n    path: \"/admin/app\",\n    name: \"应用管理\",\n    component: AdminAppPage,\n    meta: {\n      access: ACCESS_ENUM.ADMIN,\n    },\n  },\n  {\n    path: \"/admin/question\",\n    name: \"题目管理\",\n    component: AdminQuestionPage,\n    meta: {\n      access: ACCESS_ENUM.ADMIN,\n    },\n  },\n  {\n    path: \"/admin/scoring_result\",\n    name: \"评分管理\",\n    component: AdminScoringResultPage,\n    meta: {\n      access: ACCESS_ENUM.ADMIN,\n    },\n  },\n  {\n    path: \"/admin/user_answer\",\n    name: \"回答管理\",\n    component: AdminUserAnswerPage,\n    meta: {\n      access: ACCESS_ENUM.ADMIN,\n    },\n  },\n  {\n    path: \"/app_statistic\",\n    name: \"应用统计\",\n    component: AppStatisticPage,\n    meta: {\n      access: ACCESS_ENUM.ADMIN,\n    },\n  },\n  {\n    path: \"/noAuth\",\n    name: \"无权限\",\n    component: NoAuthPage,\n    meta: {\n      hideInMenu: true,\n    },\n  },\n  {\n    path: \"/hide\",\n    name: \"隐藏页面\",\n    component: HomePage,\n    meta: {\n      hideInMenu: true,\n    },\n  },\n  {\n    path: \"/user\",\n    name: \"用户\",\n    component: UserLayout,\n    children: [\n      {\n        path: \"/user/login\",\n        name: \"用户登录\",\n        component: UserLoginPage,\n      },\n      {\n        path: \"/user/register\",\n        name: \"用户注册\",\n        component: UserRegisterPage,\n      },\n    ],\n    meta: {\n      hideInMenu: true,\n    },\n  },\n];\n"
  },
  {
    "path": "yudada-frontend/src/shims-vue.d.ts",
    "content": "/* eslint-disable */\ndeclare module \"*.vue\" {\n  import type { DefineComponent } from \"vue\";\n  const component: DefineComponent<{}, {}, any>;\n  export default component;\n}\n"
  },
  {
    "path": "yudada-frontend/src/store/userStore.ts",
    "content": "import { defineStore } from \"pinia\";\nimport { ref } from \"vue\";\nimport { getLoginUserUsingGet } from \"@/api/userController\";\nimport ACCESS_ENUM from \"@/access/accessEnum\";\n\n/**\n * 登录用户信息全局状态\n */\nexport const useLoginUserStore = defineStore(\"loginUser\", () => {\n  const loginUser = ref<API.LoginUserVO>({\n    userName: \"未登录\",\n  });\n\n  function setLoginUser(newLoginUser: API.LoginUserVO) {\n    loginUser.value = newLoginUser;\n  }\n\n  async function fetchLoginUser() {\n    const res = await getLoginUserUsingGet();\n    if (res.data.code === 0 && res.data.data) {\n      loginUser.value = res.data.data;\n    } else {\n      loginUser.value = { userRole: ACCESS_ENUM.NOT_LOGIN };\n    }\n  }\n\n  return { loginUser, setLoginUser, fetchLoginUser };\n});\n"
  },
  {
    "path": "yudada-frontend/src/views/HomePage.vue",
    "content": "<template>\n  <div id=\"homePage\">\n    <div class=\"searchBar\">\n      <a-input-search\n        :style=\"{ width: '320px' }\"\n        placeholder=\"快速发现答题应用\"\n        button-text=\"搜索\"\n        size=\"large\"\n        search-button\n      />\n    </div>\n    <a-list\n      class=\"list-demo-action-layout\"\n      :grid-props=\"{ gutter: [20, 20], sm: 24, md: 12, lg: 8, xl: 6 }\"\n      :bordered=\"false\"\n      :data=\"dataList\"\n      :pagination-props=\"{\n        pageSize: searchParams.pageSize,\n        current: searchParams.current,\n        total,\n      }\"\n      @page-change=\"onPageChange\"\n    >\n      <template #item=\"{ item }\">\n        <AppCard :app=\"item\" />\n      </template>\n    </a-list>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, watchEffect } from \"vue\";\nimport AppCard from \"@/components/AppCard.vue\";\nimport API from \"@/api\";\nimport { listAppVoByPageUsingPost } from \"@/api/appController\";\nimport message from \"@arco-design/web-vue/es/message\";\nimport { REVIEW_STATUS_ENUM } from \"@/constant/app\";\n\n// 初始化搜索条件（不应该被修改）\nconst initSearchParams = {\n  current: 1,\n  pageSize: 12,\n  sortOrder: \"descend\",\n  sortField: \"createTime\",\n};\n\nconst searchParams = ref<API.AppQueryRequest>({\n  ...initSearchParams,\n});\nconst dataList = ref<API.AppVO[]>([]);\nconst total = ref<number>(0);\n\n/**\n * 加载数据\n */\nconst loadData = async () => {\n  const params = {\n    reviewStatus: REVIEW_STATUS_ENUM.PASS,\n    ...searchParams.value,\n  };\n  const res = await listAppVoByPageUsingPost(params);\n  if (res.data.code === 0) {\n    dataList.value = res.data.data?.records || [];\n    total.value = res.data.data?.total || 0;\n  } else {\n    message.error(\"获取数据失败，\" + res.data.message);\n  }\n};\n\n/**\n * 当分页变化时，改变搜索条件，触发数据加载\n * @param page\n */\nconst onPageChange = (page: number) => {\n  searchParams.value = {\n    ...searchParams.value,\n    current: page,\n  };\n};\n\n/**\n * 监听 searchParams 变量，改变时触发数据的重新加载\n */\nwatchEffect(() => {\n  loadData();\n});\n</script>\n\n<style scoped>\n#homePage {\n}\n\n.searchBar {\n  margin-bottom: 28px;\n  text-align: center;\n}\n\n.list-demo-action-layout .image-area {\n  width: 183px;\n  height: 119px;\n  overflow: hidden;\n  border-radius: 2px;\n}\n\n.list-demo-action-layout .list-demo-item {\n  padding: 20px 0;\n  border-bottom: 1px solid var(--color-fill-3);\n}\n\n.list-demo-action-layout .image-area img {\n  width: 100%;\n}\n\n.list-demo-action-layout .arco-list-item-action .arco-icon {\n  margin: 0 4px;\n}\n</style>"
  },
  {
    "path": "yudada-frontend/src/views/NoAuthPage.vue",
    "content": "<template>\n  <div id=\"noAuthPage\">无权限</div>\n</template>\n\n<script setup lang=\"ts\"></script>\n"
  },
  {
    "path": "yudada-frontend/src/views/add/AddAppPage.vue",
    "content": "<template>\n  <div id=\"addAppPage\">\n    <h2 style=\"margin-bottom: 32px\">创建应用</h2>\n    <a-form\n      :model=\"form\"\n      :style=\"{ width: '480px' }\"\n      label-align=\"left\"\n      auto-label-width\n      @submit=\"handleSubmit\"\n    >\n      <a-form-item field=\"appName\" label=\"应用名称\">\n        <a-input v-model=\"form.appName\" placeholder=\"请输入应用名称\" />\n      </a-form-item>\n      <a-form-item field=\"appDesc\" label=\"应用描述\">\n        <a-input v-model=\"form.appDesc\" placeholder=\"请输入应用描述\" />\n      </a-form-item>\n      <a-form-item field=\"appIcon\" label=\"应用图标\">\n        <a-input v-model=\"form.appIcon\" placeholder=\"请输入应用图标\" />\n      </a-form-item>\n      <!--      <a-form-item field=\"appIcon\" label=\"应用图标\">-->\n      <!--        <PictureUploader-->\n      <!--          :value=\"form.appIcon\"-->\n      <!--          :onChange=\"(value) => (form.appIcon = value)\"-->\n      <!--        />-->\n      <!--      </a-form-item>-->\n      <a-form-item field=\"appType\" label=\"应用类型\">\n        <a-select\n          v-model=\"form.appType\"\n          :style=\"{ width: '320px' }\"\n          placeholder=\"请选择应用类型\"\n        >\n          <a-option\n            v-for=\"(value, key) of APP_TYPE_MAP\"\n            :value=\"Number(key)\"\n            :label=\"value\"\n          />\n        </a-select>\n      </a-form-item>\n      <a-form-item field=\"scoringStrategy\" label=\"评分策略\">\n        <a-select\n          v-model=\"form.scoringStrategy\"\n          :style=\"{ width: '320px' }\"\n          placeholder=\"请选择评分策略\"\n        >\n          <a-option\n            v-for=\"(value, key) of APP_SCORING_STRATEGY_MAP\"\n            :value=\"Number(key)\"\n            :label=\"value\"\n          />\n        </a-select>\n      </a-form-item>\n      <a-form-item>\n        <a-button type=\"primary\" html-type=\"submit\" style=\"width: 120px\">\n          提交\n        </a-button>\n      </a-form-item>\n    </a-form>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { defineProps, ref, watchEffect, withDefaults } from \"vue\";\nimport API from \"@/api\";\nimport message from \"@arco-design/web-vue/es/message\";\nimport { useRouter } from \"vue-router\";\nimport {\n  addAppUsingPost,\n  editAppUsingPost,\n  getAppVoByIdUsingGet,\n} from \"@/api/appController\";\nimport { APP_SCORING_STRATEGY_MAP, APP_TYPE_MAP } from \"@/constant/app\";\n\ninterface Props {\n  id: string;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  id: () => {\n    return \"\";\n  },\n});\n\nconst router = useRouter();\n\nconst form = ref({\n  appDesc: \"\",\n  appIcon: \"\",\n  appName: \"\",\n  appType: 0,\n  scoringStrategy: 0,\n} as API.AppAddRequest);\n\nconst oldApp = ref<API.AppVO>();\n\n/**\n * 加载数据\n */\nconst loadData = async () => {\n  if (!props.id) {\n    return;\n  }\n  const res = await getAppVoByIdUsingGet({\n    id: props.id as any,\n  });\n  if (res.data.code === 0 && res.data.data) {\n    oldApp.value = res.data.data;\n    form.value = res.data.data;\n  } else {\n    message.error(\"获取数据失败，\" + res.data.message);\n  }\n};\n\n// 获取旧数据\nwatchEffect(() => {\n  loadData();\n});\n\n/**\n * 提交\n */\nconst handleSubmit = async () => {\n  let res: any;\n  // 如果是修改\n  if (props.id) {\n    res = await editAppUsingPost({\n      id: props.id as any,\n      ...form.value,\n    });\n  } else {\n    // 创建\n    res = await addAppUsingPost(form.value);\n  }\n  if (res.data.code === 0) {\n    message.success(\"操作成功，即将跳转到应用详情页\");\n    setTimeout(() => {\n      router.push(`/app/detail/${props.id || res.data.data}`);\n    }, 3000);\n  } else {\n    message.error(\"操作失败，\" + res.data.message);\n  }\n};\n</script>"
  },
  {
    "path": "yudada-frontend/src/views/add/AddQuestionPage.vue",
    "content": "<template>\n  <div id=\"addQuestionPage\">\n    <h2 style=\"margin-bottom: 32px\">设置题目</h2>\n    <a-form\n      :model=\"questionContent\"\n      :style=\"{ width: '480px' }\"\n      label-align=\"left\"\n      auto-label-width\n      @submit=\"handleSubmit\"\n    >\n      <a-form-item label=\"应用 id\">\n        {{ appId }}\n      </a-form-item>\n      <a-form-item label=\"题目列表\" :content-flex=\"false\" :merge-props=\"false\">\n        <a-space size=\"medium\">\n          <a-button @click=\"addQuestion(questionContent.length)\">\n            底部添加题目\n          </a-button>\n          <!-- AI 生成抽屉 -->\n          <AiGenerateQuestionDrawer\n            :appId=\"appId\"\n            :onSuccess=\"onAiGenerateSuccess\"\n            :onSSESuccess=\"onAiGenerateSuccessSSE\"\n            :onSSEClose=\"onSSEClose\"\n            :onSSEStart=\"onSSEStart\"\n          />\n        </a-space>\n        <!-- 遍历每道题目 -->\n        <div v-for=\"(question, index) in questionContent\" :key=\"index\">\n          <a-space size=\"large\">\n            <h3>题目 {{ index + 1 }}</h3>\n            <a-button size=\"small\" @click=\"addQuestion(index + 1)\">\n              添加题目\n            </a-button>\n            <a-button\n              size=\"small\"\n              status=\"danger\"\n              @click=\"deleteQuestion(index)\"\n            >\n              删除题目\n            </a-button>\n          </a-space>\n          <a-form-item field=\"posts.post1\" :label=\"`题目 ${index + 1} 标题`\">\n            <a-input v-model=\"question.title\" placeholder=\"请输入标题\" />\n          </a-form-item>\n          <!--  题目选项 -->\n          <a-space size=\"large\">\n            <h4>题目 {{ index + 1 }} 选项列表</h4>\n            <a-button\n              size=\"small\"\n              @click=\"addQuestionOption(question, question.options.length)\"\n            >\n              底部添加选项\n            </a-button>\n          </a-space>\n          <a-form-item\n            v-for=\"(option, optionIndex) in question.options\"\n            :key=\"optionIndex\"\n            :label=\"`选项 ${optionIndex + 1}`\"\n            :content-flex=\"false\"\n            :merge-props=\"false\"\n          >\n            <a-form-item label=\"选项 key\">\n              <a-input v-model=\"option.key\" placeholder=\"请输入选项 key\" />\n            </a-form-item>\n            <a-form-item label=\"选项值\">\n              <a-input v-model=\"option.value\" placeholder=\"请输入选项值\" />\n            </a-form-item>\n            <a-form-item label=\"选项结果\">\n              <a-input v-model=\"option.result\" placeholder=\"请输入选项结果\" />\n            </a-form-item>\n            <a-form-item label=\"选项得分\">\n              <a-input-number\n                v-model=\"option.score\"\n                placeholder=\"请输入选项得分\"\n              />\n            </a-form-item>\n            <a-space size=\"large\">\n              <a-button\n                size=\"mini\"\n                @click=\"addQuestionOption(question, optionIndex + 1)\"\n              >\n                添加选项\n              </a-button>\n              <a-button\n                size=\"mini\"\n                status=\"danger\"\n                @click=\"deleteQuestionOption(question, optionIndex as any)\"\n              >\n                删除选项\n              </a-button>\n            </a-space>\n          </a-form-item>\n          <!-- 题目选项结尾 -->\n        </div>\n      </a-form-item>\n      <a-form-item>\n        <a-button type=\"primary\" html-type=\"submit\" style=\"width: 120px\">\n          提交\n        </a-button>\n      </a-form-item>\n    </a-form>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { defineProps, ref, watchEffect, withDefaults } from \"vue\";\nimport API from \"@/api\";\nimport { useRouter } from \"vue-router\";\nimport {\n  addQuestionUsingPost,\n  editQuestionUsingPost,\n  listQuestionVoByPageUsingPost,\n} from \"@/api/questionController\";\nimport message from \"@arco-design/web-vue/es/message\";\nimport AiGenerateQuestionDrawer from \"@/views/add/components/AiGenerateQuestionDrawer.vue\";\n\ninterface Props {\n  appId: string;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  appId: () => {\n    return \"\";\n  },\n});\n\nconst router = useRouter();\n\n// 题目内容结构（理解为题目列表）\nconst questionContent = ref<API.QuestionContentDTO[]>([]);\n\n/**\n * 添加题目\n * @param index\n */\nconst addQuestion = (index: number) => {\n  questionContent.value.splice(index, 0, {\n    title: \"\",\n    options: [],\n  });\n};\n\n/**\n * 删除题目\n * @param index\n */\nconst deleteQuestion = (index: number) => {\n  questionContent.value.splice(index, 1);\n};\n\n/**\n * 添加题目选项\n * @param question\n * @param index\n */\nconst addQuestionOption = (question: API.QuestionContentDTO, index: number) => {\n  if (!question.options) {\n    question.options = [];\n  }\n  question.options.splice(index, 0, {\n    key: \"\",\n    value: \"\",\n  });\n};\n\n/**\n * 删除题目选项\n * @param question\n * @param index\n */\nconst deleteQuestionOption = (\n  question: API.QuestionContentDTO,\n  index: number\n) => {\n  if (!question.options) {\n    question.options = [];\n  }\n  question.options.splice(index, 1);\n};\n\nconst oldQuestion = ref<API.QuestionVO>();\n\n/**\n * 加载数据\n */\nconst loadData = async () => {\n  if (!props.appId) {\n    return;\n  }\n  const res = await listQuestionVoByPageUsingPost({\n    appId: props.appId as any,\n    current: 1,\n    pageSize: 1,\n    sortField: \"createTime\",\n    sortOrder: \"descend\",\n  });\n  if (res.data.code === 0 && res.data.data?.records) {\n    oldQuestion.value = res.data.data?.records[0];\n    if (oldQuestion.value) {\n      questionContent.value = oldQuestion.value.questionContent ?? [];\n    }\n  } else {\n    message.error(\"获取数据失败，\" + res.data.message);\n  }\n};\n\n// 获取旧数据\nwatchEffect(() => {\n  loadData();\n});\n\n/**\n * 提交\n */\nconst handleSubmit = async () => {\n  if (!props.appId || !questionContent.value) {\n    return;\n  }\n  let res: any;\n  // 如果是修改\n  if (oldQuestion.value?.id) {\n    res = await editQuestionUsingPost({\n      id: oldQuestion.value.id,\n      questionContent: questionContent.value,\n    });\n  } else {\n    // 创建\n    res = await addQuestionUsingPost({\n      appId: props.appId as any,\n      questionContent: questionContent.value,\n    });\n  }\n  if (res.data.code === 0) {\n    message.success(\"操作成功，即将跳转到应用详情页\");\n    setTimeout(() => {\n      router.push(`/app/detail/${props.appId}`);\n    }, 3000);\n  } else {\n    message.error(\"操作失败，\" + res.data.message);\n  }\n};\n\n/**\n * AI 生成题目成功后执行\n */\nconst onAiGenerateSuccess = (result: API.QuestionContentDTO[]) => {\n  message.success(`AI 生成题目成功，生成 ${result.length} 道题目`);\n  questionContent.value = [...questionContent.value, ...result];\n};\n\n/**\n * AI 生成题目成功后执行（SSE）\n */\nconst onAiGenerateSuccessSSE = (result: API.QuestionContentDTO) => {\n  questionContent.value = [...questionContent.value, result];\n};\n\n/**\n * SSE 开始生成\n * @param event\n */\nconst onSSEStart = (event: any) => {\n  message.success(\"开始生成\");\n};\n\n/**\n * SSE 生成完毕\n * @param event\n */\nconst onSSEClose = (event: any) => {\n  message.success(\"生成完毕\");\n};\n</script>"
  },
  {
    "path": "yudada-frontend/src/views/add/AddScoringResultPage.vue",
    "content": "<template>\n  <div id=\"addScoringResultPage\">\n    <h2 style=\"margin-bottom: 32px\">设置评分</h2>\n    <a-form\n      :model=\"form\"\n      :style=\"{ width: '480px' }\"\n      label-align=\"left\"\n      auto-label-width\n      @submit=\"handleSubmit\"\n    >\n      <a-form-item label=\"应用 id\">\n        {{ appId }}\n      </a-form-item>\n      <a-form-item v-if=\"updateId\" label=\"修改评分 id\">\n        {{ updateId }}\n      </a-form-item>\n      <a-form-item field=\"resultName\" label=\"结果名称\">\n        <a-input v-model=\"form.resultName\" placeholder=\"请输入结果名称\" />\n      </a-form-item>\n      <a-form-item field=\"resultDesc\" label=\"结果描述\">\n        <a-input v-model=\"form.resultDesc\" placeholder=\"请输入结果描述\" />\n      </a-form-item>\n      <a-form-item field=\"resultPicture\" label=\"结果图标\">\n        <a-input\n          v-model=\"form.resultPicture\"\n          placeholder=\"请输入结果图标地址\"\n        />\n      </a-form-item>\n      <a-form-item field=\"resultProp\" label=\"结果集\">\n        <a-input-tag\n          v-model=\"form.resultProp\"\n          :style=\"{ width: '320px' }\"\n          placeholder=\"请输出结果集，按回车确认\"\n          allow-clear\n        />\n      </a-form-item>\n      <a-form-item field=\"resultScoreRange\" label=\"结果得分范围\">\n        <a-input-number\n          v-model=\"form.resultScoreRange\"\n          placeholder=\"请输入结果得分范围\"\n        />\n      </a-form-item>\n      <a-form-item>\n        <a-button type=\"primary\" html-type=\"submit\" style=\"width: 120px\">\n          提交\n        </a-button>\n      </a-form-item>\n    </a-form>\n    <h2 style=\"margin-bottom: 32px\">评分管理</h2>\n    <ScoringResultTable :appId=\"appId\" :doUpdate=\"doUpdate\" ref=\"tableRef\" />\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { defineProps, ref, withDefaults } from \"vue\";\nimport API from \"@/api\";\nimport { useRouter } from \"vue-router\";\nimport ScoringResultTable from \"@/views/add/components/ScoringResultTable.vue\";\nimport {\n  addScoringResultUsingPost,\n  editScoringResultUsingPost,\n} from \"@/api/scoringResultController\";\nimport message from \"@arco-design/web-vue/es/message\";\n\ninterface Props {\n  appId: string;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  appId: () => {\n    return \"\";\n  },\n});\n\nconst router = useRouter();\nconst tableRef = ref();\n\n// 表单参数\nconst form = ref({\n  resultDesc: \"\",\n  resultName: \"\",\n  resultPicture: \"\",\n} as API.ScoringResultAddRequest);\n\nconst updateId = ref<any>();\n\nconst doUpdate = (scoringResult: API.ScoringResultVO) => {\n  updateId.value = scoringResult.id;\n  form.value = scoringResult;\n};\n\n/**\n * 提交\n */\nconst handleSubmit = async () => {\n  if (!props.appId) {\n    return;\n  }\n  let res: any;\n  // 如果是修改\n  if (updateId.value) {\n    res = await editScoringResultUsingPost({\n      id: updateId.value as any,\n      ...form.value,\n    });\n  } else {\n    // 创建\n    res = await addScoringResultUsingPost({\n      appId: props.appId as any,\n      ...form.value,\n    });\n  }\n  if (res.data.code === 0) {\n    message.success(\"操作成功\");\n  } else {\n    message.error(\"操作失败，\" + res.data.message);\n  }\n  if (tableRef.value) {\n    tableRef.value.loadData();\n    updateId.value = undefined;\n  }\n};\n</script>\n"
  },
  {
    "path": "yudada-frontend/src/views/add/components/AiGenerateQuestionDrawer.vue",
    "content": "<template>\n  <a-button type=\"outline\" @click=\"handleClick\">AI 生成题目</a-button>\n  <a-drawer\n    :width=\"340\"\n    :visible=\"visible\"\n    @ok=\"handleOk\"\n    @cancel=\"handleCancel\"\n    unmountOnClose\n  >\n    <template #title>AI 生成题目</template>\n    <div>\n      <a-form\n        :model=\"form\"\n        label-align=\"left\"\n        auto-label-width\n        @submit=\"handleSubmit\"\n      >\n        <a-form-item label=\"应用 id\">\n          {{ appId }}\n        </a-form-item>\n        <a-form-item field=\"questionNumber\" label=\"题目数量\">\n          <a-input-number\n            min=\"0\"\n            max=\"20\"\n            v-model=\"form.questionNumber\"\n            placeholder=\"请输入题目数量\"\n          />\n        </a-form-item>\n        <a-form-item field=\"optionNumber\" label=\"选项数量\">\n          <a-input-number\n            min=\"0\"\n            max=\"6\"\n            v-model=\"form.optionNumber\"\n            placeholder=\"请输入选项数量\"\n          />\n        </a-form-item>\n        <a-form-item>\n          <a-space>\n            <a-button\n              :loading=\"submitting\"\n              type=\"primary\"\n              html-type=\"submit\"\n              style=\"width: 120px\"\n            >\n              {{ submitting ? \"生成中\" : \"一键生成\" }}\n            </a-button>\n            <a-button\n              :loading=\"sseSubmitting\"\n              style=\"width: 120px\"\n              @click=\"handleSSESubmit\"\n            >\n              {{ sseSubmitting ? \"生成中\" : \"实时生成\" }}\n            </a-button>\n          </a-space>\n        </a-form-item>\n      </a-form>\n    </div>\n  </a-drawer>\n</template>\n\n<script setup lang=\"ts\">\nimport { defineProps, reactive, ref, withDefaults } from \"vue\";\nimport API from \"@/api\";\nimport { aiGenerateQuestionUsingPost } from \"@/api/questionController\";\nimport message from \"@arco-design/web-vue/es/message\";\n\ninterface Props {\n  appId: string;\n  onSuccess?: (result: API.QuestionContentDTO[]) => void;\n  onSSESuccess?: (result: API.QuestionContentDTO) => void;\n  onSSEStart?: (event: any) => void;\n  onSSEClose?: (event: any) => void;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  appId: () => {\n    return \"\";\n  },\n});\n\nconst form = reactive({\n  optionNumber: 2,\n  questionNumber: 10,\n} as API.AiGenerateQuestionRequest);\n\nconst visible = ref(false);\nconst submitting = ref(false);\nconst sseSubmitting = ref(false);\n\nconst handleClick = () => {\n  visible.value = true;\n};\nconst handleOk = () => {\n  visible.value = false;\n};\nconst handleCancel = () => {\n  visible.value = false;\n};\n\n/**\n * 提交\n */\nconst handleSubmit = async () => {\n  if (!props.appId) {\n    return;\n  }\n  submitting.value = true;\n  const res = await aiGenerateQuestionUsingPost({\n    appId: props.appId as any,\n    ...form,\n  });\n  if (res.data.code === 0 && res.data.data.length > 0) {\n    if (props.onSuccess) {\n      props.onSuccess(res.data.data);\n    } else {\n      message.success(\"生成题目成功\");\n    }\n    // 关闭抽屉\n    handleCancel();\n  } else {\n    message.error(\"操作失败，\" + res.data.message);\n  }\n  submitting.value = false;\n};\n\n/**\n * 提交（实时生成）\n */\nconst handleSSESubmit = async () => {\n  if (!props.appId) {\n    return;\n  }\n  sseSubmitting.value = true;\n  // 创建 SSE 请求\n  const eventSource = new EventSource(\n    // todo 手动填写完整的后端地址\n    \"http://localhost:8101/api/question/ai_generate/sse\" +\n      `?appId=${props.appId}&optionNumber=${form.optionNumber}&questionNumber=${form.questionNumber}`\n  );\n  let first = true;\n  // 接收消息\n  eventSource.onmessage = function (event) {\n    if (first) {\n      props.onSSEStart?.(event);\n      handleCancel();\n      first = !first;\n    }\n    props.onSSESuccess?.(JSON.parse(event.data));\n  };\n  // 报错或连接关闭时触发\n  eventSource.onerror = function (event) {\n    if (event.eventPhase === EventSource.CLOSED) {\n      console.log(\"关闭连接\");\n      props.onSSEClose?.(event);\n      eventSource.close();\n    } else {\n      eventSource.close();\n    }\n  };\n  sseSubmitting.value = false;\n};\n</script>\n"
  },
  {
    "path": "yudada-frontend/src/views/add/components/ScoringResultTable.vue",
    "content": "<template>\n  <a-form\n    :model=\"formSearchParams\"\n    :style=\"{ marginBottom: '20px' }\"\n    layout=\"inline\"\n    @submit=\"doSearch\"\n  >\n    <a-form-item field=\"resultName\" label=\"结果名称\">\n      <a-input\n        v-model=\"formSearchParams.resultName\"\n        placeholder=\"请输入结果名称\"\n        allow-clear\n      />\n    </a-form-item>\n    <a-form-item field=\"resultDesc\" label=\"结果描述\">\n      <a-input\n        v-model=\"formSearchParams.resultDesc\"\n        placeholder=\"请输入结果描述\"\n        allow-clear\n      />\n    </a-form-item>\n    <a-form-item>\n      <a-button type=\"primary\" html-type=\"submit\" style=\"width: 100px\">\n        搜索\n      </a-button>\n    </a-form-item>\n  </a-form>\n  <a-table\n    :columns=\"columns\"\n    :data=\"dataList\"\n    :pagination=\"{\n      showTotal: true,\n      pageSize: searchParams.pageSize,\n      current: searchParams.current,\n      total,\n    }\"\n    @page-change=\"onPageChange\"\n  >\n    <template #resultPicture=\"{ record }\">\n      <a-image width=\"64\" :src=\"record.resultPicture\" />\n    </template>\n    <template #createTime=\"{ record }\">\n      {{ dayjs(record.createTime).format(\"YYYY-MM-DD HH:mm:ss\") }}\n    </template>\n    <template #updateTime=\"{ record }\">\n      {{ dayjs(record.updateTime).format(\"YYYY-MM-DD HH:mm:ss\") }}\n    </template>\n    <template #optional=\"{ record }\">\n      <a-space>\n        <a-button status=\"success\" @click=\"doUpdate?.(record)\">修改</a-button>\n        <a-button status=\"danger\" @click=\"doDelete(record)\">删除</a-button>\n      </a-space>\n    </template>\n  </a-table>\n</template>\n\n<script setup lang=\"ts\">\nimport { defineExpose, defineProps, ref, watchEffect, withDefaults } from \"vue\";\nimport {\n  deleteScoringResultUsingPost,\n  listScoringResultVoByPageUsingPost,\n} from \"@/api/scoringResultController\";\nimport API from \"@/api\";\nimport message from \"@arco-design/web-vue/es/message\";\nimport { dayjs } from \"@arco-design/web-vue/es/_utils/date\";\n\ninterface Props {\n  appId: string;\n  doUpdate: (scoringResult: API.ScoringResultVO) => void;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  appId: () => {\n    return \"\";\n  },\n});\n\nconst formSearchParams = ref<API.ScoringResultQueryRequest>({});\n\n// 初始化搜索条件（不应该被修改）\nconst initSearchParams = {\n  current: 1,\n  pageSize: 10,\n  sortField: \"createTime\",\n  sortOrder: \"descend\",\n};\n\nconst searchParams = ref<API.ScoringResultQueryRequest>({\n  ...initSearchParams,\n});\nconst dataList = ref<API.ScoringResultVO[]>([]);\nconst total = ref<number>(0);\n\n/**\n * 加载数据\n */\nconst loadData = async () => {\n  if (!props.appId) {\n    return;\n  }\n  const params = {\n    appId: props.appId as any,\n    ...searchParams.value,\n  };\n  const res = await listScoringResultVoByPageUsingPost(params);\n  if (res.data.code === 0) {\n    dataList.value = res.data.data?.records || [];\n    total.value = res.data.data?.total || 0;\n  } else {\n    message.error(\"获取数据失败，\" + res.data.message);\n  }\n};\n\n// 暴露函数给父组件\ndefineExpose({\n  loadData,\n});\n\n\n/**\n * 执行搜索\n */\nconst doSearch = () => {\n  searchParams.value = {\n    ...initSearchParams,\n    ...formSearchParams.value,\n  };\n};\n\n/**\n * 当分页变化时，改变搜索条件，触发数据加载\n * @param page\n */\nconst onPageChange = (page: number) => {\n  searchParams.value = {\n    ...searchParams.value,\n    current: page,\n  };\n};\n\n/**\n * 删除\n * @param record\n */\nconst doDelete = async (record: API.ScoringResult) => {\n  if (!record.id) {\n    return;\n  }\n\n  const res = await deleteScoringResultUsingPost({\n    id: record.id,\n  });\n  if (res.data.code === 0) {\n    loadData();\n  } else {\n    message.error(\"删除失败，\" + res.data.message);\n  }\n};\n\n/**\n * 监听 searchParams 变量，改变时触发数据的重新加载\n */\nwatchEffect(() => {\n  loadData();\n});\n\n// 表格列配置\nconst columns = [\n  {\n    title: \"id\",\n    dataIndex: \"id\",\n  },\n  {\n    title: \"名称\",\n    dataIndex: \"resultName\",\n  },\n  {\n    title: \"描述\",\n    dataIndex: \"resultDesc\",\n  },\n  {\n    title: \"图片\",\n    dataIndex: \"resultPicture\",\n    slotName: \"resultPicture\",\n  },\n  {\n    title: \"结果属性\",\n    dataIndex: \"resultProp\",\n  },\n  {\n    title: \"评分范围\",\n    dataIndex: \"resultScoreRange\",\n  },\n  {\n    title: \"创建时间\",\n    dataIndex: \"createTime\",\n    slotName: \"createTime\",\n  },\n  {\n    title: \"更新时间\",\n    dataIndex: \"updateTime\",\n    slotName: \"updateTime\",\n  },\n  {\n    title: \"操作\",\n    slotName: \"optional\",\n  },\n];\n</script>"
  },
  {
    "path": "yudada-frontend/src/views/admin/AdminAppPage.vue",
    "content": "<template>\n  <a-form\n    :model=\"formSearchParams\"\n    :style=\"{ marginBottom: '20px' }\"\n    layout=\"inline\"\n    @submit=\"doSearch\"\n  >\n    <a-form-item field=\"appName\" label=\"应用名称\">\n      <a-input\n        v-model=\"formSearchParams.appName\"\n        placeholder=\"请输入应用名称\"\n        allow-clear\n      />\n    </a-form-item>\n    <a-form-item field=\"appDesc\" label=\"应用描述\">\n      <a-input\n        v-model=\"formSearchParams.appDesc\"\n        placeholder=\"请输入应用描述\"\n        allow-clear\n      />\n    </a-form-item>\n    <a-form-item>\n      <a-button type=\"primary\" html-type=\"submit\" style=\"width: 100px\">\n        搜索\n      </a-button>\n    </a-form-item>\n  </a-form>\n  <a-table\n    :columns=\"columns\"\n    :data=\"dataList\"\n    :pagination=\"{\n      showTotal: true,\n      pageSize: searchParams.pageSize,\n      current: searchParams.current,\n      total,\n    }\"\n    @page-change=\"onPageChange\"\n  >\n    <template #appIcon=\"{ record }\">\n      <a-image width=\"64\" :src=\"record.appIcon\" />\n    </template>\n    <template #appType=\"{ record }\">\n      {{ APP_TYPE_MAP[record.appType] }}\n    </template>\n    <template #scoringStrategy=\"{ record }\">\n      {{ APP_SCORING_STRATEGY_MAP[record.scoringStrategy] }}\n    </template>\n    <template #reviewStatus=\"{ record }\">\n      {{ REVIEW_STATUS_MAP[record.reviewStatus] }}\n    </template>\n    <template #reviewTime=\"{ record }\">\n      {{\n        record.reviewTime &&\n        dayjs(record.reviewTime).format(\"YYYY-MM-DD HH:mm:ss\")\n      }}\n    </template>\n    <template #createTime=\"{ record }\">\n      {{ dayjs(record.createTime).format(\"YYYY-MM-DD HH:mm:ss\") }}\n    </template>\n    <template #updateTime=\"{ record }\">\n      {{ dayjs(record.updateTime).format(\"YYYY-MM-DD HH:mm:ss\") }}\n    </template>\n    <template #optional=\"{ record }\">\n      <a-space>\n        <a-button\n          v-if=\"record.reviewStatus !== REVIEW_STATUS_ENUM.PASS\"\n          status=\"success\"\n          @click=\"doReview(record, REVIEW_STATUS_ENUM.PASS, '')\"\n        >\n          通过\n        </a-button>\n        <a-button\n          v-if=\"record.reviewStatus !== REVIEW_STATUS_ENUM.REJECT\"\n          status=\"warning\"\n          @click=\"doReview(record, REVIEW_STATUS_ENUM.REJECT, '不符合上架要求')\"\n        >\n          拒绝\n        </a-button>\n        <a-button status=\"danger\" @click=\"doDelete(record)\">删除</a-button>\n      </a-space>\n    </template>\n  </a-table>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, watchEffect } from \"vue\";\nimport {\n  deleteAppUsingPost,\n  doAppReviewUsingPost,\n  listAppByPageUsingPost,\n} from \"@/api/appController\";\nimport API from \"@/api\";\nimport message from \"@arco-design/web-vue/es/message\";\nimport { dayjs } from \"@arco-design/web-vue/es/_utils/date\";\nimport {\n  APP_SCORING_STRATEGY_MAP,\n  APP_TYPE_MAP,\n  REVIEW_STATUS_ENUM,\n  REVIEW_STATUS_MAP,\n} from \"@/constant/app\";\n\nconst formSearchParams = ref<API.AppQueryRequest>({});\n\n// 初始化搜索条件（不应该被修改）\nconst initSearchParams = {\n  current: 1,\n  pageSize: 10,\n  sortOrder: \"descend\",\n  sortField: \"createTime\",\n};\n\nconst searchParams = ref<API.AppQueryRequest>({\n  ...initSearchParams,\n});\nconst dataList = ref<API.App[]>([]);\nconst total = ref<number>(0);\n\n/**\n * 加载数据\n */\nconst loadData = async () => {\n  const res = await listAppByPageUsingPost(searchParams.value);\n  if (res.data.code === 0) {\n    dataList.value = res.data.data?.records || [];\n    total.value = res.data.data?.total || 0;\n  } else {\n    message.error(\"获取数据失败，\" + res.data.message);\n  }\n};\n\n/**\n * 执行搜索\n */\nconst doSearch = () => {\n  searchParams.value = {\n    ...initSearchParams,\n    ...formSearchParams.value,\n  };\n};\n\n/**\n * 当分页变化时，改变搜索条件，触发数据加载\n * @param page\n */\nconst onPageChange = (page: number) => {\n  searchParams.value = {\n    ...searchParams.value,\n    current: page,\n  };\n};\n\n/**\n * 删除\n * @param record\n */\nconst doDelete = async (record: API.App) => {\n  if (!record.id) {\n    return;\n  }\n\n  const res = await deleteAppUsingPost({\n    id: record.id,\n  });\n  if (res.data.code === 0) {\n    loadData();\n  } else {\n    message.error(\"删除失败，\" + res.data.message);\n  }\n};\n\n/**\n * 审核\n * @param record\n * @param reviewStatus\n * @param reviewMessage\n */\nconst doReview = async (\n  record: API.App,\n  reviewStatus: number,\n  reviewMessage?: string\n) => {\n  if (!record.id) {\n    return;\n  }\n\n  const res = await doAppReviewUsingPost({\n    id: record.id,\n    reviewStatus,\n    reviewMessage,\n  });\n  if (res.data.code === 0) {\n    loadData();\n  } else {\n    message.error(\"审核失败，\" + res.data.message);\n  }\n};\n\n/**\n * 监听 searchParams 变量，改变时触发数据的重新加载\n */\nwatchEffect(() => {\n  loadData();\n});\n\n// 表格列配置\nconst columns = [\n  {\n    title: \"id\",\n    dataIndex: \"id\",\n  },\n  {\n    title: \"名称\",\n    dataIndex: \"appName\",\n  },\n  {\n    title: \"描述\",\n    dataIndex: \"appDesc\",\n  },\n  {\n    title: \"图标\",\n    dataIndex: \"appIcon\",\n    slotName: \"appIcon\",\n  },\n  {\n    title: \"应用类型\",\n    dataIndex: \"appType\",\n    slotName: \"appType\",\n  },\n  {\n    title: \"评分策略\",\n    dataIndex: \"scoringStrategy\",\n    slotName: \"scoringStrategy\",\n  },\n  {\n    title: \"审核状态\",\n    dataIndex: \"reviewStatus\",\n    slotName: \"reviewStatus\",\n  },\n  {\n    title: \"审核信息\",\n    dataIndex: \"reviewMessage\",\n  },\n  {\n    title: \"审核时间\",\n    dataIndex: \"reviewTime\",\n    slotName: \"reviewTime\",\n  },\n  {\n    title: \"审核人 id\",\n    dataIndex: \"reviewerId\",\n  },\n  {\n    title: \"用户 id\",\n    dataIndex: \"userId\",\n  },\n  {\n    title: \"创建时间\",\n    dataIndex: \"createTime\",\n    slotName: \"createTime\",\n  },\n  {\n    title: \"更新时间\",\n    dataIndex: \"updateTime\",\n    slotName: \"updateTime\",\n  },\n  {\n    title: \"操作\",\n    slotName: \"optional\",\n  },\n];\n</script>"
  },
  {
    "path": "yudada-frontend/src/views/admin/AdminQuestionPage.vue",
    "content": "<template>\n  <a-form\n    :model=\"formSearchParams\"\n    :style=\"{ marginBottom: '20px' }\"\n    layout=\"inline\"\n    @submit=\"doSearch\"\n  >\n    <a-form-item field=\"appId\" label=\"应用 id\">\n      <a-input\n        v-model=\"formSearchParams.appId\"\n        placeholder=\"请输入应用 id\"\n        allow-clear\n      />\n    </a-form-item>\n    <a-form-item field=\"userId\" label=\"用户 id\">\n      <a-input\n        v-model=\"formSearchParams.userId\"\n        placeholder=\"请输入用户 id\"\n        allow-clear\n      />\n    </a-form-item>\n    <a-form-item>\n      <a-button type=\"primary\" html-type=\"submit\" style=\"width: 100px\">\n        搜索\n      </a-button>\n    </a-form-item>\n  </a-form>\n  <a-table\n    :columns=\"columns\"\n    :data=\"dataList\"\n    :pagination=\"{\n      showTotal: true,\n      pageSize: searchParams.pageSize,\n      current: searchParams.current,\n      total,\n    }\"\n    @page-change=\"onPageChange\"\n  >\n    <template #questionContent=\"{ record }\">\n      <div\n        v-for=\"question in JSON.parse(record.questionContent)\"\n        :key=\"question.title\"\n      >\n        {{ question }}\n      </div>\n    </template>\n    <template #createTime=\"{ record }\">\n      {{ dayjs(record.createTime).format(\"YYYY-MM-DD HH:mm:ss\") }}\n    </template>\n    <template #updateTime=\"{ record }\">\n      {{ dayjs(record.updateTime).format(\"YYYY-MM-DD HH:mm:ss\") }}\n    </template>\n    <template #optional=\"{ record }\">\n      <a-space>\n        <a-button status=\"danger\" @click=\"doDelete(record)\">删除</a-button>\n      </a-space>\n    </template>\n  </a-table>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, watchEffect } from \"vue\";\nimport {\n  deleteQuestionUsingPost,\n  listQuestionByPageUsingPost,\n} from \"@/api/questionController\";\nimport API from \"@/api\";\nimport message from \"@arco-design/web-vue/es/message\";\nimport { dayjs } from \"@arco-design/web-vue/es/_utils/date\";\n\nconst formSearchParams = ref<API.QuestionQueryRequest>({});\n\n// 初始化搜索条件（不应该被修改）\nconst initSearchParams = {\n  current: 1,\n  pageSize: 10,\n};\n\nconst searchParams = ref<API.QuestionQueryRequest>({\n  ...initSearchParams,\n});\nconst dataList = ref<API.Question[]>([]);\nconst total = ref<number>(0);\n\n/**\n * 加载数据\n */\nconst loadData = async () => {\n  const res = await listQuestionByPageUsingPost(searchParams.value);\n  if (res.data.code === 0) {\n    dataList.value = res.data.data?.records || [];\n    total.value = res.data.data?.total || 0;\n  } else {\n    message.error(\"获取数据失败，\" + res.data.message);\n  }\n};\n\n/**\n * 执行搜索\n */\nconst doSearch = () => {\n  searchParams.value = {\n    ...initSearchParams,\n    ...formSearchParams.value,\n  };\n};\n\n/**\n * 当分页变化时，改变搜索条件，触发数据加载\n * @param page\n */\nconst onPageChange = (page: number) => {\n  searchParams.value = {\n    ...searchParams.value,\n    current: page,\n  };\n};\n\n/**\n * 删除\n * @param record\n */\nconst doDelete = async (record: API.Question) => {\n  if (!record.id) {\n    return;\n  }\n\n  const res = await deleteQuestionUsingPost({\n    id: record.id,\n  });\n  if (res.data.code === 0) {\n    loadData();\n  } else {\n    message.error(\"删除失败，\" + res.data.message);\n  }\n};\n\n/**\n * 监听 searchParams 变量，改变时触发数据的重新加载\n */\nwatchEffect(() => {\n  loadData();\n});\n\n// 表格列配置\nconst columns = [\n  {\n    title: \"id\",\n    dataIndex: \"id\",\n  },\n  {\n    title: \"题目内容\",\n    dataIndex: \"questionContent\",\n    slotName: \"questionContent\",\n  },\n  {\n    title: \"应用 id\",\n    dataIndex: \"appId\",\n  },\n  {\n    title: \"用户 id\",\n    dataIndex: \"userId\",\n  },\n  {\n    title: \"创建时间\",\n    dataIndex: \"createTime\",\n    slotName: \"createTime\",\n  },\n  {\n    title: \"更新时间\",\n    dataIndex: \"updateTime\",\n    slotName: \"updateTime\",\n  },\n  {\n    title: \"操作\",\n    slotName: \"optional\",\n  },\n];\n</script>\n"
  },
  {
    "path": "yudada-frontend/src/views/admin/AdminScoringResultPage.vue",
    "content": "<template>\n  <a-form\n    :model=\"formSearchParams\"\n    :style=\"{ marginBottom: '20px' }\"\n    layout=\"inline\"\n    @submit=\"doSearch\"\n  >\n    <a-form-item field=\"resultName\" label=\"结果名称\">\n      <a-input\n        v-model=\"formSearchParams.resultName\"\n        placeholder=\"请输入结果名称\"\n        allow-clear\n      />\n    </a-form-item>\n    <a-form-item field=\"resultDesc\" label=\"结果描述\">\n      <a-input\n        v-model=\"formSearchParams.resultDesc\"\n        placeholder=\"请输入结果描述\"\n        allow-clear\n      />\n    </a-form-item>\n    <a-form-item field=\"appId\" label=\"应用 id\">\n      <a-input\n        v-model=\"formSearchParams.appId\"\n        placeholder=\"请输入应用 id\"\n        allow-clear\n      />\n    </a-form-item>\n    <a-form-item field=\"userId\" label=\"用户 id\">\n      <a-input\n        v-model=\"formSearchParams.userId\"\n        placeholder=\"请输入用户 id\"\n        allow-clear\n      />\n    </a-form-item>\n    <a-form-item>\n      <a-button type=\"primary\" html-type=\"submit\" style=\"width: 100px\">\n        搜索\n      </a-button>\n    </a-form-item>\n  </a-form>\n  <a-table\n    :columns=\"columns\"\n    :data=\"dataList\"\n    :pagination=\"{\n      showTotal: true,\n      pageSize: searchParams.pageSize,\n      current: searchParams.current,\n      total,\n    }\"\n    @page-change=\"onPageChange\"\n  >\n    <template #resultPicture=\"{ record }\">\n      <a-image width=\"64\" :src=\"record.resultPicture\" />\n    </template>\n    <template #createTime=\"{ record }\">\n      {{ dayjs(record.createTime).format(\"YYYY-MM-DD HH:mm:ss\") }}\n    </template>\n    <template #updateTime=\"{ record }\">\n      {{ dayjs(record.updateTime).format(\"YYYY-MM-DD HH:mm:ss\") }}\n    </template>\n    <template #optional=\"{ record }\">\n      <a-space>\n        <a-button status=\"danger\" @click=\"doDelete(record)\">删除</a-button>\n      </a-space>\n    </template>\n  </a-table>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, watchEffect } from \"vue\";\nimport {\n  deleteScoringResultUsingPost,\n  listScoringResultByPageUsingPost,\n} from \"@/api/scoringResultController\";\nimport API from \"@/api\";\nimport message from \"@arco-design/web-vue/es/message\";\nimport { dayjs } from \"@arco-design/web-vue/es/_utils/date\";\n\nconst formSearchParams = ref<API.ScoringResultQueryRequest>({});\n\n// 初始化搜索条件（不应该被修改）\nconst initSearchParams = {\n  current: 1,\n  pageSize: 10,\n};\n\nconst searchParams = ref<API.ScoringResultQueryRequest>({\n  ...initSearchParams,\n});\nconst dataList = ref<API.ScoringResult[]>([]);\nconst total = ref<number>(0);\n\n/**\n * 加载数据\n */\nconst loadData = async () => {\n  const res = await listScoringResultByPageUsingPost(searchParams.value);\n  if (res.data.code === 0) {\n    dataList.value = res.data.data?.records || [];\n    total.value = res.data.data?.total || 0;\n  } else {\n    message.error(\"获取数据失败，\" + res.data.message);\n  }\n};\n\n/**\n * 执行搜索\n */\nconst doSearch = () => {\n  searchParams.value = {\n    ...initSearchParams,\n    ...formSearchParams.value,\n  };\n};\n\n/**\n * 当分页变化时，改变搜索条件，触发数据加载\n * @param page\n */\nconst onPageChange = (page: number) => {\n  searchParams.value = {\n    ...searchParams.value,\n    current: page,\n  };\n};\n\n/**\n * 删除\n * @param record\n */\nconst doDelete = async (record: API.ScoringResult) => {\n  if (!record.id) {\n    return;\n  }\n\n  const res = await deleteScoringResultUsingPost({\n    id: record.id,\n  });\n  if (res.data.code === 0) {\n    loadData();\n  } else {\n    message.error(\"删除失败，\" + res.data.message);\n  }\n};\n\n/**\n * 监听 searchParams 变量，改变时触发数据的重新加载\n */\nwatchEffect(() => {\n  loadData();\n});\n\n// 表格列配置\nconst columns = [\n  {\n    title: \"id\",\n    dataIndex: \"id\",\n  },\n  {\n    title: \"名称\",\n    dataIndex: \"resultName\",\n  },\n  {\n    title: \"描述\",\n    dataIndex: \"resultDesc\",\n  },\n  {\n    title: \"图片\",\n    dataIndex: \"resultPicture\",\n    slotName: \"resultPicture\",\n  },\n  {\n    title: \"结果属性\",\n    dataIndex: \"resultProp\",\n  },\n  {\n    title: \"评分范围\",\n    dataIndex: \"resultScoreRange\",\n  },\n  {\n    title: \"应用 id\",\n    dataIndex: \"appId\",\n  },\n  {\n    title: \"用户 id\",\n    dataIndex: \"userId\",\n  },\n  {\n    title: \"创建时间\",\n    dataIndex: \"createTime\",\n    slotName: \"createTime\",\n  },\n  {\n    title: \"更新时间\",\n    dataIndex: \"updateTime\",\n    slotName: \"updateTime\",\n  },\n  {\n    title: \"操作\",\n    slotName: \"optional\",\n  },\n];\n</script>"
  },
  {
    "path": "yudada-frontend/src/views/admin/AdminUserAnswerPage.vue",
    "content": "<template>\n  <a-form\n    :model=\"formSearchParams\"\n    :style=\"{ marginBottom: '20px' }\"\n    layout=\"inline\"\n    @submit=\"doSearch\"\n  >\n    <a-form-item field=\"resultName\" label=\"结果名称\">\n      <a-input\n        v-model=\"formSearchParams.resultName\"\n        placeholder=\"请输入结果名称\"\n        allow-clear\n      />\n    </a-form-item>\n    <a-form-item field=\"resultDesc\" label=\"结果描述\">\n      <a-input\n        v-model=\"formSearchParams.resultDesc\"\n        placeholder=\"请输入结果描述\"\n        allow-clear\n      />\n    </a-form-item>\n    <a-form-item field=\"appId\" label=\"应用 id\">\n      <a-input\n        v-model=\"formSearchParams.appId\"\n        placeholder=\"请输入应用 id\"\n        allow-clear\n      />\n    </a-form-item>\n    <a-form-item field=\"userId\" label=\"用户 id\">\n      <a-input\n        v-model=\"formSearchParams.userId\"\n        placeholder=\"请输入用户 id\"\n        allow-clear\n      />\n    </a-form-item>\n    <a-form-item>\n      <a-button type=\"primary\" html-type=\"submit\" style=\"width: 100px\">\n        搜索\n      </a-button>\n    </a-form-item>\n  </a-form>\n  <a-table\n    :columns=\"columns\"\n    :data=\"dataList\"\n    :pagination=\"{\n      showTotal: true,\n      pageSize: searchParams.pageSize,\n      current: searchParams.current,\n      total,\n    }\"\n    @page-change=\"onPageChange\"\n  >\n    <template #resultPicture=\"{ record }\">\n      <a-image width=\"64\" :src=\"record.resultPicture\" />\n    </template>\n    <template #appType=\"{ record }\">\n      {{ APP_TYPE_MAP[record.appType] }}\n    </template>\n    <template #scoringStrategy=\"{ record }\">\n      {{ APP_SCORING_STRATEGY_MAP[record.scoringStrategy] }}\n    </template>\n    <template #createTime=\"{ record }\">\n      {{ dayjs(record.createTime).format(\"YYYY-MM-DD HH:mm:ss\") }}\n    </template>\n    <template #updateTime=\"{ record }\">\n      {{ dayjs(record.updateTime).format(\"YYYY-MM-DD HH:mm:ss\") }}\n    </template>\n    <template #optional=\"{ record }\">\n      <a-space>\n        <a-button status=\"danger\" @click=\"doDelete(record)\">删除</a-button>\n      </a-space>\n    </template>\n  </a-table>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, watchEffect } from \"vue\";\nimport {\n  deleteUserAnswerUsingPost,\n  listUserAnswerByPageUsingPost,\n} from \"@/api/userAnswerController\";\nimport API from \"@/api\";\nimport message from \"@arco-design/web-vue/es/message\";\nimport { dayjs } from \"@arco-design/web-vue/es/_utils/date\";\nimport { APP_SCORING_STRATEGY_MAP, APP_TYPE_MAP } from \"@/constant/app\";\n\nconst formSearchParams = ref<API.UserAnswerQueryRequest>({});\n\n// 初始化搜索条件（不应该被修改）\nconst initSearchParams = {\n  current: 1,\n  pageSize: 10,\n};\n\nconst searchParams = ref<API.UserAnswerQueryRequest>({\n  ...initSearchParams,\n});\nconst dataList = ref<API.UserAnswer[]>([]);\nconst total = ref<number>(0);\n\n/**\n * 加载数据\n */\nconst loadData = async () => {\n  const res = await listUserAnswerByPageUsingPost(searchParams.value);\n  if (res.data.code === 0) {\n    dataList.value = res.data.data?.records || [];\n    total.value = res.data.data?.total || 0;\n  } else {\n    message.error(\"获取数据失败，\" + res.data.message);\n  }\n};\n\n/**\n * 执行搜索\n */\nconst doSearch = () => {\n  searchParams.value = {\n    ...initSearchParams,\n    ...formSearchParams.value,\n  };\n};\n\n/**\n * 当分页变化时，改变搜索条件，触发数据加载\n * @param page\n */\nconst onPageChange = (page: number) => {\n  searchParams.value = {\n    ...searchParams.value,\n    current: page,\n  };\n};\n\n/**\n * 删除\n * @param record\n */\nconst doDelete = async (record: API.UserAnswer) => {\n  if (!record.id) {\n    return;\n  }\n\n  const res = await deleteUserAnswerUsingPost({\n    id: record.id,\n  });\n  if (res.data.code === 0) {\n    loadData();\n  } else {\n    message.error(\"删除失败，\" + res.data.message);\n  }\n};\n\n/**\n * 监听 searchParams 变量，改变时触发数据的重新加载\n */\nwatchEffect(() => {\n  loadData();\n});\n\n// 表格列配置\nconst columns = [\n  {\n    title: \"id\",\n    dataIndex: \"id\",\n  },\n  {\n    title: \"选项\",\n    dataIndex: \"choices\",\n  },\n  {\n    title: \"结果 id\",\n    dataIndex: \"resultId\",\n  },\n  {\n    title: \"名称\",\n    dataIndex: \"resultName\",\n  },\n  {\n    title: \"描述\",\n    dataIndex: \"resultDesc\",\n  },\n  {\n    title: \"图片\",\n    dataIndex: \"resultPicture\",\n    slotName: \"resultPicture\",\n  },\n  {\n    title: \"得分\",\n    dataIndex: \"resultScore\",\n  },\n  {\n    title: \"应用 id\",\n    dataIndex: \"appId\",\n  },\n  {\n    title: \"应用类型\",\n    dataIndex: \"appType\",\n    slotName: \"appType\",\n  },\n  {\n    title: \"评分策略\",\n    dataIndex: \"scoringStrategy\",\n    slotName: \"scoringStrategy\",\n  },\n  {\n    title: \"用户 id\",\n    dataIndex: \"userId\",\n  },\n  {\n    title: \"创建时间\",\n    dataIndex: \"createTime\",\n    slotName: \"createTime\",\n  },\n  {\n    title: \"更新时间\",\n    dataIndex: \"updateTime\",\n    slotName: \"updateTime\",\n  },\n  {\n    title: \"操作\",\n    slotName: \"optional\",\n  },\n];\n</script>\n"
  },
  {
    "path": "yudada-frontend/src/views/admin/AdminUserPage.vue",
    "content": "<template>\n  <a-form\n    :model=\"formSearchParams\"\n    :style=\"{ marginBottom: '20px' }\"\n    layout=\"inline\"\n    @submit=\"doSearch\"\n  >\n    <a-form-item field=\"userName\" label=\"用户名\">\n      <a-input\n        allow-clear\n        v-model=\"formSearchParams.userName\"\n        placeholder=\"请输入用户名\"\n      />\n    </a-form-item>\n    <a-form-item field=\"userProfile\" label=\"用户简介\">\n      <a-input\n        allow-clear\n        v-model=\"formSearchParams.userProfile\"\n        placeholder=\"请输入用户简介\"\n      />\n    </a-form-item>\n    <a-form-item>\n      <a-button type=\"primary\" html-type=\"submit\" style=\"width: 100px\">\n        搜索\n      </a-button>\n    </a-form-item>\n  </a-form>\n  <a-table\n    :columns=\"columns\"\n    :data=\"dataList\"\n    :pagination=\"{\n      showTotal: true,\n      pageSize: searchParams.pageSize,\n      current: searchParams.current,\n      total,\n    }\"\n    @page-change=\"onPageChange\"\n  >\n    <template #userAvatar=\"{ record }\">\n      <a-image width=\"64\" :src=\"record.userAvatar\" />\n    </template>\n    <template #createTime=\"{ record }\">\n      {{ dayjs(record.createTime).format(\"YYYY-MM-DD HH:mm:ss\") }}\n    </template>\n    <template #updateTime=\"{ record }\">\n      {{ dayjs(record.updateTime).format(\"YYYY-MM-DD HH:mm:ss\") }}\n    </template>\n    <template #optional=\"{ record }\">\n      <a-space>\n        <a-button status=\"danger\" @click=\"doDelete(record)\">删除</a-button>\n      </a-space>\n    </template>\n  </a-table>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, watchEffect } from \"vue\";\nimport {\n  deleteUserUsingPost,\n  listUserByPageUsingPost,\n} from \"@/api/userController\";\nimport API from \"@/api\";\nimport message from \"@arco-design/web-vue/es/message\";\nimport { dayjs } from \"@arco-design/web-vue/es/_utils/date\";\n\nconst formSearchParams = ref<API.UserQueryRequest>({});\n\n// 初始化搜索条件（不应该被修改）\nconst initSearchParams = {\n  current: 1,\n  pageSize: 10,\n};\n\nconst searchParams = ref<API.UserQueryRequest>({\n  ...initSearchParams,\n});\nconst dataList = ref<API.User[]>([]);\nconst total = ref<number>(0);\n\n/**\n * 加载数据\n */\nconst loadData = async () => {\n  const res = await listUserByPageUsingPost(searchParams.value);\n  if (res.data.code === 0) {\n    dataList.value = res.data.data?.records || [];\n    total.value = res.data.data?.total || 0;\n  } else {\n    message.error(\"获取数据失败，\" + res.data.message);\n  }\n};\n\n/**\n * 执行搜索\n */\nconst doSearch = () => {\n  searchParams.value = {\n    ...initSearchParams,\n    ...formSearchParams.value,\n  };\n};\n\n/**\n * 当分页变化时，改变搜索条件，触发数据加载\n * @param page\n */\nconst onPageChange = (page: number) => {\n  searchParams.value = {\n    ...searchParams.value,\n    current: page,\n  };\n};\n\n/**\n * 删除\n * @param record\n */\nconst doDelete = async (record: API.User) => {\n  if (!record.id) {\n    return;\n  }\n\n  const res = await deleteUserUsingPost({\n    id: record.id,\n  });\n  if (res.data.code === 0) {\n    loadData();\n  } else {\n    message.error(\"删除失败，\" + res.data.message);\n  }\n};\n\n/**\n * 监听 searchParams 变量，改变时触发数据的重新加载\n */\nwatchEffect(() => {\n  loadData();\n});\n\n// 表格列配置\nconst columns = [\n  {\n    title: \"id\",\n    dataIndex: \"id\",\n  },\n  {\n    title: \"账号\",\n    dataIndex: \"userAccount\",\n  },\n  {\n    title: \"用户名\",\n    dataIndex: \"userName\",\n  },\n  {\n    title: \"用户头像\",\n    dataIndex: \"userAvatar\",\n    slotName: \"userAvatar\",\n  },\n  {\n    title: \"用户简介\",\n    dataIndex: \"userProfile\",\n  },\n  {\n    title: \"权限\",\n    dataIndex: \"userRole\",\n  },\n  {\n    title: \"创建时间\",\n    dataIndex: \"createTime\",\n    slotName: \"createTime\",\n  },\n  {\n    title: \"更新时间\",\n    dataIndex: \"updateTime\",\n    slotName: \"updateTime\",\n  },\n  {\n    title: \"操作\",\n    slotName: \"optional\",\n  },\n];\n</script>\n"
  },
  {
    "path": "yudada-frontend/src/views/answer/AnswerResultPage.vue",
    "content": "<template>\n  <div id=\"answerResultPage\">\n    <a-card>\n      <a-row style=\"margin-bottom: 16px\">\n        <a-col flex=\"auto\" class=\"content-wrapper\">\n          <h2>{{ data.resultName }}</h2>\n          <p>结果描述：{{ data.resultDesc }}</p>\n          <p>结果 id：{{ data.resultId }}</p>\n          <p>结果得分：{{ data.resultScore }}</p>\n          <p>我的答案：{{ data.choices }}</p>\n          <p>应用 id：{{ data.appId }}</p>\n          <p>应用类型：{{ APP_TYPE_MAP[data.appType] }}</p>\n          <p>评分策略：{{ APP_SCORING_STRATEGY_MAP[data.scoringStrategy] }}</p>\n          <p>\n            <a-space>\n              答题人：\n              <div :style=\"{ display: 'flex', alignItems: 'center' }\">\n                <a-avatar\n                  :size=\"24\"\n                  :image-url=\"data.user?.userAvatar\"\n                  :style=\"{ marginRight: '8px' }\"\n                />\n                <a-typography-text\n                  >{{ data.user?.userName ?? \"无名\" }}\n                </a-typography-text>\n              </div>\n            </a-space>\n          </p>\n          <p>\n            答题时间：{{ dayjs(data.createTime).format(\"YYYY-MM-DD HH:mm:ss\") }}\n          </p>\n          <a-space size=\"medium\">\n            <a-button type=\"primary\" :href=\"`/answer/do/${data.appId}`\"\n              >去答题\n            </a-button>\n          </a-space>\n        </a-col>\n        <a-col flex=\"320px\">\n          <a-image width=\"100%\" :src=\"data.resultPicture\" />\n        </a-col>\n      </a-row>\n    </a-card>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { defineProps, ref, watchEffect, withDefaults } from \"vue\";\nimport API from \"@/api\";\nimport { getUserAnswerVoByIdUsingGet } from \"@/api/userAnswerController\";\nimport message from \"@arco-design/web-vue/es/message\";\nimport { useRouter } from \"vue-router\";\nimport { dayjs } from \"@arco-design/web-vue/es/_utils/date\";\nimport { APP_SCORING_STRATEGY_MAP, APP_TYPE_MAP } from \"../../constant/app\";\n\ninterface Props {\n  id: string;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  id: () => {\n    return \"\";\n  },\n});\n\nconst router = useRouter();\n\nconst data = ref<API.UserAnswerVO>({});\n\n/**\n * 加载数据\n */\nconst loadData = async () => {\n  if (!props.id) {\n    return;\n  }\n  const res = await getUserAnswerVoByIdUsingGet({\n    id: props.id as any,\n  });\n  if (res.data.code === 0) {\n    data.value = res.data.data as any;\n  } else {\n    message.error(\"获取数据失败，\" + res.data.message);\n  }\n};\n\n/**\n * 监听 searchParams 变量，改变时触发数据的重新加载\n */\nwatchEffect(() => {\n  loadData();\n});\n</script>\n\n<style scoped>\n#answerResultPage {\n}\n\n#answerResultPage .content-wrapper > * {\n  margin-bottom: 24px;\n}\n</style>"
  },
  {
    "path": "yudada-frontend/src/views/answer/DoAnswerPage.vue",
    "content": "<template>\n  <div id=\"doAnswerPage\">\n    <a-card>\n      <h1>{{ app.appName }}</h1>\n      <p>{{ app.appDesc }}</p>\n      <h2 style=\"margin-bottom: 16px\">\n        {{ current }}、{{ currentQuestion?.title }}\n      </h2>\n      <div>\n        <a-radio-group\n          direction=\"vertical\"\n          v-model=\"currentAnswer\"\n          :options=\"questionOptions\"\n          @change=\"doRadioChange\"\n        />\n      </div>\n      <div style=\"margin-top: 24px\">\n        <a-space size=\"large\">\n          <a-button\n            type=\"primary\"\n            circle\n            v-if=\"current < questionContent.length\"\n            :disabled=\"!currentAnswer\"\n            @click=\"current += 1\"\n          >\n            下一题\n          </a-button>\n          <a-button\n            type=\"primary\"\n            v-if=\"current === questionContent.length\"\n            :loading=\"submitting\"\n            circle\n            :disabled=\"!currentAnswer\"\n            @click=\"doSubmit\"\n          >\n            {{ submitting ? \"评分中\" : \"查看结果\" }}\n          </a-button>\n          <a-button v-if=\"current > 1\" circle @click=\"current -= 1\">\n            上一题\n          </a-button>\n        </a-space>\n      </div>\n    </a-card>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport {\n  computed,\n  defineProps,\n  reactive,\n  ref,\n  watchEffect,\n  withDefaults,\n} from \"vue\";\nimport API from \"@/api\";\nimport { useRouter } from \"vue-router\";\nimport { listQuestionVoByPageUsingPost } from \"@/api/questionController\";\nimport message from \"@arco-design/web-vue/es/message\";\nimport { getAppVoByIdUsingGet } from \"@/api/appController\";\nimport {\n  addUserAnswerUsingPost,\n  generateUserAnswerIdUsingGet,\n} from \"@/api/userAnswerController\";\n\ninterface Props {\n  appId: string;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  appId: () => {\n    return \"\";\n  },\n});\n\nconst router = useRouter();\n\nconst app = ref<API.AppVO>({});\n// 题目内容结构（理解为题目列表）\nconst questionContent = ref<API.QuestionContentDTO[]>([]);\n\n// 当前题目的序号（从 1 开始）\nconst current = ref(1);\n// 当前题目\nconst currentQuestion = ref<API.QuestionContentDTO>({});\n// 当前题目选项\nconst questionOptions = computed(() => {\n  return currentQuestion.value?.options\n    ? currentQuestion.value.options.map((option) => {\n        return {\n          label: `${option.key}. ${option.value}`,\n          value: option.key,\n        };\n      })\n    : [];\n});\n// 当前答案\nconst currentAnswer = ref<string>();\n// 回答列表\nconst answerList = reactive<string[]>([]);\n// 是否正在提交结果\nconst submitting = ref(false);\n\n// 唯一 id\nconst id = ref<number>();\n\n// 生成唯一 id\nconst generateId = async () => {\n  const res = await generateUserAnswerIdUsingGet();\n  if (res.data.code === 0) {\n    id.value = res.data.data as any;\n  } else {\n    message.error(\"获取唯一 id 失败，\" + res.data.message);\n  }\n};\n\n// 进入页面时，生成唯一 id\nwatchEffect(() => {\n  generateId();\n});\n\n/**\n * 加载数据\n */\nconst loadData = async () => {\n  if (!props.appId) {\n    return;\n  }\n  // 获取 app\n  let res: any = await getAppVoByIdUsingGet({\n    id: props.appId as any,\n  });\n  if (res.data.code === 0) {\n    app.value = res.data.data as any;\n  } else {\n    message.error(\"获取应用失败，\" + res.data.message);\n  }\n  // 获取题目\n  res = await listQuestionVoByPageUsingPost({\n    appId: props.appId as any,\n    current: 1,\n    pageSize: 1,\n    sortField: \"createTime\",\n    sortOrder: \"descend\",\n  });\n  if (res.data.code === 0 && res.data.data?.records) {\n    questionContent.value = res.data.data.records[0].questionContent;\n  } else {\n    message.error(\"获取题目失败，\" + res.data.message);\n  }\n};\n\n// 获取旧数据\nwatchEffect(() => {\n  loadData();\n});\n\n// 改变 current 题号后，会自动更新当前题目和答案\nwatchEffect(() => {\n  currentQuestion.value = questionContent.value[current.value - 1];\n  currentAnswer.value = answerList[current.value - 1];\n});\n\n/**\n * 选中选项后，保存选项记录\n * @param value\n */\nconst doRadioChange = (value: string) => {\n  answerList[current.value - 1] = value;\n};\n\n/**\n * 提交\n */\nconst doSubmit = async () => {\n  if (!props.appId || !answerList) {\n    return;\n  }\n  submitting.value = true;\n  const res = await addUserAnswerUsingPost({\n    appId: props.appId as any,\n    choices: answerList,\n    id: id.value as any,\n  });\n  if (res.data.code === 0 && res.data.data) {\n    router.push(`/answer/result/${res.data.data}`);\n  } else {\n    message.error(\"提交答案失败，\" + res.data.message);\n  }\n  submitting.value = false;\n};\n</script>\n"
  },
  {
    "path": "yudada-frontend/src/views/answer/MyAnswerPage.vue",
    "content": "<template>\n  <a-form\n    :model=\"formSearchParams\"\n    :style=\"{ marginBottom: '20px' }\"\n    layout=\"inline\"\n    @submit=\"doSearch\"\n  >\n    <a-form-item field=\"resultName\" label=\"结果名称\">\n      <a-input\n        v-model=\"formSearchParams.resultName\"\n        placeholder=\"请输入结果名称\"\n        allow-clear\n      />\n    </a-form-item>\n    <a-form-item field=\"resultDesc\" label=\"结果描述\">\n      <a-input\n        v-model=\"formSearchParams.resultDesc\"\n        placeholder=\"请输入结果描述\"\n        allow-clear\n      />\n    </a-form-item>\n    <a-form-item field=\"appId\" label=\"应用 id\">\n      <a-input\n        v-model=\"formSearchParams.appId\"\n        placeholder=\"请输入应用 id\"\n        allow-clear\n      />\n    </a-form-item>\n    <a-form-item>\n      <a-button type=\"primary\" html-type=\"submit\" style=\"width: 100px\">\n        搜索\n      </a-button>\n    </a-form-item>\n  </a-form>\n  <a-table\n    :columns=\"columns\"\n    :data=\"dataList\"\n    :pagination=\"{\n      showTotal: true,\n      pageSize: searchParams.pageSize,\n      current: searchParams.current,\n      total,\n    }\"\n    @page-change=\"onPageChange\"\n  >\n    <template #resultPicture=\"{ record }\">\n      <a-image width=\"64\" :src=\"record.resultPicture\" />\n    </template>\n    <template #appType=\"{ record }\">\n      {{ APP_TYPE_MAP[record.appType] }}\n    </template>\n    <template #scoringStrategy=\"{ record }\">\n      {{ APP_SCORING_STRATEGY_MAP[record.scoringStrategy] }}\n    </template>\n    <template #createTime=\"{ record }\">\n      {{ dayjs(record.createTime).format(\"YYYY-MM-DD HH:mm:ss\") }}\n    </template>\n    <template #updateTime=\"{ record }\">\n      {{ dayjs(record.updateTime).format(\"YYYY-MM-DD HH:mm:ss\") }}\n    </template>\n    <template #optional=\"{ record }\">\n      <a-space>\n        <a-button status=\"danger\" @click=\"doDelete(record)\">删除</a-button>\n      </a-space>\n    </template>\n  </a-table>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, watchEffect } from \"vue\";\nimport {\n  deleteUserAnswerUsingPost,\n  listMyUserAnswerVoByPageUsingPost,\n} from \"@/api/userAnswerController\";\nimport API from \"@/api\";\nimport message from \"@arco-design/web-vue/es/message\";\nimport { dayjs } from \"@arco-design/web-vue/es/_utils/date\";\nimport { APP_SCORING_STRATEGY_MAP, APP_TYPE_MAP } from \"@/constant/app\";\n\nconst formSearchParams = ref<API.UserAnswerQueryRequest>({});\n\n// 初始化搜索条件（不应该被修改）\nconst initSearchParams = {\n  current: 1,\n  pageSize: 10,\n};\n\nconst searchParams = ref<API.UserAnswerQueryRequest>({\n  ...initSearchParams,\n});\nconst dataList = ref<API.UserAnswerVO[]>([]);\nconst total = ref<number>(0);\n\n/**\n * 加载数据\n */\nconst loadData = async () => {\n  const res = await listMyUserAnswerVoByPageUsingPost(searchParams.value);\n  if (res.data.code === 0) {\n    dataList.value = res.data.data?.records || [];\n    total.value = res.data.data?.total || 0;\n  } else {\n    message.error(\"获取数据失败，\" + res.data.message);\n  }\n};\n\n/**\n * 执行搜索\n */\nconst doSearch = () => {\n  searchParams.value = {\n    ...initSearchParams,\n    ...formSearchParams.value,\n  };\n};\n\n/**\n * 当分页变化时，改变搜索条件，触发数据加载\n * @param page\n */\nconst onPageChange = (page: number) => {\n  searchParams.value = {\n    ...searchParams.value,\n    current: page,\n  };\n};\n\n/**\n * 删除\n * @param record\n */\nconst doDelete = async (record: API.UserAnswer) => {\n  if (!record.id) {\n    return;\n  }\n\n  const res = await deleteUserAnswerUsingPost({\n    id: record.id,\n  });\n  if (res.data.code === 0) {\n    loadData();\n  } else {\n    message.error(\"删除失败，\" + res.data.message);\n  }\n};\n\n/**\n * 监听 searchParams 变量，改变时触发数据的重新加载\n */\nwatchEffect(() => {\n  loadData();\n});\n\n// 表格列配置\nconst columns = [\n  {\n    title: \"id\",\n    dataIndex: \"id\",\n  },\n  {\n    title: \"选项\",\n    dataIndex: \"choices\",\n  },\n  {\n    title: \"结果 id\",\n    dataIndex: \"resultId\",\n  },\n  {\n    title: \"名称\",\n    dataIndex: \"resultName\",\n  },\n  {\n    title: \"描述\",\n    dataIndex: \"resultDesc\",\n  },\n  {\n    title: \"图片\",\n    dataIndex: \"resultPicture\",\n    slotName: \"resultPicture\",\n  },\n  {\n    title: \"得分\",\n    dataIndex: \"resultScore\",\n  },\n  {\n    title: \"应用 id\",\n    dataIndex: \"appId\",\n  },\n  {\n    title: \"应用类型\",\n    dataIndex: \"appType\",\n    slotName: \"appType\",\n  },\n  {\n    title: \"评分策略\",\n    dataIndex: \"scoringStrategy\",\n    slotName: \"scoringStrategy\",\n  },\n  {\n    title: \"创建时间\",\n    dataIndex: \"createTime\",\n    slotName: \"createTime\",\n  },\n  {\n    title: \"操作\",\n    slotName: \"optional\",\n  },\n];\n</script>"
  },
  {
    "path": "yudada-frontend/src/views/app/AppDetailPage.vue",
    "content": "<template>\n  <div id=\"appDetailPage\">\n    <a-card>\n      <a-row style=\"margin-bottom: 16px\">\n        <a-col flex=\"auto\" class=\"content-wrapper\">\n          <h2>{{ data.appName }}</h2>\n          <p>{{ data.appDesc }}</p>\n          <p>应用类型：{{ APP_TYPE_MAP[data.appType] }}</p>\n          <p>评分策略：{{ APP_SCORING_STRATEGY_MAP[data.scoringStrategy] }}</p>\n          <p>\n            <a-space>\n              作者：\n              <div :style=\"{ display: 'flex', alignItems: 'center' }\">\n                <a-avatar\n                  :size=\"24\"\n                  :image-url=\"data.user?.userAvatar\"\n                  :style=\"{ marginRight: '8px' }\"\n                />\n                <a-typography-text\n                  >{{ data.user?.userName ?? \"无名\" }}\n                </a-typography-text>\n              </div>\n            </a-space>\n          </p>\n          <p>\n            创建时间：{{ dayjs(data.createTime).format(\"YYYY-MM-DD HH:mm:ss\") }}\n          </p>\n          <a-space size=\"medium\">\n            <a-button type=\"primary\" :href=\"`/answer/do/${id}`\"\n              >开始答题\n            </a-button>\n            <a-button @click=\"doShare\">分享应用</a-button>\n            <a-button v-if=\"isMy\" :href=\"`/add/question/${id}`\"\n              >设置题目\n            </a-button>\n            <a-button v-if=\"isMy\" :href=\"`/add/scoring_result/${id}`\"\n              >设置评分\n            </a-button>\n            <a-button v-if=\"isMy\" :href=\"`/add/app/${id}`\">修改应用</a-button>\n          </a-space>\n        </a-col>\n        <a-col flex=\"320px\">\n          <a-image width=\"100%\" :src=\"data.appIcon\" />\n        </a-col>\n      </a-row>\n    </a-card>\n    <ShareModal :link=\"shareLink\" title=\"应用分享\" ref=\"shareModalRef\" />\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, defineProps, ref, watchEffect, withDefaults } from \"vue\";\nimport API from \"@/api\";\nimport { getAppVoByIdUsingGet } from \"@/api/appController\";\nimport message from \"@arco-design/web-vue/es/message\";\nimport { useRouter } from \"vue-router\";\nimport { dayjs } from \"@arco-design/web-vue/es/_utils/date\";\nimport { useLoginUserStore } from \"@/store/userStore\";\nimport { APP_SCORING_STRATEGY_MAP, APP_TYPE_MAP } from \"@/constant/app\";\nimport ShareModal from \"@/components/ShareModal.vue\";\n\ninterface Props {\n  id: string;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  id: () => {\n    return \"\";\n  },\n});\n\nconst router = useRouter();\n\nconst data = ref<API.AppVO>({});\n\n// 获取登录用户\nconst loginUserStore = useLoginUserStore();\nlet loginUserId = loginUserStore.loginUser?.id;\n// 是否为本人创建\nconst isMy = computed(() => {\n  return loginUserId && loginUserId === data.value.userId;\n});\n\n/**\n * 加载数据\n */\nconst loadData = async () => {\n  if (!props.id) {\n    return;\n  }\n  const res = await getAppVoByIdUsingGet({\n    id: props.id as any,\n  });\n  if (res.data.code === 0) {\n    data.value = res.data.data as any;\n  } else {\n    message.error(\"获取数据失败，\" + res.data.message);\n  }\n};\n\n/**\n * 监听 searchParams 变量，改变时触发数据的重新加载\n */\nwatchEffect(() => {\n  loadData();\n});\n\n// 分享弹窗的引用\nconst shareModalRef = ref();\n\n// 分享链接\nconst shareLink = `${window.location.protocol}//${window.location.host}/app/detail/${props.id}`;\n\n// 分享\nconst doShare = (e: Event) => {\n  if (shareModalRef.value) {\n    shareModalRef.value.openModal();\n  }\n  // 阻止冒泡，防止跳转到详情页\n  e.stopPropagation();\n};\n</script>\n\n<style scoped>\n#appDetailPage {\n}\n\n#appDetailPage .content-wrapper > * {\n  margin-bottom: 24px;\n}\n</style>\n"
  },
  {
    "path": "yudada-frontend/src/views/statistic/AppStatisticPage.vue",
    "content": "<template>\n  <div id=\"appStatisticPage\">\n    <h2>热门应用统计</h2>\n    <v-chart :option=\"appAnswerCountOptions\" style=\"height: 300px\" />\n    <h2>应用结果统计</h2>\n    <div class=\"search-bar\">\n      <a-input-search\n        :style=\"{ width: '320px' }\"\n        placeholder=\"输入 appId\"\n        button-text=\"搜索\"\n        size=\"large\"\n        search-button\n        @search=\"(value) => loadAppAnswerResultCountData(value)\"\n      />\n    </div>\n    <div style=\"margin-bottom: 16px\" />\n    <v-chart :option=\"appAnswerResultCountOptions\" style=\"height: 300px\" />\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watchEffect } from \"vue\";\nimport API from \"@/api\";\nimport message from \"@arco-design/web-vue/es/message\";\nimport {\n  getAppAnswerCountUsingGet,\n  getAppAnswerResultCountUsingGet,\n} from \"@/api/appStatisticController\";\nimport VChart from \"vue-echarts\";\nimport \"echarts\";\n\nconst appAnswerCountList = ref<API.AppAnswerCountDTO[]>([]);\nconst appAnswerResultCountList = ref<API.AppAnswerResultCountDTO[]>([]);\n\n/**\n * 加载数据\n */\nconst loadAppAnswerCountData = async () => {\n  const res = await getAppAnswerCountUsingGet();\n  if (res.data.code === 0) {\n    appAnswerCountList.value = res.data.data || [];\n  } else {\n    message.error(\"获取数据失败，\" + res.data.message);\n  }\n};\n\n// 统计选项\nconst appAnswerCountOptions = computed(() => {\n  return {\n    xAxis: {\n      type: \"category\",\n      data: appAnswerCountList.value.map((item) => item.appId),\n      name: \"应用 id\",\n    },\n    yAxis: {\n      type: \"value\",\n      name: \"用户答案数\",\n    },\n    series: [\n      {\n        data: appAnswerCountList.value.map((item) => item.answerCount),\n        type: \"bar\",\n      },\n    ],\n  };\n});\n\n/**\n * 加载数据\n */\nconst loadAppAnswerResultCountData = async (appId: string) => {\n  if (!appId) {\n    return;\n  }\n  const res = await getAppAnswerResultCountUsingGet({\n    appId: appId as any,\n  });\n  if (res.data.code === 0) {\n    appAnswerResultCountList.value = res.data.data || [];\n  } else {\n    message.error(\"获取数据失败，\" + res.data.message);\n  }\n};\n\n// 统计选项\nconst appAnswerResultCountOptions = computed(() => {\n  return {\n    tooltip: {\n      trigger: \"item\",\n    },\n    legend: {\n      orient: \"vertical\",\n      left: \"left\",\n    },\n    series: [\n      {\n        name: \"应用答案结果分布\",\n        type: \"pie\",\n        radius: \"50%\",\n        data: appAnswerResultCountList.value.map((item) => {\n          return { value: item.resultCount, name: item.resultName };\n        }),\n        emphasis: {\n          itemStyle: {\n            shadowBlur: 10,\n            shadowOffsetX: 0,\n            shadowColor: \"rgba(0, 0, 0, 0.5)\",\n          },\n        },\n      },\n    ],\n  };\n});\n\n/**\n * 参数改变时触发数据的重新加载\n */\nwatchEffect(() => {\n  loadAppAnswerCountData();\n});\n\n/**\n * 参数改变时触发数据的重新加载\n */\nwatchEffect(() => {\n  loadAppAnswerResultCountData(\"\");\n});\n</script>\n\n<style scoped></style>\n"
  },
  {
    "path": "yudada-frontend/src/views/user/UserLoginPage.vue",
    "content": "<template>\n  <div id=\"userLoginPage\">\n    <h2 style=\"margin-bottom: 16px\">用户登录</h2>\n    <a-form\n      :model=\"form\"\n      :style=\"{ width: '480px', margin: '0 auto' }\"\n      label-align=\"left\"\n      auto-label-width\n      @submit=\"handleSubmit\"\n    >\n      <a-form-item field=\"userAccount\" label=\"账号\">\n        <a-input v-model=\"form.userAccount\" placeholder=\"请输入账号\" />\n      </a-form-item>\n      <a-form-item field=\"userPassword\" tooltip=\"密码不小于 8 位\" label=\"密码\">\n        <a-input-password\n          v-model=\"form.userPassword\"\n          placeholder=\"请输入密码\"\n        />\n      </a-form-item>\n      <a-form-item>\n        <div\n          style=\"\n            display: flex;\n            width: 100%;\n            align-items: center;\n            justify-content: space-between;\n          \"\n        >\n          <a-button type=\"primary\" html-type=\"submit\" style=\"width: 120px\">\n            登录\n          </a-button>\n          <a-link href=\"/user/register\">新用户注册</a-link>\n        </div>\n      </a-form-item>\n    </a-form>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { reactive } from \"vue\";\nimport API from \"@/api\";\nimport { userLoginUsingPost } from \"@/api/userController\";\nimport { useLoginUserStore } from \"@/store/userStore\";\nimport message from \"@arco-design/web-vue/es/message\";\nimport { useRouter } from \"vue-router\";\n\nconst loginUserStore = useLoginUserStore();\nconst router = useRouter();\n\nconst form = reactive({\n  userAccount: \"\",\n  userPassword: \"\",\n} as API.UserLoginRequest);\n\n/**\n * 提交\n */\nconst handleSubmit = async () => {\n  const res = await userLoginUsingPost(form);\n  if (res.data.code === 0) {\n    await loginUserStore.fetchLoginUser();\n    message.success(\"登录成功\");\n    router.push({\n      path: \"/\",\n      replace: true,\n    });\n  } else {\n    message.error(\"登录失败，\" + res.data.message);\n  }\n};\n</script>\n"
  },
  {
    "path": "yudada-frontend/src/views/user/UserRegisterPage.vue",
    "content": "<template>\n  <div id=\"userRegisterPage\">\n    <h2 style=\"margin-bottom: 16px\">用户注册</h2>\n    <a-form\n      :model=\"form\"\n      :style=\"{ width: '480px', margin: '0 auto' }\"\n      label-align=\"left\"\n      auto-label-width\n      @submit=\"handleSubmit\"\n    >\n      <a-form-item field=\"userAccount\" label=\"账号\">\n        <a-input v-model=\"form.userAccount\" placeholder=\"请输入账号\" />\n      </a-form-item>\n      <a-form-item field=\"userPassword\" tooltip=\"密码不小于 8 位\" label=\"密码\">\n        <a-input-password\n          v-model=\"form.userPassword\"\n          placeholder=\"请输入密码\"\n        />\n      </a-form-item>\n      <a-form-item\n        field=\"checkPassword\"\n        tooltip=\"确认密码不小于 8 位\"\n        label=\"确认密码\"\n      >\n        <a-input-password\n          v-model=\"form.checkPassword\"\n          placeholder=\"请输入确认密码\"\n        />\n      </a-form-item>\n      <a-form-item>\n        <div\n          style=\"\n            display: flex;\n            width: 100%;\n            align-items: center;\n            justify-content: space-between;\n          \"\n        >\n          <a-button type=\"primary\" html-type=\"submit\" style=\"width: 120px\">\n            注册\n          </a-button>\n          <a-link href=\"/user/login\">老用户登录</a-link>\n        </div>\n      </a-form-item>\n    </a-form>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { reactive } from \"vue\";\nimport API from \"@/api\";\nimport { userRegisterUsingPost } from \"@/api/userController\";\nimport message from \"@arco-design/web-vue/es/message\";\nimport { useRouter } from \"vue-router\";\n\nconst router = useRouter();\n\nconst form = reactive({\n  userAccount: \"\",\n  userPassword: \"\",\n  checkPassword: \"\",\n} as API.UserRegisterRequest);\n\n/**\n * 提交\n */\nconst handleSubmit = async () => {\n  const res = await userRegisterUsingPost(form);\n  if (res.data.code === 0) {\n    message.success(\"注册成功\");\n    router.push({\n      path: \"/user/login\",\n      replace: true,\n    });\n  } else {\n    message.error(\"注册失败，\" + res.data.message);\n  }\n};\n</script>\n"
  },
  {
    "path": "yudada-frontend/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"module\": \"esnext\",\n    \"strict\": true,\n    \"jsx\": \"preserve\",\n    \"moduleResolution\": \"node\",\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"useDefineForClassFields\": true,\n    \"sourceMap\": true,\n    \"baseUrl\": \".\",\n    \"types\": [\n      \"webpack-env\"\n    ],\n    \"paths\": {\n      \"@/*\": [\n        \"src/*\"\n      ]\n    },\n    \"lib\": [\n      \"esnext\",\n      \"dom\",\n      \"dom.iterable\",\n      \"scripthost\"\n    ]\n  },\n  \"include\": [\n    \"src/**/*.ts\",\n    \"src/**/*.tsx\",\n    \"src/**/*.vue\",\n    \"tests/**/*.ts\",\n    \"tests/**/*.tsx\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  },
  {
    "path": "yudada-frontend/vue.config.js",
    "content": "const { defineConfig } = require(\"@vue/cli-service\");\nmodule.exports = defineConfig({\n  transpileDependencies: true,\n  lintOnSave: false,\n});\n"
  }
]