岐黄慧问六月最终项目完成情况
经过数个月的开发,岐黄慧问最终成功完成所有功能,以下是该项目所有模块的代码说明:
每日中药卡片生成模块后端代码说明
(一).数据库设计
1. 中药表 (`herb`)
- `id` (主键, INT)
- `name` (名称, VARCHAR)
- `image_url` (图片URL, VARCHAR)
- `description` (简介, TEXT)
- `created_time` (创建时间, DATETIME)
2. 用户推送记录表 (`push_record`)
- `id` (主键, INT)
- `user_id` (用户ID, INT, 外键关联用户表)
- `herb_id` (中药ID, String, 外键关联中药表)
- `push_date` (推送日期, DATE)
- `blessing` (吉祥话, TEXT)
- `created_time` (创建时间, DATETIME)
- UML类图
(二).Model层(详细代码见项目Mode包中相关类)
1.herb类:与中药表对应的对象,记录中药相关信息。
2.pushRecord类:记录每天给用户推送的中药信息。
4.代码中应用到的相关技术描述
(三).Service层(详细代码见项目service包中对应的类)
1.Service层在项目中的作用
在 Spring Boot 应用的经典分层架构中,Service 层(服务层)扮演着至关重要的角色,它是连接控制器层(Controller)和数据访问层(Repository)的桥梁。
2.HerbService类(用来编写和中草药相关的操作业务)
3.PushRecordService类(用来编写与推送记录相关的操作业务)
4.代码应用到的相关技术说明。
相关注解
利用MongoDB管道查询一条随机记录
(四).DAO层/Mapper层(持久层,用来编写和数据库相关的操作,详细代码见项目中repository文件夹中相关类)
1.HerbRepository(用来编写和中草药相关的数据库操作)
2.pushRecordRepository(编写推送中药卡片记录的数据库操作)
3.代码中应用到的相关技术介绍
JPA技术
引入依赖
在Properties配置文件中添加配置信息
继承相关接口
public interface XXXRepository extends (Jpa/Mongo)ReposiMongotory <ClassName, Integer>
最后只需声明方法,JPA会自动根据方法名为我们生成对应的方法体而无需手动编写操作数据库的具体代码。
例如本段代码中findbyId(Interger id)等价于生成
Select herb from table herb where id = {id}.
(五).Controller层(具体代码见项目Controller包下相关类)
CardController类
在该类中我们要编写一个接口,前端检测到用户查询每日中药卡片推送记录时会发送一个请求,我们的接口要返回给前端一个CardVO对象,包含的属性依次代表中草药图片地址、名称、用途用法、还有一句调用deepseek大模型生成的和该中药相关的祝福语,同时要满足
用户如果是当天第一次登陆则返回一个随机草药信息,如果不是返回当天已经推送的信息,下面我们来看具体的实现逻辑。
-
- 请求头校验与用户信息获取
前端发送的请求头中会携带与用户身份相关的token信息,我们首先校验该token,将校验出的身份信息进行验证。
-
- 创建需要的对象
随后我们创建三个对象,分别为准备返回给前端的CardVO对象,和精确到天的currentDate对象和精确到秒的currentDateTime对象。我们随后将用户信息和currentDate对象传入isPushed方法中,该方法回去调用一个查询当前用户在当天pushRecode记录的数据库操作,根据方法返回的结果便可以知道该用户在当天是否是第一次登陆。
-
- 根据上一阶段的成果选择对应的执行逻辑
如果是,则从数据库中随机查询一条中草药信息,为了安全我们用optional确保查询为空时做出安全措施,在检验确实查出来一条记录后将该记录封装到cardVO中,并调用已经封装好的方法调用大模型为我们生成一句和该中草药相关信息的祝福存入cardVO相关属性中。在返回给前端信息时先将相关推送信息封装成一个pushRecord对象存储到数据库中。
如果不是,则将之前推送的信息返回
4.封装的调用大模型相关方法
先定义大模型API
再封装相关的提示词(用于规定AI生成的祝福语结构),并基于RestTemplate发送请求给大模型拿到返回结果,经行正则校验和最大长度限制。
每日中药卡片生成模块前端代码说明
模块概述
中药卡片模块是一个展示每日中药材信息的交互式组件,主要功能包括:
- 预加载中药材数据
- 显示中药材卡片弹窗
- 处理用户交互逻辑
该模块集成在导航栏组件中,通过点击"每日卡片"按钮触发显示。
核心代码分析
1. 模板部分 (Template)
<!-- 中药卡片弹窗 --> <n-modal v-model:show="showCardModal" @clickoutside="closeModal" > <n-card style="width: 600px; max-width: 90vw;" :bordered="false" size="huge" role="dialog" aria-modal="true" > <div class="herb-card">
<div class="herb-title"> <n-icon size="24" class="icon" > <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" > <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" /> <path d="M11 7h2v5h-2zm0 6h2v2h-2z" /> </svg> </n-icon> <span>每日中药材推荐</span> </div> <div class="herb-content"> <h3>{{ herbCardData.name }}</h3> <p class="herb-describe">{{ herbCardData.describe }}</p> <p class="herb-word">——{{ herbCardData.word }}</p> </div> </div> </n-card> </n-modal> |
2. 前端脚本部分 (Script)
// 状态管理 const showCardModal = ref(false) const herbCardData = ref(null) const isFetching = ref(false) // 显示卡片函数 const showDailyCard = async () => { showDropdown.value = false // 如果数据已经存在,直接显示 if (herbCardData.value) { console.log('已存在卡片数据,直接显示', herbCardData.value) showCardModal.value = true return } // 如果数据不存在且正在获取中,等待获取完成 if (isFetching.value) { loading.value = true // 创建一个轮询检查数据是否已获取 const checkInterval = setInterval(() => { if (herbCardData.value) { clearInterval(checkInterval) loading.value = false showCardModal.value = true } }, 300) return } // 如果数据不存在且没有在获取中,重新获取 loading.value = true try { const response = await getHerbCard() herbCardData.value = { img: response.img || 'https://sys01.lib.hkbu.edu.hk/cmed/mpid/images/D00483.jpg', name: response.name || '宁夏枸杞,中宁枸杞', describe: response.describe || '对免疫功能有影响作用...', word: response.word || '祝您吉祥!', } showCardModal.value = true } catch (error) { console.error('获取每日卡片失败:', error) } finally { loading.value = false } } // 组件挂载时预加载数据 onMounted(async () => { if (!herbCardData.value && !isFetching.value) { isFetching.value = true try { const response = await getHerbCard() herbCardData.value = { img: response.img || 'https://sys01.lib.hkbu.edu.hk/cmed/mpid/images/D00483.jpg', name: response.name || '宁夏枸杞,中宁枸杞', describe: response.describe || '对免疫功能有影响作用...', word: response.word || '祝您吉祥!', } } catch (error) { console.error('预加载每日卡片失败:', error) isFetching.value = false } finally { isFetching.value = false } } }) // 关闭弹窗 const closeModal = () => { showCardModal.value = false } |
3. API 接口
export function getHerbCard(id) { return request({ url: `/card/get`, method: 'GET' }) } |
样式设计
/* 中药卡片样式 */ .herb-title { display: flex; align-items: center; gap: 8px; margin-bottom: 15px; } .herb-card { display: flex; flex-direction: column; gap: 20px; } .herb-image { width: 100%; max-height: 300px; object-fit: contain; border-radius: 8px; } .herb-content { padding: 0 10px; } .herb-content h3 { color: #333; margin-bottom: 15px; text-align: center; } .herb-describe { color: #555; line-height: 1.6; margin-bottom: 15px; } .herb-word { color: #888; font-style: italic; text-align: right; } /* 响应式设计 */ @media (max-width: 768px) { .herb-card { flex-direction: column; } .herb-image { max-height: 200px; } } |
功能逻辑说明
- 数据预加载机制 :
- 组件挂载时(onMounted)自动调用API预加载中药卡片数据
- 使用isFetching标志防止重复请求
- 提供默认数据以防API请求失败
- 智能显示逻辑 :
- 如果数据已存在,直接显示
- 如果数据正在加载,设置轮询检查
- 如果数据不存在且未加载,发起新请求
- 错误处理 :
- 捕获API请求错误并记录到控制台
- 提供优雅降级的默认数据展示
- 用户体验优化 :
- 显示加载状态(loading.value)
- 响应式设计适配不同屏幕尺寸
- 点击外部区域可关闭弹窗(@clickoutside)
使用说明
- 集成方式 :
- 该模块已集成在导航栏组件中
- 通过用户下拉菜单中的"每日卡片"选项触发
- 调用方法 :
// 显示卡片 showDailyCard() // 关闭卡片 closeModal() |
- 数据格式要求 :
interface HerbCardData { img: string; // 中药材图片URL name: string; // 中药材名称 describe: string; // 中药材描述 word: string; // 祝福语或引用 } |
性能优化
- 数据缓存 :
- 使用herbCardData变量缓存API返回数据
- 避免重复请求相同数据
- 按需加载 :
- 仅在用户点击"每日卡片"时显示弹窗
- 预加载数据但不立即渲染
- 资源优化 :
- 图片使用object-fit: contain保持比例
- 设置最大高度防止过大图片影响布局
该模块通过精心设计的数据加载逻辑和响应式UI,为用户提供了流畅的中药材信息浏览体验。
AI问答模块后端代码说明
- 数据库设计
1.对话表(conversations)
2.消息表(messages)
3.收藏表(collections_chat)
4.UML类图:
- model层
- conversation类:与conversation表对应的对象,记录对话的相关信息
- Message类:与messages表对应的对象,记录消息的相关信息
- Collections_chat类:与Collections_chat表对应的对象,记录收藏的相关信息
- repository层
1.ConversationRepository
ConversationRepository 是一个 Spring Data JPA 的仓库接口,用于对数据库中的 Conversation 实体进行操作,提供对话数据的访问和管理能力。
该接口继承自 JpaRepository<Conversation, Long>,提供了以下基础功能:
基本的 CRUD 操作(创建、读取、更新、删除)
基于主键(Long 类型)的操作
分页和排序支持
按用户ID查询最新对话
- 功能:查找指定用户ID的最新对话记录
- 参数:userId - 用户ID
- 返回:Optional包装的最新对话记录
按用户ID查询所有对话(按更新时间降序)
- 功能:获取指定用户的所有对话记录,按更新时间降序排列
- 参数:userId - 用户ID
- 返回:对话记录列表
获取所有对话(包含用户和消息关联数据)
- 功能:获取所有对话记录,并预加载关联的用户和消息数据
- 特点:使用 JOIN FETCH 避免 N+1 查询问题
- 排序:按创建时间降序排列
高级搜索功能
- 功能:支持多条件动态查询对话记录
- 参数:convId:对话ID(精确匹配)
- title:对话标题(模糊匹配)
- userId:用户ID(精确匹配)
- 特点:所有参数均可为null,表示不按该条件过滤
- 排序:按创建时间降序排列
- MessageRepository
接口概述
MessageRepository 是一个 Spring Data JPA 仓库接口,专门用于管理 Message 实体,提供消息数据的存储、查询和删除操作。该接口支持基本的 CRUD 功能,并针对消息系统的特殊需求提供了多种自定义查询方法。
继承功能
继承自 JpaRepository<Message, Long>,提供了:
- 基本的消息增删改查功能
- 基于 Long 类型主键的操作
- 分页和排序支持
核心查询方法
1. 消息查询方法
- List<Message> findByConv_Id(Long convId):根据对话ID查询所有消息
- List<Message> findByParentMsgId(Long parentMsgId):根据父消息ID查询子消息
- List<Message> findByConvOrderByCreatedAtAsc(Conversation conversation):按对话查询消息并按创建时间升序排列
- List<Message> findByConv_IdOrderByCreatedAtAsc(Long convId):按对话ID查询消息并按创建时间升序排列
2. 消息删除方法
- void deleteByConv_Id(Long convId):删除指定对话的所有消息
- @Modifying void deleteLastAssistantMessage(Long convId):删除指定对话中最后一条助手消息(使用原生SQL)
- @Modifying void deleteByConvId(Long convId):使用JPQL删除指定对话的所有消息
3. 高级查询方法
- @Query List<Message> findByConvId(Long convId):使用JPQL查询对话消息,并预加载对话和父消息关联数据
应用场景
该接口主要用于:
- 对话消息的检索和管理
- 消息树形结构的构建(通过父消息ID)
- 对话清空功能
- 消息撤回功能(删除特定消息)
- 消息时间线展示(按时间排序)
接口设计考虑了消息系统的常见需求,提供了高效的查询和灵活的删除操作,特别适合聊天机器人等需要管理大量对话消息的应用场景。
3.CollectionsChatRepository
接口概述
CollectionsChatRepository 是一个 Spring Data JPA 仓库接口,用于管理 CollectionsChat 实体,提供用户收藏对话数据的存储和查询功能。
继承功能
继承自 JpaRepository<CollectionsChat, Long>,支持基础的 CRUD 操作,并基于 Long 类型主键进行数据访问。
自定义查询方法
- 按用户查询收藏记录(按创建时间降序)
- List<CollectionsChat> findByUserOrderByCreatedAtDesc(User user)
- 根据用户对象查询其所有收藏记录,并按创建时间降序排列。
- 检查用户是否已收藏某内容
- boolean existsByUserAndContent(User user, String content)
- 判断指定用户是否已收藏某条内容,避免重复收藏。
- 按收藏ID和用户ID查询
- @Query("SELECT c FROM CollectionsChat c WHERE c.user.userId = :userId AND c.id = :collectionId")
- 根据用户ID和收藏ID精确查询收藏记录,确保数据权限控制。
应用场景
- 主要用于用户收藏对话的管理,包括:查询用户收藏列表
- 防止重复收藏
- 精确查找特定收藏记录
- service层
1.ConversationService
ConversationService.java 是一个 Spring 的 `@Service` 类,主要作用是封装与对话(Conversation]和消息关联Message相关的业务逻辑。以下是其主要功能:
- 提供核心对话操作
- saveConversation(Conversation conversation):保存或更新一个对话。
- getConversationById(Long convId)]:根据 ID 查询对话,若不存在则抛出异常。
- getAllConversations():获取所有对话列表。
- 支持删除操作
- deleteConversation(Long convId):根据 ID 删除指定对话。
- deleteConversationWithMessages(Long id):删除指定 ID 的对话及其关联的所有消息,确保数据一致性。
- 创建新对话
- createNewConversation():创建一个默认配置的新对话对象并保存到数据库中,包含固定用户信息和初始标题。
2.AdminConversationService
AdminConversationService.java 是一个 Spring 的 @Service 类,主要作用是为管理员提供与对话(Conversation)和消息(Message)相关的业务逻辑处理。以下是其核心功能:
- 获取对话数据
getAllConversations():获取所有对话及其关联的用户信息。
getConversationsByUserId(Long userId):根据用户 ID 获取该用户的所有对话。
searchConversations(Long convId, String title, Long userId):支持通过对话 ID、标题或用户 ID 搜索对话。
- 获取消息数据
getMessagesByConversationId(Long convId):根据对话 ID 获取所有相关消息。
3. 删除操作
deleteConversation(Long convId):删除指定 ID 的对话及其关联的消息,确保数据一致性。
deleteMessage(Long msgId):删除指定 ID 的单条消息。
3.MessageService
MessageService.java 是一个 Spring 的 @Service 类,主要作用是封装与消息(Message)相关的业务逻辑,提供统一的操作接口。以下是其核心功能:
- 消息的增删改查
saveMessage(Message message):保存一条新消息或更新已有消息。
getMessageById(Long msgId):根据消息 ID 查询消息对象,若不存在则返回 null。
deleteMessage(Long msgId):删除指定 ID 的消息。
updateMessageContent(Long msgId, String newContent):更新指定消息的内容。
2. 基于对话的消息查询
getMessagesByConversationId(Long convId):根据对话 ID 获取该对话下的所有消息列表。
4.CollectionsChatService
CollectionsChatService.java 是一个 Spring 的 @Service 类,主要作用是封装与用户收藏内容(CollectionsChat)相关的业务逻辑。以下是其核心功能:
- 添加收藏
addCollection(Long userId, CollectionsChatRequest request):根据用户 ID 和请求内容创建一条新的收藏记录,并保存到数据库。
2. 获取收藏数据
getCollectionById(Long userId, Long collectionId):根据用户 ID 和收藏 ID 查询对应的收藏内容。
getUserCollections(Long userId):获取指定用户的全部收藏列表,并按时间倒序返回。
3. 删除收藏
deleteCollection(Long userId, Long collectionId):删除指定用户的一条收藏记录。
(五)DAO层
1.ConversationDTO
ConversationDTO ,用于在系统各层之间(如 Controller、Service、前端)传递与“对话”相关的数据。它不直接参与业务逻辑,而是作为数据容器使用。用于展示用户的历史对话记录列表(含标题、时间、用户信息等)。返回某个对话的元信息,例如对话数量、最后更新时间。在前后端分离架构中作为 JSON 响应体返回给前端组件使用。
主要作用:
封装会话数据:
用于表示一次用户对话的信息,包括:
id:对话唯一标识
userId:所属用户 ID
title:对话标题
username:用户名(展示用)
createdAt 和 updatedAt:创建和更新时间
messageCount:消息数量
提供多种构造方式:
支持无参构造方法,便于 JSON 反序列化。
提供带参数的构造方法,方便从数据库实体 Conversation 转换为 DTO。
解耦领域模型与接口数据结构:
避免将数据库实体类直接暴露给前端或网络接口,增强系统的安全性与灵活性。
允许根据业务需求自由扩展字段(如添加 username),而不影响底层模型。
支持 REST API 数据交互:
常用于返回用户对话列表、单个对话详情等接口数据,适合作为 Spring Boot 控制器中响应体的数据类型。
2.CollectionsChatRequest
ollectionsChatRequest用于封装客户端向服务器发送中医知识问答相关请求时所需的数据。该类主要用于接收用户提交的聊天内容以及相关的用户和会话信息。
主要作用:
封装请求数据:
用于在 Spring MVC 控制器中接收 HTTP 请求体中的字段,如 content(聊天内容)、id(可能表示对话或收藏 ID)、user(用户信息)等。
内容非空校验:
使用 @NotBlank 注解确保 content 字段不能为空,保证必要信息的完整性。
支持 Lombok 的 Getter/Setter:
使用 @Getter 和 @Setter 注解自动生成 getter 和 setter 方法,简化代码结构,提升可维护性。
3.MessageDTO
MessageDTO 是一个 数据传输对象(DTO),用于在系统不同层级之间传递“消息”或“对话记录”的数据。它主要用于中医知识问答模块中,表示用户与系统之间的单条交互内容具体来说:用于展示用户与 AI 助手之间的对话记录。支持多轮对话中消息链的构建(通过 parentMsgId)。在前后端分离架构中作为 JSON 响应体返回给前端组件使用。
主要作用:
封装消息数据:
用于表示一次对话中的单条消息,包含以下关键信息:
role:消息角色(如 user、assistant),标识是谁发送的消息。
content:消息内容,即用户提问或系统回复的具体文本。
createdAt:消息创建时间。
id:消息唯一标识,用于数据操作。
contentType:内容类型(如 text、image 等)。
lang:语言类型(如 zh、en)。
parentMsgId:父级消息 ID,用于构建对话上下文关系。
构造方法支持灵活初始化:
提供带参构造器用于快速创建带有基础字段的消息对象。
提供无参构造器以支持 JSON 反序列化和框架自动注入。
(六)controller层
1.ChatController
ChatController 是一个 Spring Boot 控制器类,用于实现中医知识问答系统的聊天功能。它集成了多轮对话、文件上传识别、翻译、流式响应等能力,是系统中最核心的接口控制器之一。
该控制器主要实现一下功能:
详细接口解释:
- translateText(String text, String from, String to)
作用:调用百度翻译 API 实现文本翻译。
参数说明:
text: 待翻译的文本
from: 原始语言(默认 auto)
to: 目标语言(默认 en)
流程:
构建签名与请求参数
使用 RestTemplate 发送请求
返回翻译结果或错误信息
- getConversations(Long userId, HttpServletRequest request)
作用:获取某个用户的历史对话列表。
逻辑流程:
校验用户是否存在
查询数据库中的所有对话记录
将 Conversation 映射为 ConversationDTO
返回格式:List<ConversationDTO>
3. getMessagesByConversation(Long convId, HttpServletRequest request)
作用:获取某次对话的所有消息记录。
流程:
根据 convId 查询消息列表
将 Message 转换为 MessageDTO
返回格式:List<MessageDTO>
4. updateConversationTitle(Long convId, Map<String, String> request)
作用:更新对话标题
操作逻辑:
查询对话对象
设置新标题和更新时间
保存到数据库
5. chat(String messagesJson, boolean newConversation, MultipartFile file)
作用:处理用户发送的聊天请求,支持文本与文件上传
关键逻辑:
解析前端传来的 JSON 消息
如果有文件上传,读取内容并附加到消息中
向 AI 模型(如 DeepSeek)发起请求
创建或更新会话记录
保存用户消息与 AI 回复
若上传的是图像,使用 Baidu OCR 进行识别
文件处理路径:
存储原始文件与缩略图
保存 Attachment 到数据库
6. streamMessage(String messageContent, Map<String, String> request)
作用:通过 WebSocket 模拟流式响应
使用技术:STOMP + SimpMessagingTemplate
流程:
接收用户输入
分段模拟 AI 流式回答
向 /topic/messages 推送 start, chunk, end 类型事件
7. streamResponse(...)
作用:通过 SseEmitter 实现真正的 Server-Sent Events 流式响应。
特性:
支持文件上传识别
异步调用 AI 模型
流式返回 AI 回答
优势:
用户可逐步看到回答内容
提升交互体验
8. saveUserMessage(...)
作用:根据消息创建或更新会话,并保存用户消息
逻辑:
如果是新会话则新建,否则使用已有会话
自动生成会话标题
保存 Message 记录
9. saveAiMessage(Conversation conversation, String content)
作用:将 AI 的回复保存为 Message 对象
字段填充:
角色:assistant
内容:AI 回复内容
时间戳:当前时间
10. deleteConversation(Long id)
作用:删除一次对话及其相关联的消息
调用服务层:conversationService.deleteConversationWithMessages(id)
事务控制:@Transactional
11. uploadAttachment(MultipartFile file, Long msgId, Map<String, String> request)
作用:上传附件文件(如图片、PDF 等)
处理逻辑:
生成唯一文件名
保存至临时目录
返回文件元数据(URL、大小、类型等)
2.MessageController
MessageController 是一个 Spring Boot 的 @RestController,用于处理与“消息”相关的 HTTP 请求。它是中医知识问答系统中对话模块的重要组成部分,负责管理用户与 AI 助手之间的消息交互。
总体作用
该控制器封装了对 Message 对象的基本 CRUD 操作,并提供 RESTful 接口供前端调用,实现:
消息创建
消息查询(按 ID、按对话)
消息内容更新
消息删除
- createMessage(Message message)
HTTP 方法:POST
路径:/api/messages
参数:
@RequestBody Message message:从请求体中解析 JSON 数据,封装为 Message 对象
作用:将一条新消息保存到数据库。
响应状态码:
201 Created:消息创建成功
- getMessage(Long msgId)
HTTP 方法:GET
路径:/api/messages/{msgId}
参数:
@PathVariable Long msgId:要查询的消息 ID
作用:根据消息 ID 查询单条消息。
响应状态码:
200 OK:消息存在,返回消息对象
404 Not Found:消息不存在
- getMessagesByConversation(Long convId)
HTTP 方法:GET
路径:/api/messages/conversation/{convId}
参数:
@PathVariable Long convId:对话 ID
作用:获取某个对话下的所有消息记录。
返回值:List<Message>
- deleteMessage(Long msgId)
HTTP 方法:DELETE
路径:/api/messages/{msgId}
参数:
@PathVariable Long msgId:要删除的消息 ID
作用:删除指定 ID 的消息。
响应状态码:
204 No Content:删除成功
- updateContent(Long msgId, String newContent)
HTTP 方法:PUT
路径:/api/messages/{msgId}/content
参数:
@PathVariable Long msgId:要更新的消息 ID
@RequestBody String newContent:新的消息内容
作用:更新某条消息的内容。
响应状态码:
200 OK:更新成功,返回更新后的消息对象
404 Not Found:消息不存在
3.AdminConversationController
AdminConversationController 是一个 Spring Boot 控制器类,专为 管理员或后台用户 提供对“对话”和“消息”的管理功能。它与普通用户的 ChatController 不同,侧重于系统级管理、搜索、删除等操作,适用于中医知识问答系统的后台管理系统。
- getAllConversations(HttpServletRequest request)
HTTP 方法:GET
路径:/api/admin/conversations
作用:获取系统中所有的对话记录,并封装为 ConversationDTO 格式返回。
字段映射逻辑:
id: 对话唯一标识
title: 对话标题
createdAt: 创建时间
userId, username: 所属用户信息
messageCount: 消息数量
返回格式:List<ConversationDTO>
- getConversationsByUserId(Long userId, HttpServletRequest request)
HTTP 方法:GET
路径:/api/admin/conversations/user/{userId}
参数:
@PathVariable Long userId:用户 ID
作用:根据用户 ID 查询其所有对话记录。
返回类型:List<Conversation>
适用场景:管理员查看某个用户的历史对话记录。
3. getMessagesByConversationId(Long convId, HttpServletRequest request)
HTTP 方法:GET
路径:/api/admin/conversations/{convId}/messages
参数:
@PathVariable Long convId:对话 ID
作用:获取某次对话下的所有消息记录,并转换为 MessageDTO 格式。
字段映射逻辑:
id: 消息 ID
role: 发送者角色(user / assistant)
content: 消息内容
contentType, lang: 内容类型和语言
parentMsgId: 父级消息 ID(用于构建上下文链)
4. searchConversations(Long convId, String title, Long userId, HttpServletRequest request)
HTTP 方法:GET
路径:/api/admin/conversations/search
参数(可选):
convId: 对话 ID
title: 对话标题
userId: 用户 ID
作用:支持多条件查询对话记录,常用于后台搜索功能。
返回格式:List<Map<String, Object>>,包含:
id: 对话 ID
title: 标题
createdAt: 创建时间
user: 关联用户信息(userId, username)
5. deleteConversation(Long convId)
HTTP 方法:DELETE
路径:/api/admin/conversations/{convId}
参数:
@PathVariable Long convId:要删除的对话 ID
作用:从数据库中彻底删除一条对话记录及其关联消息。
调用服务层:adminConversationService.deleteConversation(convId)
注意:通常建议使用软删除,但此处是硬删除。
6. deleteMessage(Long msgId)
HTTP 方法:DELETE
路径:/api/admin/conversations/messages/{msgId}
参数:
@PathVariable Long msgId:要删除的消息 ID
作用:删除某条具体的对话消息。
调用服务层:adminConversationService.deleteMessage(msgId)
适用场景:审核、纠错、清理垃圾数据等。
4.CollectionsChatController
CollectionsChatController 是一个 Spring Boot 控制器类,用于实现中医知识问答系统中的 收藏功能。它允许用户将特定的对话或消息收藏起来,便于后续查看和管理。
这个控制器是整个问答系统中与“用户收藏”相关的接口核心,提供对收藏记录的增删查等操作,适用于构建用户个人的知识库、历史记录收藏等功能模块。
1. addCollection(CollectionsChatRequest request, Long userId, HttpServletRequest httpRequest)
HTTP 方法:POST
路径:/api/collections
参数:
@RequestBody CollectionsChatRequest request: 包含要收藏的内容信息(如对话 ID、消息 ID 等)
@RequestParam Long userId: 当前用户的 ID
HttpServletRequest httpRequest: 请求对象,用于提取 token 验证身份
作用:将一条新的收藏记录保存到数据库。
权限验证逻辑:
检查请求头中的 Authorization 字段是否为有效的 Bearer Token
调用服务层方法:collectionsChatService.addCollection(userId, request)
返回类型:ApiResponse
2. getCollectionById(Long userId, Long collectionId, HttpServletRequest request)
HTTP 方法:GET
路径:/api/collections/{collectionId}
参数:
@RequestParam Long userId: 用户 ID
@PathVariable Long collectionId: 收藏记录 ID
作用:根据收藏 ID 获取对应的收藏内容详情。
调用服务层方法:collectionsChatService.getCollectionById(userId, collectionId)
适用场景:用户查看具体某条收藏内容
3. deleteCollection(Long userId, Long collectionId)
HTTP 方法:DELETE
路径:/api/collections/{collectionId}
参数:
@RequestParam Long userId: 用户 ID
@PathVariable Long collectionId: 收藏记录 ID
作用:删除用户的某条收藏记录。
参数校验:
检查 userId 和 collectionId 是否为空
异常处理:
如果发生错误,返回包含错误信息的 ApiResponse
调用服务层方法:collectionsChatService.deleteCollection(userId, collectionId)
4. getUserCollections(Long userId, HttpServletRequest request)
HTTP 方法:GET
路径:/api/collections
参数:
@RequestParam Long userId: 用户 ID
作用:获取当前用户所有的收藏记录。
校验逻辑:
检查 userId 是否为空
检查用户是否存在
调用服务层方法:collectionsChatService.getUserCollections(userId)
返回格式:封装后的 ApiResponse,通常包含 List<Collection> 数据
AI问答模块前端代码说明
1. 概述
本AI问答模块是一个基于Vue3和Pinia的前端应用,包含用户AI问答功能、收藏功能、应用界面功能和管理员管理功能。系统采用响应式设计,支持多模态问答方式,提供完善的功能
2. 用户端功能说明
2.1 功能模块
会话管理模块
会话生命周期管理
- 新建会话 :创建全新对话环境,清空当前消息记录,显示欢迎引导语
- 加载会话 :从服务器获取历史会话列表,支持点击加载任意历史对话
- 会话持久化:自动保存会话到后端,确保用户数据不丢失
会话交互功能
- 会话重命名 :允许用户自定义对话标题,便于后续查找
- 会话状态指示 :当前活跃会话会有特殊样式标识(蓝色边框和高亮背景)
2. 聊天交互模块
消息处理核心
- 双向消息流 :完整实现用户消息发送和AI回复展示的闭环
- 消息编辑 :允许用户修改已发送内容,保持对话准确性
- 消息操作 :提供复制、重新生成等实用功能
- 上下文保持 :自动维护对话上下文,确保AI回复连贯性
流式交互技术
- 实时显示 :采用SSE(Server-Sent Events)技术实现打字机效果
- 性能优化 :分块处理流数据,平衡性能与实时性
- 中断控制 :用户可随时停止AI生成过程
2.3 特殊内容处理
- Markdown支持 :完美渲染代码块、表格、列表等复杂格式
- 文件附件 :支持上传图片/文档等附件辅助对话
- 内容安全 :对用户输入和AI输出都进行适当处理,防止XSS攻击
会话清理:提供删除功能,可移除不再需要的对话记录
典籍解读模块后端代码说明
(一)数据库设计
1.书籍表(book)
2.典籍表(classics)
3.收藏表(collected)
4.会话表(qa_sessions)
5.消息表(qa_messages)
6.UML类图
(二)Model层
- Book类:与book表对应的对象,记录书籍相关信息。
- Classic类:与classics表对应的对象,记录典籍(书籍篇目)相关信息。
- Collected类:与collected表对应的对象,记录典籍收藏相关信息。
- QASession类:与qa_session表对应的对象,记录典籍问答对话相关信息。
- QAMessage类:与qa_message表对应的对象,记录典籍问答消息相关信息。
(三)Repository层
1.BookRepository
提供对 Book 数据表的访问能力,并支持动态查询和分页功能
接口用途:BookRepository 是一个 Spring Data JPA 的仓库接口,用于对数据库中的 Book 实体进行操作。
继承功能:它继承自 JpaRepository<Book, Integer>,提供了基本的 CRUD 操作(如保存、删除、查询等)以及基于主键(这里是 Integer 类型)的操作。
自定义查询:通过 @Query 注解定义了一个名为 searchBooks 的方法,支持根据书名、作者或朝代进行模糊搜索,并使用分页功能(由 Pageable 控制)。
参数说明:
name:书籍名称的模糊匹配。
author:作者的模糊匹配。
dynasty:朝代的模糊匹配。
这些参数均可以为 null,表示不按该条件过滤。
分页支持:使用 Page<Book> 返回结果,支持分页查询,便于前端展示时实现分页功能。
- ClassicRepository
-接口用途:用于对数据库中的classic实体进行操作。
- 继承功能:它继承自 `JpaRepository<Classic, Long>`,提供了基本的 CRUD 操作(如保存、删除、查询等)以及基于主键(这里是 `Long` 类型)的操作。
- 支持的查询功能:分页获取所有经典条目。根据书籍 ID 查询相关经典内容,并支持分页。
- CollectedRepository
接口用途:用于对数据库中的 Collected 实体进行操作,表示用户的收藏记录。
继承功能:它继承自 JpaRepository<Collected, Long>,提供了基本的 CRUD 操作(如保存、删除、查询等)以及基于主键(这里是 Long 类型)的操作。
定义的方法说明:
findByUserAndClassic:根据用户和经典条目查找是否存在对应的收藏记录。
deleteByUserAndClassic:删除指定用户和经典条目的收藏记录。
findByUser:根据用户获取所有该用户的收藏记录。
findCollections:通过自定义 JPQL 查询,支持根据用户 ID 和经典条目 ID 进行过滤查询,并支持分页功能。
userId 和 classicId 可为 null,表示不按该条件过滤。
- QaSessionRepository
主要负责提供对 QaSession 数据表的访问能力,支持基于用户和经典条目的问答记录查询,并支持后台管理的分页查询功能。
接口用途:用于对数据库中的 QaSession 实体进行操作,表示问答会话记录。
继承功能:它继承自 JpaRepository<QaSession, Long>,提供了基本的 CRUD 操作(如保存、删除、查询等)以及基于主键(这里是 Long 类型)的操作。
定义的方法说明:
findByUserIdAndClassicId:根据用户 ID 和经典条目 ID 查询相关的问答会话记录。
findByClassicId:根据经典条目 ID 查询所有与之相关的问答会话记录。
findAdminSessions:通过自定义 JPQL 查询,支持根据用户 ID 和经典条目 ID 进行过滤查询,并支持分页功能。
userId 和 classicId 可为 null,表示不按该条件过滤。
- QaMessageRepository
主要负责提供对 QaMessage 数据表的访问能力,支持根据问答会话 ID 查询消息记录
接口用途:用于对数据库中的 QaMessage 实体进行操作,表示问答会话中的消息记录。
继承功能:它继承自 JpaRepository<QaMessage, Long>,提供了基本的 CRUD 操作(如保存、删除、查询等)以及基于主键(这里是 Long 类型)的操作。
findBySession_Id:根据问答会话 ID 查询该会话下的所有消息记录。
- Service层
- BookService
主要负责处理书籍相关的业务逻辑,包括查询(支持分页和搜索)、新增、更新和删除操作,是控制器层与数据访问层之间的中间桥梁。
类用途:封装与书籍(Book)相关的业务逻辑,调用 BookRepository 进行数据持久化操作。
依赖注入:使用 Lombok 的 @RequiredArgsConstructor 注解自动注入 BookRepository,实现无需手动编写构造函数。
方法说明:
getPaginatedBooks:分页获取书籍列表,支持根据书名、作者、朝代进行模糊搜索。若所有条件为空,则返回全部书籍。
getBookById:根据 ID 查询单个书籍对象。
saveBook:保存或更新书籍记录。
deleteBook:根据 ID 删除书籍记录。
- ClassicService
类用途:封装与经典条目(Classic)相关的业务逻辑,调用 ClassicRepository 进行数据持久化操作。
依赖注入:
使用了 @Autowired 注解手动注入 ClassicRepository。
同时使用了 Lombok 的 @RequiredArgsConstructor,表明如果存在其他 final 字段也会自动注入。
方法说明:
save:保存或更新一个经典条目。
delete:根据 ID 删除一个经典条目。
getClassicById:根据 ID 查询单个经典条目,若不存在则返回 null。
getPaginatedClassicsByBook:分页查询某本书籍下的所有经典条目。
- CollectedService
类用途:封装与用户收藏记录(Collected)相关的业务逻辑,调用 CollectedRepository 进行数据持久化操作。
依赖注入:
手动通过构造函数注入了以下组件:
CollectedRepository
UserService
ClassicService
UserRepository
方法说明:
isCollected:判断某个用户是否已收藏指定的经典条目。
toggleCollected:
切换用户的收藏状态:如果已收藏则删除收藏记录,否则创建新的收藏记录。
使用 @Transactional 注解确保数据库操作事务性。
getUserCollections:获取指定用户的所有收藏记录。
- AdminCollectedServicce
类用途:提供后台管理功能中关于用户收藏记录(Collected)的查询服务。
依赖注入:通过 Lombok 的 @RequiredArgsConstructor 注解自动注入以下依赖:
CollectedRepository
UserRepository
方法说明:
getAllCollections:
支持根据用户 ID 和经典条目 ID 查询收藏记录,并进行分页展示。
查询结果按 id 降序排序,确保最新的记录优先显示。
将实体类 Collected 转换为数据传输对象 AdminCollectedDTO,用于脱敏和结构化输出。
- AIService
主要负责对接外部 AI 大模型接口,提供同步问答、回答增强及流式输出三种功能,支撑系统中的智能问答模块.
类用途:封装与 AI 问答接口交互的业务逻辑,用于调用外部大模型 API(如 DeepSeek-R1)并处理响应内容。
功能模块:
基础同步调用:
getAIResponse:向指定的 AI 接口发送请求并获取完整回答内容。
使用了自定义超时设置的 RestTemplate 发送 POST 请求。
设置了请求头(Content-Type、Authorization)和请求体(prompt 内容)。
增强回答功能:
getEnhancedResponse:基于已有问答记录(QaMessage)生成优化后的 AI 回答。
流式输出支持:
StreamCallback:定义流式输出的回调接口,包含开始、接收内容、完成和出错等事件。
streamAIResponse:启动异步线程模拟流式输出,逐字返回 AI 回答结果,适用于前端实时展示。
- QAService
主要负责处理用户与 AI 之间的问答流程控制,包括会话创建、消息存储、AI 回答生成、用户反馈记录以及支持流式输出的 SSE 接口
类用途:封装与问答会话(QaSession)和问答消息(QaMessage)相关的业务逻辑。
主要功能模块:
会话管理:
createNewSession:创建新的问答会话,并基于用户提问内容生成简洁标题。
renameSession:修改已有会话的标题。
deleteSession:删除指定 ID 的会话。
getUserSessions:获取某用户在特定典籍下的所有问答会话。
消息管理:
addMessageToSession:向指定会话中添加一条消息,并自动触发 AI 回复。
saveMessage:保存消息到数据库,支持设置父子消息关系。
regenerateResponse:重新生成 AI 对某条用户消息的回答。
updateMessage:更新用户的提问内容并重新生成 AI 回答。
deleteMessage:删除指定 ID 的消息及其后续对话链。
saveFeedback:记录用户对 AI 回答的反馈(好评、差评或中性)。
流式交互支持:
streamResponse:基于 SSE 技术实现流式回答输出,适用于前端实时展示。保存用户消息。构造提示词并调用 AI 流式接口。通过 SSE 发送逐字输出给前端。
streamRegenerateResponse:流式重新生成 AI 回答。删除后续对话。重置原 AI 消息内容。调用 AI 流式接口生成新回答。
streamEditResponse:流式编辑用户提问并生成新回复。修改用户提问内容。清除后续对话。调用 AI 流式接口生成新回答。
辅助方法
1. extractThinkContent(String input)
作用:提取 AI 返回中的思考部分后的内容。
逻辑说明:
查找 think 或 HTML 标签后的内容。
清洗处理后返回。
2. generateFallbackTitle(String input)
作用:当 AI 无法生成标题时,使用此方法构造默认标题。
逻辑说明:
提取前几个中文字符作为标题。
3. createMessageShell(QaSession session, String role, Long parentId)
作用:创建空内容的消息壳,用于流式输出前占位。
逻辑说明:
创建空白内容的 QaMessage 实体。
设置角色、父消息等信息。
立即保存以便前端引用。
- AdminQAService
类用途
提供后台管理系统对用户问答记录(QaSession 和 QaMessage)的查询与管理功能。通过封装数据转换逻辑和调用仓库层方法,为控制器提供结构清晰的数据接口。
依赖注入
使用 @RequiredArgsConstructor 注解自动注入以下组件:
qaSessionRepository:用于操作问答会话数据。
userRepository:用于获取用户信息,补充到返回的 DTO 中。
qaMessageRepository:用于查询或删除具体的问答消息。
核心方法说明
getAllSessions(Long userId, Long classicId, int page, int size)
作用:分页查询所有问答会话,并支持按用户 ID 和典籍 ID 过滤。
逻辑说明:
调用 qaSessionRepository.findAdminSessions 查询符合条件的会话。
使用 PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")) 实现分页并按创建时间降序排列。
将每个 QaSession 转换为 AdminQaSessionDTO 返回给前端。
包含用户名称(若用户不存在则显示“已删除用户”)。
典籍标题、会话标题、创建时间等信息。
调用场景:管理员查看用户提问记录列表。
2. deleteSession(Long id)
作用:删除指定 ID 的问答会话。
逻辑说明:
直接调用 JPA 的 deleteById() 方法删除会话。
3. getMessagesBySessionId(Long sessionId)
作用:根据会话 ID 获取所有相关的问答消息。
逻辑说明:
调用 qaMessageRepository.findBySession_Id 查询所有消息。
调用场景:管理员查看详情时展示具体对话内容。
4. deleteMessage(Long messageId)
作用:删除指定 ID 的问答消息。
逻辑说明:
调用 JPA 的 deleteById() 方法删除单条消息。
调用场景:管理员清理违规或错误内容。
- DAO层
1.AdminCollectedDTO
是一个 DTO 类,用于向后台管理页面传递 用户收藏记录 的结构化数据,包含:
收藏 ID
用户信息(ID + 昵称)
典籍信息(ID + 标题)
用户设置的收藏标题
作用:
在后台管理界面中展示用户收藏列表时使用。
用于封装从数据库查询出的用户收藏信息,并返回给前端。
避免将完整的实体类暴露给前端,起到 脱敏 和 结构化输出 的作用。
2.AdminQASessionDTO
类用途
用于在 后台管理系统 中展示用户的 问答会话记录。
封装与问答会话相关的 基础信息字段,便于前端展示和接口调用
3.ClassicSimpleDTO
用于在接口中返回 经典条目(Classic) 的 基础信息。
是一个简化版的 DTO,仅包含 ID 和标题字段,适用于列表展示或关联引用场景。
- CollectedDTO
用于在接口中返回用户的 收藏记录信息。
封装与用户收藏相关的部分字段,便于前端展示和接口调用。
- PageDTO
用于封装 分页查询结果,统一返回给前端的数据结构。
支持任意类型的分页内容转换,适用于所有需要分页展示的接口。
核心方法说明:
public static <T, E> PageDTO<T> fromPage(Page<E> page, Function<E, T> converter)
作用:将 Spring Data 的分页结果 Page 转换为自定义的 PageDTO 对象。
逻辑说明:
使用传入的 Page 数据构造新的 DTO。
将每条实体数据通过 converter 函数转换为目标 DTO 类型(如 ClassicSimpleDTO::fromEntity)。
设置分页相关属性(当前页、大小、总数等)。
(六)Controller层
- BookController
用于处理与书籍(Book)相关的 HTTP 请求。它通过注入 BookService 和 ClassicService 来实现业务逻辑,并提供 RESTful API 接口供前端或其他服务调用。
RESTful API 管理:
使用 @RestController 注解表明该类是一个控制器,并且所有返回值直接写入 HTTP 响应体中。
所有接口路径以 /api/books 为前缀。
跨域支持:
使用 @CrossOrigin(origins = "*") 允许来自任何域名的跨域请求,便于前后端分离开发。
书籍数据管理接口:
@GetMapping:获取分页书籍列表,支持按名称、作者和朝代过滤。
@GetMapping("/{id}"):根据 ID 获取单个书籍信息。
@PostMapping:创建新书籍。
@PutMapping("/{id}"):更新指定 ID 的书籍信息。
@DeleteMapping("/{id}"):删除指定 ID 的书籍。
典籍(Classic)管理接口:
@GetMapping("/{bookId}/classics"):获取某本书籍下的典籍分页列表。
@PostMapping("/{bookId}/classics"):为某本书籍新增典籍。
@PutMapping("/{bookId}/classics/{classicId}):更新某本书籍下的某个典籍。
@DeleteMapping("/{bookId}/classics/{classicId}):删除某本书籍下的某个典籍。
服务依赖注入:
通过 @Autowired 注入 BookService 和 ClassicService,用于调用业务逻辑层方法。
- ClassicController
用于处理与典籍(Classic)相关的 HTTP 请求。它通过注入 ClassicService 来实现业务逻辑,并提供 RESTful API 接口供前端或其他服务调用。
RESTful API 管理:
使用 @RestController 注解表明该类是一个控制器,并且所有返回值直接写入 HTTP 响应体中。
所有接口路径以 /api/classics 为前缀。
跨域支持:
使用 @CrossOrigin(origins = "*") 允许来自任何域名的跨域请求,便于前后端分离开发。
服务依赖注入:
通过 @Autowired 注入 ClassicService,用于调用业务逻辑层方法。
典籍数据管理接口:
@GetMapping:根据书籍 ID 获取分页典籍列表,参数包含书籍 ID、页码和每页数量。
@GetMapping("/{id}"):根据 ID 获取单个典籍信息。
@PostMapping:创建新的典籍。
@PutMapping("/{id}"):更新指定 ID 的典籍信息。
@DeleteMapping("/{id}"):删除指定 ID 的典籍。
- CollectedController
RESTful API 管理
使用 @RestController 注解表明该类是一个控制器,所有返回值直接写入 HTTP 响应体中。
所有接口路径以 /api/collected 为前缀。
跨域支持
使用 @CrossOrigin(origins = "*") 允许来自任何域名的跨域请求,便于前后端分离开发。
依赖注入
采用构造函数注入方式引入以下服务:
[CollectedService]
[UserService]
[ClassicService]
核心功能接口:
查询收藏状态,根据用户 ID 和典籍 ID 查询该用户是否已收藏此典籍。
调用 [CollectedService.isCollected(user, classic)] 判断是否存在对应的收藏记录。
切换收藏状态,实现收藏与取消收藏的切换操作。
调用 [CollectedService.toggleCollected(userId, classicId, title)] 进行状态切换。
获取用户收藏列,获取指定用户的全部收藏记录。
将 [Collected] 对象转换为 [CollectedDTO] 数据传输对象,包含:
收藏 ID、标题;关联的典籍 ID;典籍所属书籍 ID 及名称;用户 ID
- AdminCollectedController
RESTful API 管理
使用 @RestController 注解表明该类是一个控制器,所有返回值直接写入 HTTP 响应体中。
所有接口路径以 /api/admin/collected 为前缀,表明这是管理员专用接口。
跨域支持
使用 @CrossOrigin(origins = "*") 允许来自任何域名的跨域请求,便于前后端分离开发。
依赖注入
使用 Lombok 的 @RequiredArgsConstructor 注解自动创建构造函数,注入不可变的 [AdminCollectedService] 实例。
核心功能接口
分页查询收藏记录
提供管理员视角下的收藏记录分页查询功能。
支持根据以下参数进行过滤:
userId: 用户 ID(可选)
classicId: 典籍 ID(可选)
page: 当前页码,默认为 0
size: 每页数量,默认为 10
调用 [AdminCollectedService.getAllCollections(userId, classicId, page, size)] 获取数据。
返回类型为 Page<AdminCollectedDTO>,适配前端分页展示需求。
- QAController
RESTful API 管理
使用 @RestController 注解表明该类是一个控制器,所有返回值直接写入 HTTP 响应体中。
所有接口路径以 /api/qa 为前缀。
跨域支持
使用 @CrossOrigin(origins = "*") 允许来自任何域名的跨域请求,便于前后端分离开发。
依赖注入
使用 Lombok 的 @RequiredArgsConstructor 自动注入不可变的 [QaService] 实例。
核心功能接口:
会话管理
创建会话,创建一个新的问答会话,并可选地添加第一条问题。调用 [QaService.createNewSession(userId, classicId, firstQuestion)]
获取用户会话列表,获取指定用户在某典籍下的所有问答会话。调用 [QaService.getUserSessions(userId, classicId)]
删除会话,删除指定 ID 的问答会话。调用 [QaService.deleteSession(sessionId)]
重命名会话,修改会话标题。调用 [QaService.renameSession(sessionId, newTitle)]
消息管理
发送消息,向指定会话中添加一条消息。
调用 [QaService.addMessageToSession(sessionId, content, role, parentId)]
获取会话中的消息列表,获取指定会话的所有消息。
调用 [QaService.getMessagesBySessionId(sessionId)]
编辑消息内容,修改某条消息的内容。
调用 [QaService.updateMessage(messageId, newContent)]
删除消息,删除指定 ID 的消息。
调用 [QaService.deleteMessage(messageId)]
流式交互支持
流式提问,支持前端实时接收 AI 回答的流式输出。
调用 [QaService.streamResponse(...)] 实现 SSE 流式通信。
流式重新生成回复,流式重新生成某条消息的回复。
调用 [QaService.streamRegenerateResponse(messageId, emitter)]
流式编辑回复,对已有消息进行修改后进行流式回答。
调用 [QaService.streamEditResponse(messageId, newContent, emitter)]
- AdminQAController
用于管理问答会话(QaSession)和消息(QaMessage)的后台操作。它通过注入 [AdminQaService] 来实现业务逻辑,并提供分页查询、删除等接口供管理员使用。
RESTful API 管理
使用 @RestController 注解表明该类是一个控制器,所有返回值直接写入 HTTP 响应体中。
所有接口路径以 /api/admin/qa-sessions 为前缀,表明这是管理员专用接口。
跨域支持
使用 @CrossOrigin(origins = "*") 允许来自任何域名的跨域请求,便于前后端分离开发。
依赖注入
使用 Lombok 的 @RequiredArgsConstructor 自动创建构造函数,注入不可变的 [AdminQaService] 实例。
核心功能接口:
获取所有问答会话(分页),分页获取所有问答会话记录,支持按用户 ID 和典籍 ID 过滤。返回类型为 Page<AdminQaSessionDTO>,适配后台展示需求。
调用 [AdminQaService.getAllSessions(userId, classicId, page, size)]
删除指定问答会话,删除指定 ID 的问答会话。调用 [AdminQaService.deleteSession(id)],返回 204 No Content 表示删除成功。
获取会话中的消息列表,获取指定会话下的所有问答消息。
调用 [AdminQaService.getMessagesBySessionId(sessionId)]
删除指定消息,删除指定 ID 的问答消息。
调用 [AdminQaService.deleteMessage(messageId)],返回 204 No Content 表示删除成功。
典籍解读模块前端代码说明
个人信息模块后端代码说明
(一) 数据库设计
- 用户表 (`user`)
- `user_id` (主键, INT)
- `username` (用户名,VARCHAR)
- `email` (用户邮箱, VARCHAR)
- `password` (密码, VARCHAR)
- `avatar` (头像存储路径, VARCHAR)
- `created_at` (创建时间, DATETIME)
- uml类图
(二)Model层
- user类:与用户表对应的对象,记录用户相关信息。
(三)UserService类(用来编写与用户信息相关的操作业务)
1.注册
判断两次密码是否一致,用户名是否存在,邮箱是否注册,如满足,成功注册,否则返回错误信息,
2. 登录
邮箱密码判断正确后,返回个人信息及token,否则返回错误信息
3.邮件重置密码
3.1 随机密码生成
生成8-12位的包含英文大小写、数字的随机密码
3.2 邮件发送
使用JavaMailSender发送邮件
4. 获取和修改个人用户信息
4.1 获取个人用户信息
根据token得到userid,传给前端个人信息
4.2 修改个人信息
根据token得到userid,判断用户名唯一后更新数据库
- 上传头像
5.1 上传准备
获取文件存储目录 uploadDir,检查用户是否已有头像(即 user.getAvatar() 不为空)
如果有旧头像,则构造出旧头像在服务器上的文件路径,并检查路径是否合法,删除旧头像文件。如果删除失败,返回错误信息。
5.2 上传新头像
从上传的文件中提取文件扩展名,并基于用户ID和时间戳生成新的唯一文件名
构造目标文件路径 targetLocation,并将上传的文件内容写入到该路径。
构建新的头像访问路径 avatarPath
将新路径设置到用户对象的 avatar 字段中,并保存到数据库。
5.3 返回
成功时返回包含新头像路径的成功消息,否则返回错误信息
- 修改密码
从token获取userid,判断旧密码是否正确,再判断新密码是否合规,符合则修改数据库,否则返回错误信息
- UserController 类
将不同的操作映射到对应的 API 接口,并通过调用 UserService 来完成具体逻辑处理
- UserRepository类
提供对 User 实体进行数据库操作的方法和自定义查询方法
个人信息模块前端代码说明
- 概述
本模块包含用户登录、注册、个人主页环节,以及token、401拦截的内容,为用户提供良好的软件体验
- 功能说明
- API接口说明
API包括用户注册、用户登录、通过邮箱重置密码、获取用户个人资料、更新用户个人资料、上传用户头像、修改用户密码。
所有接口都通过 `@/utils/http` 模块中的 request 函数进行调用,统一处理请求和响应。
import request from '@/utils/http' export const registerAPI = (registerRequest) => { return request({ url: '/user/register', method: 'POST', data: registerRequest, }) } export const loginAPI = (loginRequest) => { return request({ url: '/user/login', method: 'POST', data: loginRequest, }) } export const emailResetPasswordAPI = (resetPasswordRequest) => { return request({ url: '/user/email-reset-password', method: 'POST', data: resetPasswordRequest, }) } export const getProfileAPI = () => { return request({ url: '/user/profile', method: 'GET', }) } export const updateProfileAPI = (data) => { return request({ url: '/user/update-profile', method: 'POST', data, }) } export const uploadAvatarAPI = (formData) => { return request({ url: '/user/upload-avatar', method: 'POST', headers: { 'Content-Type': 'multipart/form-data', }, data: formData, }) } export const changePasswordAPI = (data) => { return request({ url: '/user/change-password', method: 'POST', data, }) } |
- 样式设计
- 总结
拦截器
(一)TokenInterceptor
- 定义无需认证的白名单路径
- 放行所有OPTIONS预检请求
- 放行白名单路径
- 如果请求不在白名单内,从请求头中获取 Token 并进行验证
- webConfig
注入拦截器
趣味问答模块后端代码说明
1. 概述
本文档详细说明趣味问答模块的后端实现,该模块主要功能包括:
- 题目分类管理
- 题库管理
- 答题记录管理
- 排行榜功能
- 成就系统
系统采用标准的Spring Boot分层架构,分为Model、Repository、Service和Controller四层。
2. Model层
2.1 核心实体类
2.1.1 题目分类(QuestionClass)
@Entity @Table(name = "question_class") public class QuestionClass { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; // 分类ID
@Column private String className; // 分类名称 } |
2.1.2 题库(QuestionBank)
@Entity @Table(name = "question_bank") public class QuestionBank { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; // 题目ID
@Column private Integer classId; // 所属分类ID
@Column(columnDefinition = "TEXT") private String question; // 题目内容
@Column(name = "correct_answer", length = 255) private String correctAnswer; // 正确答案
@Column(length = 255) private String options; // 选项(JSON格式)
@Temporal(TemporalType.TIMESTAMP) private Date createdTime; // 创建时间
@Column(columnDefinition = "TEXT") private String explanation; // 题目解析 } |
2.1.3 答题记录(AnswerRecord)
@Entity @Table(name = "answer_record") public class AnswerRecord { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; // 记录ID
@Column(name = "user_id") private Integer userId; // 用户ID
@Column private Integer classId; // 题目分类ID
@Column private Integer number; // 题目数量
@Column(name = "correct") private String correct; // 正确情况(01串表示)
@Enumerated(EnumType.STRING) @Column(columnDefinition = "ENUM('PRACTICE','RANK')") private PlayMode playMode; // 游戏模式
@Column private String questions; // 题目ID列表(逗号分隔)
@Column private String answers; // 用户答案
@Column private Integer activating; // 是否激活(1:进行中,0:已完成)
@Column(columnDefinition = "datetime(0)") private LocalDateTime time; // 答题时间 } |
2.1.4 排行榜记录(RankRecord)
@Entity @Table(name = "ranking_record") public class RankRecord { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; // 记录ID
@Column(name = "user_id") private Integer userId; // 用户ID
@Column private Integer recordId; // 关联的答题记录ID
@Column private Integer number; // 题目数量
@Column private double score; // 得分
@Column private LocalDateTime time; // 记录时间 } |
2.1.5 成就系统(Achievement)
@Entity @Table(name = "achievement") public class Achievement { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; // 成就ID
@Column(length = 255) private String name; // 成就名称
@Column(length = 255) private String conditions; // 达成条件
@Column(name = "icon_url", length = 255) private String iconUrl; // 图标URL } |
2.1.6 用户成就(UserAchievement)
@Entity @Table(name = "user_achievement") public class UserAchievement { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; // 记录ID
@Column(name = "user_id") private Integer userId; // 用户ID
@Column(name = "achievement_id") private Integer achievementId; // 成就ID
@Column private LocalDateTime achieveTime; // 达成时间 } |
3. Repository层
3.1 题目分类仓库(QuestionClassRepository)
@Repository public interface QuestionClassRepository extends JpaRepository<QuestionClass, Integer> { Optional<QuestionClass> findById(Integer id); List<QuestionClass> findAll(); } |
3.2 题库仓库(QuestionBankRepository)
@Repository public interface QuestionBankRepository extends JpaRepository<QuestionBank, Integer> { Optional<QuestionBank> findById(Integer id); List<QuestionBank> findByClassId(Integer classId);
// 随机获取指定分类的题目(不包含答案) @Query("SELECT NEW com.example.qihuangserver.dto.question.QuestionBankDTO(q.id, q.question, q.options) " + "FROM QuestionBank q WHERE q.classId = :classId ORDER BY RAND() LIMIT :limit") List<QuestionBankDTO> findRandomByClassId(Integer classId, Integer limit);
// 批量查询题目(不包含答案) @Query("SELECT NEW com.example.qihuangserver.dto.question.QuestionBankDTO(q.id, q.question, q.options) " + "FROM QuestionBank q WHERE q.id IN :ids") List<QuestionBankDTO> findByIds(List<Integer> ids); } |
3.3 答题记录仓库(AnswerRecordRepository)
@Repository public interface AnswerRecordRepository extends JpaRepository<AnswerRecord, Integer> { Optional<AnswerRecord> findById(Integer id); List<AnswerRecord> findByUserId(Integer userId);
// 查询用户的激活/历史记录 @Query(value = "SELECT * FROM answer_record WHERE user_id = ?1 AND activating = ?2", nativeQuery = true) List<AnswerRecord> findByUserIdAndIsActivate(Integer userId, Integer isActivate); } |
3.4 排行榜仓库(RankRecordRepository)
@Repository public interface RankRecordRepository extends JpaRepository<RankRecord, Integer> { Optional<RankRecord> findById(Integer id); } |
3.5 成就仓库(AchievementRepository)
@Repository public interface AchievementRepository extends JpaRepository<Achievement, Integer> { Optional<Achievement> findById(Integer id); } |
3.6 用户成就仓库(UserAchievementRepository)
@Repository public interface UserAchievementRepository extends JpaRepository<UserAchievement, Integer> { Optional<UserAchievement> findById(Integer id); } |
4. Service层
4.1 题目分类服务(QuestionClassService)
@Service public class QuestionClassService { @Autowired private QuestionClassRepository repository;
public Optional<QuestionClass> findById(Integer id) { return repository.findById(id); }
public List<QuestionClass> findAll() { return repository.findAll(); }
public QuestionClass save(QuestionClass questionClass) { return repository.save(questionClass); }
public void deleteById(Integer id) { repository.deleteById(id); } } |
4.2 题库服务(QuestionBankService)
@Service public class QuestionBankService { @Autowired private QuestionBankRepository repository;
public Optional<QuestionBank> findById(Integer id) { return repository.findById(id); }
public List<QuestionBank> findByClassId(Integer classId) { return repository.findByClassId(classId); }
public List<QuestionBankDTO> findRandomByClassId(Integer classId, Integer limit) { return repository.findRandomByClassId(classId, limit); }
public List<QuestionBankDTO> getQuestionByQuestionStr(String questionStr) { // 解析题目ID字符串并批量查询 String[] questionIds = questionStr.split(","); List<Integer> idList = Arrays.stream(questionIds) .map(Integer::parseInt) .collect(Collectors.toList()); return repository.findByIds(idList); } } |
4.3 答题记录服务(AnswerRecordService)
@Service public class AnswerRecordService { @Autowired private AnswerRecordRepository answerRecordRepository; @Autowired private QuestionBankRepository questionBankRepository;
// 开始答题记录 public AnswerRecord startAnswerRecord(Integer userId, Integer classId, Integer limit, PlayMode playMode, String questionStr, String answerStr) { AnswerRecord record = AnswerRecord.builder() .userId(userId) .classId(classId) .number(limit) .playMode(playMode) .questions(questionStr) .answers(answerStr) .activating(1) .time(LocalDateTime.now()) .build(); return answerRecordRepository.save(record); }
// 完成答题记录并批改 public AnswerRecord finishAnswerRecord(DataRequest dataRequest) { Integer id = dataRequest.getInteger("answerRecordId"); String answers = dataRequest.getString("answers");
AnswerRecord record = answerRecordRepository.findById(id) .orElseThrow(() -> new RuntimeException("未找到答题记录"));
// 批改逻辑 String[] questionIds = record.getQuestions().split(","); StringBuilder correctBuilder = new StringBuilder();
for (int i = 0; i < questionIds.length; i++) { Integer qid = Integer.parseInt(questionIds[i].trim()); String rightAnswer = questionBankRepository.findById(qid) .map(QuestionBank::getCorrectAnswer) .orElse("");
char userAnswer = (answers != null && i < answers.length()) ? answers.charAt(i) : ' '; correctBuilder.append(String.valueOf(userAnswer).equalsIgnoreCase(rightAnswer) ? '1' : '0'); }
record.setActivating(0); record.setAnswers(answers); record.setCorrect(correctBuilder.toString()); return answerRecordRepository.save(record); }
// 查询用户的激活/历史记录 public List<AnswerRecord> findActivateRecordByUserId(Integer userId) { return answerRecordRepository.findByUserIdAndIsActivate(userId, 1); }
public List<AnswerRecord> findHistoryRecordByUserId(Integer userId) { return answerRecordRepository.findByUserIdAndIsActivate(userId, 0); } } |
4.4 排行榜服务(RankRecordService)
@Service public class rankRecordService { @Autowired private RankRecordRepository repository;
public Optional<RankRecord> findById(Integer id) { return repository.findById(id); }
public RankRecord save(RankRecord rankRecord) { return repository.save(rankRecord); }
public void deleteById(Integer id) { repository.deleteById(id); } } |
4.5 成就服务(AchievementService)
@Service public class achievementService { @Autowired private AchievementRepository repository;
public Optional<Achievement> findById(Integer id) { return repository.findById(id); }
public Achievement save(Achievement achievement) { return repository.save(achievement); }
public void deleteById(Integer id) { repository.deleteById(id); } } |
4.6 用户成就服务(UserAchievementService)
@Service public class userAchievementService { @Autowired private UserAchievementRepository repository;
public Optional<UserAchievement> findById(Integer id) { return repository.findById(id); }
public UserAchievement save(UserAchievement userAchievement) { return repository.save(userAchievement); }
public void deleteById(Integer id) { repository.deleteById(id); } } |
5. Controller层
5.1 题库控制器(QuestionBankController)
@RestController @RequestMapping("/api/questionBank") public class QuesionBankController { @Resource private QuestionBankService questionBankService; @Resource private QuestionClassService questionClassService;
// 获取所有题目分类 @PostMapping("findAllQuestionClasses") public ResponseEntity<List<QuestionClass>> findAllQuestionClasses() { return ResponseEntity.ok(questionClassService.findAll()); }
// 根据分类ID查询题目 @PostMapping("findQuestionByClassId") public ResponseEntity<List<QuestionBank>> findQuestionByClassId(@RequestBody DataRequest dataRequest) { Integer classId = dataRequest.getInteger("classId"); return ResponseEntity.ok(questionBankService.findByClassId(classId)); }
// 随机获取指定分类的题目 @PostMapping("findRandomQuestionByClassId") public ResponseEntity<List<QuestionBankDTO>> findRandomQuestionByClassId(@RequestBody DataRequest dataRequest) { Integer classId = dataRequest.getInteger("classId"); Integer limit = dataRequest.getInteger("limit"); return ResponseEntity.ok(questionBankService.findRandomByClassId(classId, limit)); } } |
5.2 答题记录控制器(AnswerRecordController)
@RestController @RequestMapping("/api/answerRecord") public class AnswerRecordController { @Resource private AnswerRecordService answerRecordService; @Resource private QuestionBankService questionBankService; @Resource private UserRepository userRepository;
// 开始答题 @PostMapping("startRecord") public ResponseEntity<AnswerRecordDTO> startRecord(@RequestBody DataRequest dataRequest, @RequestHeader("Authorization") String authHeader) { // 验证token并获取用户ID long userId = JWTUtils.getUserIdFromJwtToken(authHeader.replace("Bearer ", ""));
Integer classId = dataRequest.getInteger("classId"); Integer limit = dataRequest.getInteger("limit"); PlayMode playMode = PlayMode.valueOf(dataRequest.getString("playMode"));
// 获取随机题目 List<QuestionBankDTO> questions = questionBankService.findRandomByClassId(classId, limit); String questionStr = questions.stream() .map(QuestionBankDTO::getId) .map(String::valueOf) .collect(Collectors.joining(","));
// 创建答题记录 AnswerRecord record = answerRecordService.startAnswerRecord( (int)userId, classId, limit, playMode, questionStr, "*".repeat(limit));
// 构建返回DTO AnswerRecordDTO dto = AnswerRecordDTO.builder() .id(record.getId()) .userId((int)userId) .classId(classId) .number(limit) .playMode(playMode) .questions(questions) .answers(record.getAnswers()) .time(record.getTime()) .build();
return ResponseEntity.ok(dto); }
// 初始化答题记录 @PostMapping("initRecord") public ResponseEntity<List<AnswerRecordDTO>> initRecord(@RequestHeader("Authorization") String authHeader) { long userId = JWTUtils.getUserIdFromJwtToken(authHeader.replace("Bearer ", ""));
// 检查是否有激活的记录 List<AnswerRecord> activateRecords = answerRecordService.findActivateRecordByUserId((int)userId); if (!activateRecords.isEmpty()) { return ResponseEntity.ok(activateRecords.stream() .map(this::getAnswerRecordDTOByRecord) .collect(Collectors.toList())); }
// 返回历史记录 return ResponseEntity.ok(answerRecordService.findHistoryRecordByUserId((int)userId).stream() .map(this::getAnswerRecordDTOByRecord) .collect(Collectors.toList())); }
// 完成答题 @PostMapping("finishRecord") public Result finishRecord(@RequestBody DataRequest dataRequest) { try { AnswerRecord record = answerRecordService.finishAnswerRecord(dataRequest); return Result.success(record.getDTOExceptQuestions(), "批改完成"); } catch (Exception e) { return Result.error("批改失败:" + e.getMessage()); } }
// 辅助方法:构建答题记录DTO private AnswerRecordDTO getAnswerRecordDTOByRecord(AnswerRecord record) { AnswerRecordDTO dto = record.getDTOExceptQuestions(); dto.setQuestions(questionBankService.getQuestionByQuestionStr(record.getQuestions())); return dto; } } |
6. 总结
本趣味问答模块采用标准的Spring Boot分层架构设计,各层职责明确:
- Model层 :定义数据实体和业务对象,使用JPA注解映射数据库表
- Repository层 :提供数据访问接口,继承JpaRepository获得基本CRUD能力
- Service层 :实现核心业务逻辑,处理事务和异常
- Controller层 :处理HTTP请求,返回JSON响应
系统支持多种玩法模式(练习、排位),完整的答题记录跟踪,以及成就系统和排行榜功能。通过合理的DTO设计实现了前后端数据的安全传输。
趣味问答模块前端代码说明
1. 概述
本趣味问答模块是一个基于Vue3和Pinia的前端应用,包含用户答题功能和管理员管理功能。系统采用响应式设计,支持多种答题模式,并提供完善的题目管理功能。
2. 用户端功能说明
2.1 功能模块
2.1.1 题目分类选择界面
用户端首页展示所有题目分类卡片,每个卡片包含:
- 分类名称
- 历史最高正确率
- 测试次数
- 两种答题模式按钮
// 关键代码 const computedQuizList = computed(() => { return classList.value.map((cls) => { const records = historyList.value.filter( (record) => record.classId === cls.id && record.activating === 0 );
let highestRate = 0; let recordCount = records.length;
// 计算历史最高正确率 if (records.length > 0) { records.forEach((record) => { const correctStr = record.correct || ''; const total = correctStr.length; if (total > 0) { const correctNum = (correctStr.match(/1/g) || []).length; const rate = (correctNum / total) * 100; if (rate > highestRate) { highestRate = rate; } } }); }
return { id: cls.id, title: cls.className, rate: records.length > 0 ? highestRate.toFixed(2) + '%' : '0%', count: recordCount, }; }); }); |
2.1.2 答题界面
答题界面主要功能:
- 题目展示区
- 选项选择
- 答题卡导航
- 计时功能
- 提交答案
// 关键代码 const submitAnswers = async () => { try { loadingBar.start(); const answerString = answers.value.join(''); const res = await finishAnswerRecord(quizStore.currentQuiz.id, answerString);
// 计算正确率 correctCount.value = 0; for (let i = 0; i < answers.value.length; i++) { if (res.data.correct[i] === '1') { correctCount.value++; } } accuracy.value = Math.round((correctCount.value / answers.value.length) * 100);
// 显示完成对话框 dialog.success({ title: '答题完成', content: `恭喜您完成作答!正确率: ${accuracy.value}%`, onPositiveClick: () => { isReviewMode.value = true; quizStore.setReviewMode(res.data, accuracy.value); }, }); } catch (error) { console.error('提交答案失败:', error); dialog.error({ title: '提交失败', content: '提交答案失败,请稍后再试。', }); } }; |
2.1.3 答题结果回顾
答题完成后进入回顾模式:
- 显示每道题的正确/错误状态
- 计算并显示整体正确率
- 可查看每道题的正确答案
// 回顾模式相关代码 const setReviewMode = (data,acc) => { isReviewMode.value = true reviewData.value = data // 计算正确率 if (data && data.correct) { let correctCount = 0 for (let i = 0; i < data.correct.length; i++) { if (data.correct[i] === '1') { correctCount++ } } accuracy.value = acc } } |
2.2 状态管理
使用Pinia管理答题状态:
// store定义 export const useQuizStore = defineStore('quiz', () => { const currentQuiz = ref(null) const currentClassName = ref(null) const isReviewMode = ref(false) const reviewData = ref(null) const accuracy = ref(0) const startAnswerRecordStore =async (classId, playMode, limit,className) => { const res = await startAnswerRecord(classId, playMode, limit) currentClassName.value = className if (res) { currentQuiz.value = res isReviewMode.value = false reviewData.value = null } return res } return { currentQuiz, currentClassName, isReviewMode, reviewData, accuracy, setCurrentQuiz, setReviewMode, startAnswerRecordStore, leaveQuiz } }) |
2.3 特色功能
- 自动计时与提交 :30分钟倒计时,时间结束自动提交
- 答题进度保存 :未完成的答题可以继续
- 答题卡导航 :快速跳转到任意题目
- 答题统计 :记录历史最高正确率和测试次数
3. 管理员端功能说明
3.1 功能模块
3.1.1 分类管理界面
管理员可以查看所有题目分类,并进入具体分类管理题目:
// 关键代码 const goToQuestionManagement = (classId) => { router.push({ name: 'quiz-answer-admin', query: { classId: classId.toString(), className: classList.value.find((cls) => cls.id === classId)?.className || '', }, }); }; |
3.1.2 题目管理界面
管理员可以对题目进行增删改查操作:
// 关键功能代码 const addNewQuestion = () => { const newQuestion = { id: Date.now(), question: '新题目内容', options: '选项A\\n选项B\\n选项C\\n选项D', correctAnswer: 'A', }; questions.value.push(newQuestion); currentIndex.value = questions.value.length - 1; }; const confirmDeleteQuestion = () => { questions.value.splice(currentIndex.value, 1); if (currentIndex.value >= questions.value.length) { currentIndex.value = Math.max(0, questions.value.length - 1); } }; const saveQuestionChanges = () => { // 调用API保存所有修改 console.log('保存题目修改:', currentQuestion.value); }; |
3.2 特色功能
- 题目编辑 :直接编辑题目内容和选项
- 批量导入 :支持JSON格式题目批量导入
- 正确答案设置 :可视化设置正确答案
- 题目导航 :快速跳转查看不同题目
// 批量导入功能 const handleFileChange = async (data) => { const file = data.file?.file; if (!file) return; try { const content = await readFileAsText(file); const importedQuestions = JSON.parse(content);
if (!Array.isArray(importedQuestions)) { throw new Error('文件格式不正确,应该是一个数组'); } questions.value = [...questions.value, ...importedQuestions]; message.success(`成功导入 ${importedQuestions.length} 道题目`); } catch (error) { console.error('导入失败:', error); message.error(`导入失败: ${error.message}`); } }; |
4. API接口说明
4.1 用户端API
export const findRandomQuestionByclassId = (classId,limit) => { return request({ url: '/questionBank/findRandomQuestionByClassId', method: 'POST', data: { data:{classId,limit }} }) } export const startAnswerRecord = (classId, playMode, limit) => { return request({ url: '/answerRecord/startRecord', method: 'POST', data: { data:{classId, playMode, limit }} }) } export const finishAnswerRecord = (answerRecordId, answers) => { return request({ url: '/answerRecord/finishRecord', method: 'POST', data: { data:{answerRecordId, answers}} }) } |
4.2 管理员端API
export const findQuestionByclassId = (classId) => { return request({ url: '/questionBank/findQuestionByClassId', method: 'POST', data: { data:{classId }} }) } export const findAllQuestionClasses = () => { return request({ url: '/questionBank/findAllQuestionClasses', method: 'POST' }) } |
5. 样式设计
系统采用统一的样式变量和设计规范:
:root { --shenlan: rgba(53, 86, 90, 1); --zhonglv: rgba(92, 176, 106, 1); --danmi: rgba(255, 254, 248, 1); --black-1: rgba(68, 68, 68, 1); --grayswhite: rgba(255, 255, 255, 1); --transparent: rgba(255, 255, 255, 0.6); } |
答题界面采用左右分栏设计:
- 左侧:题目展示和作答区
- 右侧:答题卡和统计信息
6. 总结
本趣味问答模块前端实现具有以下特点:
- 用户端提供流畅的答题体验,支持多种答题模式
- 管理员端提供完善的题目管理功能
- 采用Pinia进行状态管理,保证数据一致性
- 响应式设计适配不同设备
- 详细的答题统计和回顾功能
系统通过清晰的界面设计和丰富的交互功能,为用户提供了有趣且实用的答题体验,同时为管理员提供了便捷的题目管理工具。