diff --git a/docs/learning_system_dictionaries.md b/docs/learning_system_dictionaries.md new file mode 100644 index 00000000..43ab7281 --- /dev/null +++ b/docs/learning_system_dictionaries.md @@ -0,0 +1,177 @@ +# 学习系统数据字典配置文档 + +## 概述 + +本文档描述了学习系统中使用的所有数据字典配置,包括字典类型、字典值和使用说明。 + +## 字典列表 + +### 1. 课程难度 (course_difficulty) + +**用途**: 定义课程的难度等级 + +| 标签 | 值 | 排序 | 标签类型 | 说明 | +|------|----|----|---------|------| +| 初级 | beginner | 1 | success | 适合初学者的基础课程 | +| 中级 | intermediate | 2 | warning | 需要一定基础的进阶课程 | +| 高级 | advanced | 3 | danger | 面向专业人士的高级课程 | + +### 2. 课程状态 (course_status) + +**用途**: 定义课程的发布状态 + +| 标签 | 值 | 排序 | 标签类型 | 说明 | +|------|----|----|---------|------| +| 草稿 | draft | 1 | info | 课程正在编辑中,未发布 | +| 已发布 | published | 2 | success | 课程已发布,学生可以学习 | +| 已下架 | archived | 3 | danger | 课程已下架,不再提供学习 | + +### 3. 章节状态 (chapter_status) + +**用途**: 定义章节的发布状态 + +| 标签 | 值 | 排序 | 标签类型 | 说明 | +|------|----|----|---------|------| +| 草稿 | draft | 1 | info | 章节正在编辑中,未发布 | +| 已发布 | published | 2 | success | 章节已发布,可以学习 | +| 隐藏 | hidden | 3 | warning | 章节已隐藏,暂时不显示 | + +### 4. 知识点状态 (knowledge_status) + +**用途**: 定义知识点的发布状态 + +| 标签 | 值 | 排序 | 标签类型 | 说明 | +|------|----|----|---------|------| +| 草稿 | draft | 1 | info | 知识点正在编辑中,未发布 | +| 已发布 | published | 2 | success | 知识点已发布,可以学习 | +| 隐藏 | hidden | 3 | warning | 知识点已隐藏,暂时不显示 | + +### 5. 题目类型 (question_type) + +**用途**: 定义题目的类型 + +| 标签 | 值 | 排序 | 标签类型 | 说明 | +|------|----|----|---------|------| +| 单选题 | single_choice | 1 | - | 只能选择一个正确答案 | +| 多选题 | multiple_choice | 2 | success | 可以选择多个正确答案 | +| 判断题 | true_false | 3 | warning | 判断对错的题目 | +| 填空题 | fill_blank | 4 | danger | 需要填写答案的题目 | + +### 6. 题目难度 (question_difficulty) + +**用途**: 定义题目的难度等级 + +| 标签 | 值 | 排序 | 标签类型 | 说明 | +|------|----|----|---------|------| +| 简单 | easy | 1 | success | 基础题目,容易解答 | +| 中等 | medium | 2 | warning | 中等难度题目 | +| 困难 | hard | 3 | danger | 高难度题目,需要深入思考 | + +### 7. 学习状态 (learning_status) + +**用途**: 定义用户的学习状态 + +| 标签 | 值 | 排序 | 标签类型 | 说明 | +|------|----|----|---------|------| +| 未开始 | not_started | 1 | info | 用户还未开始学习该课程 | +| 学习中 | learning | 2 | warning | 用户正在学习该课程 | +| 已完成 | completed | 3 | success | 用户已完成该课程的学习 | +| 已暂停 | paused | 4 | danger | 用户暂停了该课程的学习 | + +## 使用方法 + +### 1. 后端使用 + +在Go代码中,可以直接使用字典值: + +```go +// 创建课程时设置难度 +course := learning.Course{ + Title: "Go语言入门", + Difficulty: "beginner", // 使用字典值 + Status: "draft", +} +``` + +### 2. 前端使用 + +#### 2.1 导入字典工具 + +```javascript +import { LearningDict } from '@/utils/learningDict' +``` + +#### 2.2 获取标签和类型 + +```javascript +// 获取课程难度标签 +const difficultyLabel = LearningDict.CourseDifficulty.getLabel('beginner') // 返回: '初级' + +// 获取课程难度标签类型 +const difficultyType = LearningDict.CourseDifficulty.getType('beginner') // 返回: 'success' + +// 获取所有选项 +const difficultyOptions = LearningDict.CourseDifficulty.getOptions() +``` + +#### 2.3 在Vue组件中使用 + +```vue + + + +``` + +### 3. 数据库初始化 + +执行以下SQL脚本来初始化字典数据: + +```sql +-- 执行字典初始化脚本 +source server/sql/learning_dictionaries.sql; +``` + +## 扩展说明 + +### 添加新的字典值 + +1. **后端**: 在 `server/source/system/dictionary_detail.go` 中添加新的字典详情 +2. **前端**: 在 `web/src/utils/learningDict.js` 中添加对应的选项和工具函数 +3. **数据库**: 执行相应的SQL语句添加新的字典记录 + +### 修改现有字典值 + +1. 更新数据库中的字典记录 +2. 更新前端字典工具文件 +3. 确保所有使用该字典值的地方都已更新 + +## 注意事项 + +1. **一致性**: 确保前后端使用的字典值完全一致 +2. **扩展性**: 新增字典值时要考虑向后兼容性 +3. **国际化**: 如需支持多语言,可以扩展字典工具函数 +4. **缓存**: 前端可以考虑缓存字典数据以提高性能 + +## 维护记录 + +| 日期 | 版本 | 修改内容 | 修改人 | +|------|------|----------|--------| +| 2024-01-XX | 1.0 | 初始版本,包含7个基础字典 | System | + +--- + +*本文档会随着系统功能的扩展而持续更新* diff --git a/server/api/v1/enter.go b/server/api/v1/enter.go index 5c1dff44..2fcda7a7 100644 --- a/server/api/v1/enter.go +++ b/server/api/v1/enter.go @@ -2,12 +2,14 @@ package v1 import ( "github.com/flipped-aurora/gin-vue-admin/server/api/v1/example" + "github.com/flipped-aurora/gin-vue-admin/server/api/v1/learning" "github.com/flipped-aurora/gin-vue-admin/server/api/v1/system" ) var ApiGroupApp = new(ApiGroup) type ApiGroup struct { - SystemApiGroup system.ApiGroup - ExampleApiGroup example.ApiGroup + SystemApiGroup system.ApiGroup + ExampleApiGroup example.ApiGroup + LearningApiGroup learning.ApiGroup } diff --git a/server/api/v1/learning/chapter.go b/server/api/v1/learning/chapter.go new file mode 100644 index 00000000..faa11c74 --- /dev/null +++ b/server/api/v1/learning/chapter.go @@ -0,0 +1,200 @@ +package learning + +import ( + "strconv" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/learning" + learningReq "github.com/flipped-aurora/gin-vue-admin/server/model/learning/request" + "github.com/flipped-aurora/gin-vue-admin/server/service" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type ChapterApi struct{} + +// CreateChapter 创建章节 +// @Tags Chapter +// @Summary 创建章节 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body learningReq.ChapterCreate true "创建章节" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /chapter/createChapter [post] +func (chapterApi *ChapterApi) CreateChapter(c *gin.Context) { + var req learningReq.ChapterCreate + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + chapter := learning.Chapter{ + CourseId: req.CourseId, + Title: req.Title, + Description: req.Description, + Sort: req.Sort, + Status: req.Status, + Duration: req.Duration, + } + + err = service.ServiceGroupApp.LearningServiceGroup.ChapterService.CreateChapter(c.Request.Context(), &chapter) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败", c) + return + } + response.OkWithMessage("创建成功", c) +} + +// DeleteChapter 删除章节 +// @Tags Chapter +// @Summary 删除章节 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除章节" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /chapter/deleteChapter [delete] +func (chapterApi *ChapterApi) DeleteChapter(c *gin.Context) { + ID := c.Query("ID") + err := service.ServiceGroupApp.LearningServiceGroup.ChapterService.DeleteChapter(c.Request.Context(), ID) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// DeleteChapterByIds 批量删除章节 +// @Tags Chapter +// @Summary 批量删除章节 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /chapter/deleteChapterByIds [delete] +func (chapterApi *ChapterApi) DeleteChapterByIds(c *gin.Context) { + IDs := c.QueryArray("IDs[]") + err := service.ServiceGroupApp.LearningServiceGroup.ChapterService.DeleteChapterByIds(c.Request.Context(), IDs) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败", c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// UpdateChapter 更新章节 +// @Tags Chapter +// @Summary 更新章节 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body learningReq.ChapterUpdate true "更新章节" +// @Success 200 {object} response.Response{msg=string} "更新成功" +// @Router /chapter/updateChapter [put] +func (chapterApi *ChapterApi) UpdateChapter(c *gin.Context) { + var req learningReq.ChapterUpdate + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + chapter := learning.Chapter{ + GVA_MODEL: global.GVA_MODEL{ID: req.ID}, + CourseId: req.CourseId, + Title: req.Title, + Description: req.Description, + Sort: req.Sort, + Status: req.Status, + Duration: req.Duration, + } + + err = service.ServiceGroupApp.LearningServiceGroup.ChapterService.UpdateChapter(c.Request.Context(), chapter) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败", c) + return + } + response.OkWithMessage("更新成功", c) +} + +// FindChapter 用id查询章节 +// @Tags Chapter +// @Summary 用id查询章节 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query learning.Chapter true "用id查询章节" +// @Success 200 {object} response.Response{data=learning.Chapter,msg=string} "查询成功" +// @Router /chapter/findChapter [get] +func (chapterApi *ChapterApi) FindChapter(c *gin.Context) { + ID := c.Query("ID") + rechapter, err := service.ServiceGroupApp.LearningServiceGroup.ChapterService.GetChapter(c.Request.Context(), ID) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败", c) + return + } + response.OkWithData(rechapter, c) +} + +// GetChapterList 分页获取章节列表 +// @Tags Chapter +// @Summary 分页获取章节列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query learningReq.ChapterSearch true "分页获取章节列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /chapter/getChapterList [get] +func (chapterApi *ChapterApi) GetChapterList(c *gin.Context) { + var pageInfo learningReq.ChapterSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := service.ServiceGroupApp.LearningServiceGroup.ChapterService.GetChapterInfoList(c.Request.Context(), pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} + +// GetChaptersByCourse 根据课程获取章节列表 +// @Tags Chapter +// @Summary 根据课程获取章节列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param courseId query uint true "课程ID" +// @Success 200 {object} response.Response{data=[]learning.Chapter,msg=string} "获取成功" +// @Router /chapter/getChaptersByCourse [get] +func (chapterApi *ChapterApi) GetChaptersByCourse(c *gin.Context) { + courseIdStr := c.Query("courseId") + courseId, err := strconv.ParseUint(courseIdStr, 10, 32) + if err != nil { + response.FailWithMessage("课程ID格式错误", c) + return + } + list, err := service.ServiceGroupApp.LearningServiceGroup.ChapterService.GetChaptersByCourse(c.Request.Context(), uint(courseId)) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithData(list, c) +} diff --git a/server/api/v1/learning/course.go b/server/api/v1/learning/course.go new file mode 100644 index 00000000..7987e6e0 --- /dev/null +++ b/server/api/v1/learning/course.go @@ -0,0 +1,197 @@ +package learning + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/learning" + learningReq "github.com/flipped-aurora/gin-vue-admin/server/model/learning/request" + "github.com/flipped-aurora/gin-vue-admin/server/service" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type CourseApi struct{} + +// CreateCourse 创建课程 +// @Tags Course +// @Summary 创建课程 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body learningReq.CourseCreate true "创建课程" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /course/createCourse [post] +func (courseApi *CourseApi) CreateCourse(c *gin.Context) { + var req learningReq.CourseCreate + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + course := learning.Course{ + Title: req.Title, + Description: req.Description, + Cover: req.Cover, + Difficulty: req.Difficulty, + Duration: req.Duration, + Status: req.Status, + Price: req.Price, + Tags: req.Tags, + Sort: req.Sort, + } + + err = service.ServiceGroupApp.LearningServiceGroup.CourseService.CreateCourse(c.Request.Context(), &course) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败", c) + return + } + response.OkWithMessage("创建成功", c) +} + +// DeleteCourse 删除课程 +// @Tags Course +// @Summary 删除课程 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除课程" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /course/deleteCourse [delete] +func (courseApi *CourseApi) DeleteCourse(c *gin.Context) { + ID := c.Query("ID") + err := service.ServiceGroupApp.LearningServiceGroup.CourseService.DeleteCourse(c.Request.Context(), ID) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// DeleteCourseByIds 批量删除课程 +// @Tags Course +// @Summary 批量删除课程 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /course/deleteCourseByIds [delete] +func (courseApi *CourseApi) DeleteCourseByIds(c *gin.Context) { + IDs := c.QueryArray("IDs[]") + err := service.ServiceGroupApp.LearningServiceGroup.CourseService.DeleteCourseByIds(c.Request.Context(), IDs) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败", c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// UpdateCourse 更新课程 +// @Tags Course +// @Summary 更新课程 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body learningReq.CourseUpdate true "更新课程" +// @Success 200 {object} response.Response{msg=string} "更新成功" +// @Router /course/updateCourse [put] +func (courseApi *CourseApi) UpdateCourse(c *gin.Context) { + var req learningReq.CourseUpdate + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + course := learning.Course{ + GVA_MODEL: global.GVA_MODEL{ID: req.ID}, + Title: req.Title, + Description: req.Description, + Cover: req.Cover, + Difficulty: req.Difficulty, + Duration: req.Duration, + Status: req.Status, + Price: req.Price, + Tags: req.Tags, + Sort: req.Sort, + } + + err = service.ServiceGroupApp.LearningServiceGroup.CourseService.UpdateCourse(c.Request.Context(), course) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败", c) + return + } + response.OkWithMessage("更新成功", c) +} + +// FindCourse 用id查询课程 +// @Tags Course +// @Summary 用id查询课程 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query learning.Course true "用id查询课程" +// @Success 200 {object} response.Response{data=learning.Course,msg=string} "查询成功" +// @Router /course/findCourse [get] +func (courseApi *CourseApi) FindCourse(c *gin.Context) { + ID := c.Query("ID") + recourse, err := service.ServiceGroupApp.LearningServiceGroup.CourseService.GetCourse(c.Request.Context(), ID) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败", c) + return + } + response.OkWithData(recourse, c) +} + +// GetCourseList 分页获取课程列表 +// @Tags Course +// @Summary 分页获取课程列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query learningReq.CourseSearch true "分页获取课程列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /course/getCourseList [get] +func (courseApi *CourseApi) GetCourseList(c *gin.Context) { + var pageInfo learningReq.CourseSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := service.ServiceGroupApp.LearningServiceGroup.CourseService.GetCourseInfoList(c.Request.Context(), pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} + +// GetPublishedCourses 获取已发布的课程列表 +// @Tags Course +// @Summary 获取已发布的课程列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=[]learning.Course,msg=string} "获取成功" +// @Router /course/getPublishedCourses [get] +func (courseApi *CourseApi) GetPublishedCourses(c *gin.Context) { + list, err := service.ServiceGroupApp.LearningServiceGroup.CourseService.GetPublishedCourses(c.Request.Context()) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithData(list, c) +} diff --git a/server/api/v1/learning/enter.go b/server/api/v1/learning/enter.go new file mode 100644 index 00000000..d56276ca --- /dev/null +++ b/server/api/v1/learning/enter.go @@ -0,0 +1,12 @@ +package learning + +type ApiGroup struct { + CourseApi + ChapterApi + KnowledgePointApi + QuestionApi + UserLearningApi + WrongQuestionApi + ExamApi + UserExamApi +} diff --git a/server/api/v1/learning/exam.go b/server/api/v1/learning/exam.go new file mode 100644 index 00000000..0a83403c --- /dev/null +++ b/server/api/v1/learning/exam.go @@ -0,0 +1,276 @@ +package learning + +import ( + "strconv" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/learning" + learningReq "github.com/flipped-aurora/gin-vue-admin/server/model/learning/request" + "github.com/flipped-aurora/gin-vue-admin/server/service" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type ExamApi struct{} + +// CreateExam 创建考试 +// @Tags Exam +// @Summary 创建考试 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body learningReq.ExamCreate true "创建考试" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /exam/createExam [post] +func (examApi *ExamApi) CreateExam(c *gin.Context) { + var req learningReq.ExamCreate + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + exam := learning.Exam{ + Title: req.Title, + Description: req.Description, + CourseId: req.CourseId, + Duration: req.Duration, + TotalScore: req.TotalScore, + PassScore: req.PassScore, + QuestionCount: req.QuestionCount, + StartTime: req.StartTime, + EndTime: req.EndTime, + Status: req.Status, + AllowRetake: req.AllowRetake, + MaxAttempts: req.MaxAttempts, + } + + err = service.ServiceGroupApp.LearningServiceGroup.ExamService.CreateExam(c.Request.Context(), &exam) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败", c) + return + } + response.OkWithMessage("创建成功", c) +} + +// DeleteExam 删除考试 +// @Tags Exam +// @Summary 删除考试 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除考试" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /exam/deleteExam [delete] +func (examApi *ExamApi) DeleteExam(c *gin.Context) { + ID := c.Query("ID") + err := service.ServiceGroupApp.LearningServiceGroup.ExamService.DeleteExam(c.Request.Context(), ID) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// DeleteExamByIds 批量删除考试 +// @Tags Exam +// @Summary 批量删除考试 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /exam/deleteExamByIds [delete] +func (examApi *ExamApi) DeleteExamByIds(c *gin.Context) { + IDs := c.QueryArray("IDs[]") + err := service.ServiceGroupApp.LearningServiceGroup.ExamService.DeleteExamByIds(c.Request.Context(), IDs) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败", c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// UpdateExam 更新考试 +// @Tags Exam +// @Summary 更新考试 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body learningReq.ExamUpdate true "更新考试" +// @Success 200 {object} response.Response{msg=string} "更新成功" +// @Router /exam/updateExam [put] +func (examApi *ExamApi) UpdateExam(c *gin.Context) { + var req learningReq.ExamUpdate + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + exam := learning.Exam{ + GVA_MODEL: global.GVA_MODEL{ID: req.ID}, + Title: req.Title, + Description: req.Description, + CourseId: req.CourseId, + Duration: req.Duration, + TotalScore: req.TotalScore, + PassScore: req.PassScore, + QuestionCount: req.QuestionCount, + StartTime: req.StartTime, + EndTime: req.EndTime, + Status: req.Status, + AllowRetake: req.AllowRetake, + MaxAttempts: req.MaxAttempts, + } + + err = service.ServiceGroupApp.LearningServiceGroup.ExamService.UpdateExam(c.Request.Context(), exam) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败", c) + return + } + response.OkWithMessage("更新成功", c) +} + +// FindExam 用id查询考试 +// @Tags Exam +// @Summary 用id查询考试 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query learning.Exam true "用id查询考试" +// @Success 200 {object} response.Response{data=learning.Exam,msg=string} "查询成功" +// @Router /exam/findExam [get] +func (examApi *ExamApi) FindExam(c *gin.Context) { + ID := c.Query("ID") + reexam, err := service.ServiceGroupApp.LearningServiceGroup.ExamService.GetExam(c.Request.Context(), ID) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败", c) + return + } + response.OkWithData(reexam, c) +} + +// GetExamList 分页获取考试列表 +// @Tags Exam +// @Summary 分页获取考试列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query learningReq.ExamSearch true "分页获取考试列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /exam/getExamList [get] +func (examApi *ExamApi) GetExamList(c *gin.Context) { + var pageInfo learningReq.ExamSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := service.ServiceGroupApp.LearningServiceGroup.ExamService.GetExamInfoList(c.Request.Context(), pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} + +// GetActiveExams 获取进行中的考试列表 +// @Tags Exam +// @Summary 获取进行中的考试列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=[]learning.Exam,msg=string} "获取成功" +// @Router /exam/getActiveExams [get] +func (examApi *ExamApi) GetActiveExams(c *gin.Context) { + list, err := service.ServiceGroupApp.LearningServiceGroup.ExamService.GetActiveExams(c.Request.Context()) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithData(list, c) +} + +// GetExamsByCourse 根据课程获取考试列表 +// @Tags Exam +// @Summary 根据课程获取考试列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param courseId query uint true "课程ID" +// @Success 200 {object} response.Response{data=[]learning.Exam,msg=string} "获取成功" +// @Router /exam/getExamsByCourse [get] +func (examApi *ExamApi) GetExamsByCourse(c *gin.Context) { + courseIdStr := c.Query("courseId") + courseId := uint(0) + if courseIdStr != "" { + if id, err := strconv.ParseUint(courseIdStr, 10, 32); err == nil { + courseId = uint(id) + } + } + + list, err := service.ServiceGroupApp.LearningServiceGroup.ExamService.GetExamsByCourse(c.Request.Context(), courseId) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithData(list, c) +} + +// PublishExam 发布考试 +// @Tags Exam +// @Summary 发布考试 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body learningReq.PublishExamRequest true "发布考试请求" +// @Success 200 {object} response.Response{msg=string} "发布成功" +// @Router /exam/publishExam [put] +func (examApi *ExamApi) PublishExam(c *gin.Context) { + var req learningReq.PublishExamRequest + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + err = service.ServiceGroupApp.LearningServiceGroup.ExamService.PublishExam(c.Request.Context(), req.ID) + if err != nil { + global.GVA_LOG.Error("发布失败!", zap.Error(err)) + response.FailWithMessage("发布失败", c) + return + } + response.OkWithMessage("发布成功", c) +} + +// GetExamStatistics 获取考试统计 +// @Tags Exam +// @Summary 获取考试统计 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=learningReq.ExamStatistics,msg=string} "获取成功" +// @Router /exam/getExamStatistics [get] +func (examApi *ExamApi) GetExamStatistics(c *gin.Context) { + stats, err := service.ServiceGroupApp.LearningServiceGroup.ExamService.GetExamStatistics(c.Request.Context()) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithData(stats, c) +} diff --git a/server/api/v1/learning/knowledge_point.go b/server/api/v1/learning/knowledge_point.go new file mode 100644 index 00000000..a2a71e84 --- /dev/null +++ b/server/api/v1/learning/knowledge_point.go @@ -0,0 +1,204 @@ +package learning + +import ( + "strconv" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/learning" + learningReq "github.com/flipped-aurora/gin-vue-admin/server/model/learning/request" + "github.com/flipped-aurora/gin-vue-admin/server/service" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type KnowledgePointApi struct{} + +// CreateKnowledgePoint 创建知识点 +// @Tags KnowledgePoint +// @Summary 创建知识点 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body learningReq.KnowledgePointCreate true "创建知识点" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /knowledgePoint/createKnowledgePoint [post] +func (knowledgePointApi *KnowledgePointApi) CreateKnowledgePoint(c *gin.Context) { + var req learningReq.KnowledgePointCreate + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + knowledgePoint := learning.KnowledgePoint{ + ChapterId: req.ChapterId, + Title: req.Title, + Content: req.Content, + VideoUrl: req.VideoUrl, + Attachments: req.Attachments, + Sort: req.Sort, + Duration: req.Duration, + Status: req.Status, + } + + err = service.ServiceGroupApp.LearningServiceGroup.KnowledgePointService.CreateKnowledgePoint(c.Request.Context(), &knowledgePoint) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败", c) + return + } + response.OkWithMessage("创建成功", c) +} + +// DeleteKnowledgePoint 删除知识点 +// @Tags KnowledgePoint +// @Summary 删除知识点 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除知识点" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /knowledgePoint/deleteKnowledgePoint [delete] +func (knowledgePointApi *KnowledgePointApi) DeleteKnowledgePoint(c *gin.Context) { + ID := c.Query("ID") + err := service.ServiceGroupApp.LearningServiceGroup.KnowledgePointService.DeleteKnowledgePoint(c.Request.Context(), ID) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// DeleteKnowledgePointByIds 批量删除知识点 +// @Tags KnowledgePoint +// @Summary 批量删除知识点 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /knowledgePoint/deleteKnowledgePointByIds [delete] +func (knowledgePointApi *KnowledgePointApi) DeleteKnowledgePointByIds(c *gin.Context) { + IDs := c.QueryArray("IDs[]") + err := service.ServiceGroupApp.LearningServiceGroup.KnowledgePointService.DeleteKnowledgePointByIds(c.Request.Context(), IDs) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败", c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// UpdateKnowledgePoint 更新知识点 +// @Tags KnowledgePoint +// @Summary 更新知识点 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body learningReq.KnowledgePointUpdate true "更新知识点" +// @Success 200 {object} response.Response{msg=string} "更新成功" +// @Router /knowledgePoint/updateKnowledgePoint [put] +func (knowledgePointApi *KnowledgePointApi) UpdateKnowledgePoint(c *gin.Context) { + var req learningReq.KnowledgePointUpdate + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + knowledgePoint := learning.KnowledgePoint{ + GVA_MODEL: global.GVA_MODEL{ID: req.ID}, + ChapterId: req.ChapterId, + Title: req.Title, + Content: req.Content, + VideoUrl: req.VideoUrl, + Attachments: req.Attachments, + Sort: req.Sort, + Duration: req.Duration, + Status: req.Status, + } + + err = service.ServiceGroupApp.LearningServiceGroup.KnowledgePointService.UpdateKnowledgePoint(c.Request.Context(), knowledgePoint) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败", c) + return + } + response.OkWithMessage("更新成功", c) +} + +// FindKnowledgePoint 用id查询知识点 +// @Tags KnowledgePoint +// @Summary 用id查询知识点 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query learning.KnowledgePoint true "用id查询知识点" +// @Success 200 {object} response.Response{data=learning.KnowledgePoint,msg=string} "查询成功" +// @Router /knowledgePoint/findKnowledgePoint [get] +func (knowledgePointApi *KnowledgePointApi) FindKnowledgePoint(c *gin.Context) { + ID := c.Query("ID") + reknowledgePoint, err := service.ServiceGroupApp.LearningServiceGroup.KnowledgePointService.GetKnowledgePoint(c.Request.Context(), ID) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败", c) + return + } + response.OkWithData(reknowledgePoint, c) +} + +// GetKnowledgePointList 分页获取知识点列表 +// @Tags KnowledgePoint +// @Summary 分页获取知识点列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query learningReq.KnowledgePointSearch true "分页获取知识点列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /knowledgePoint/getKnowledgePointList [get] +func (knowledgePointApi *KnowledgePointApi) GetKnowledgePointList(c *gin.Context) { + var pageInfo learningReq.KnowledgePointSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := service.ServiceGroupApp.LearningServiceGroup.KnowledgePointService.GetKnowledgePointInfoList(c.Request.Context(), pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} + +// GetKnowledgePointsByChapter 根据章节获取知识点列表 +// @Tags KnowledgePoint +// @Summary 根据章节获取知识点列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param chapterId query uint true "章节ID" +// @Success 200 {object} response.Response{data=[]learning.KnowledgePoint,msg=string} "获取成功" +// @Router /knowledgePoint/getKnowledgePointsByChapter [get] +func (knowledgePointApi *KnowledgePointApi) GetKnowledgePointsByChapter(c *gin.Context) { + chapterIdStr := c.Query("chapterId") + chapterId, err := strconv.ParseUint(chapterIdStr, 10, 32) + if err != nil { + response.FailWithMessage("章节ID格式错误", c) + return + } + list, err := service.ServiceGroupApp.LearningServiceGroup.KnowledgePointService.GetKnowledgePointsByChapter(c.Request.Context(), uint(chapterId)) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithData(list, c) +} diff --git a/server/api/v1/learning/question.go b/server/api/v1/learning/question.go new file mode 100644 index 00000000..2f154f2e --- /dev/null +++ b/server/api/v1/learning/question.go @@ -0,0 +1,206 @@ +package learning + +import ( + "strconv" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/learning" + learningReq "github.com/flipped-aurora/gin-vue-admin/server/model/learning/request" + "github.com/flipped-aurora/gin-vue-admin/server/service" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type QuestionApi struct{} + +// CreateQuestion 创建题目 +// @Tags Question +// @Summary 创建题目 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body learningReq.QuestionCreate true "创建题目" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /question/createQuestion [post] +func (questionApi *QuestionApi) CreateQuestion(c *gin.Context) { + var req learningReq.QuestionCreate + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + question := learning.Question{ + KnowledgePointId: req.KnowledgePointId, + Title: req.Title, + Content: req.Content, + Type: req.Type, + Options: req.Options, + Answer: req.Answer, + Explanation: req.Explanation, + Difficulty: req.Difficulty, + Score: req.Score, + } + + err = service.ServiceGroupApp.LearningServiceGroup.QuestionService.CreateQuestion(c.Request.Context(), &question) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败", c) + return + } + response.OkWithMessage("创建成功", c) +} + +// DeleteQuestion 删除题目 +// @Tags Question +// @Summary 删除题目 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除题目" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /question/deleteQuestion [delete] +func (questionApi *QuestionApi) DeleteQuestion(c *gin.Context) { + ID := c.Query("ID") + err := service.ServiceGroupApp.LearningServiceGroup.QuestionService.DeleteQuestion(c.Request.Context(), ID) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// DeleteQuestionByIds 批量删除题目 +// @Tags Question +// @Summary 批量删除题目 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /question/deleteQuestionByIds [delete] +func (questionApi *QuestionApi) DeleteQuestionByIds(c *gin.Context) { + IDs := c.QueryArray("IDs[]") + err := service.ServiceGroupApp.LearningServiceGroup.QuestionService.DeleteQuestionByIds(c.Request.Context(), IDs) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败", c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// UpdateQuestion 更新题目 +// @Tags Question +// @Summary 更新题目 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body learningReq.QuestionUpdate true "更新题目" +// @Success 200 {object} response.Response{msg=string} "更新成功" +// @Router /question/updateQuestion [put] +func (questionApi *QuestionApi) UpdateQuestion(c *gin.Context) { + var req learningReq.QuestionUpdate + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + question := learning.Question{ + GVA_MODEL: global.GVA_MODEL{ID: req.ID}, + KnowledgePointId: req.KnowledgePointId, + Title: req.Title, + Content: req.Content, + Type: req.Type, + Options: req.Options, + Answer: req.Answer, + Explanation: req.Explanation, + Difficulty: req.Difficulty, + Score: req.Score, + } + + err = service.ServiceGroupApp.LearningServiceGroup.QuestionService.UpdateQuestion(c.Request.Context(), question) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败", c) + return + } + response.OkWithMessage("更新成功", c) +} + +// FindQuestion 用id查询题目 +// @Tags Question +// @Summary 用id查询题目 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query learning.Question true "用id查询题目" +// @Success 200 {object} response.Response{data=learning.Question,msg=string} "查询成功" +// @Router /question/findQuestion [get] +func (questionApi *QuestionApi) FindQuestion(c *gin.Context) { + ID := c.Query("ID") + requestion, err := service.ServiceGroupApp.LearningServiceGroup.QuestionService.GetQuestion(c.Request.Context(), ID) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败", c) + return + } + response.OkWithData(requestion, c) +} + +// GetQuestionList 分页获取题目列表 +// @Tags Question +// @Summary 分页获取题目列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query learningReq.QuestionSearch true "分页获取题目列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /question/getQuestionList [get] +func (questionApi *QuestionApi) GetQuestionList(c *gin.Context) { + var pageInfo learningReq.QuestionSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := service.ServiceGroupApp.LearningServiceGroup.QuestionService.GetQuestionInfoList(c.Request.Context(), pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} + +// GetQuestionsByKnowledgePoint 根据知识点获取题目列表 +// @Tags Question +// @Summary 根据知识点获取题目列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param knowledgePointId query uint true "知识点ID" +// @Success 200 {object} response.Response{data=[]learning.Question,msg=string} "获取成功" +// @Router /question/getQuestionsByKnowledgePoint [get] +func (questionApi *QuestionApi) GetQuestionsByKnowledgePoint(c *gin.Context) { + knowledgePointIdStr := c.Query("knowledgePointId") + knowledgePointId, err := strconv.ParseUint(knowledgePointIdStr, 10, 32) + if err != nil { + response.FailWithMessage("知识点ID格式错误", c) + return + } + list, err := service.ServiceGroupApp.LearningServiceGroup.QuestionService.GetQuestionsByKnowledgePoint(c.Request.Context(), uint(knowledgePointId)) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithData(list, c) +} diff --git a/server/api/v1/learning/user_exam.go b/server/api/v1/learning/user_exam.go new file mode 100644 index 00000000..f3ee3057 --- /dev/null +++ b/server/api/v1/learning/user_exam.go @@ -0,0 +1,349 @@ +package learning + +import ( + "strconv" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/learning" + learningReq "github.com/flipped-aurora/gin-vue-admin/server/model/learning/request" + "github.com/flipped-aurora/gin-vue-admin/server/service" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type UserExamApi struct{} + +// CreateUserExam 创建用户考试记录 +// @Tags UserExam +// @Summary 创建用户考试记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body learningReq.UserExamCreate true "创建用户考试记录" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /userExam/createUserExam [post] +func (userExamApi *UserExamApi) CreateUserExam(c *gin.Context) { + var req learningReq.UserExamCreate + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + userExam := learning.UserExam{ + UserId: req.UserId, + ExamId: req.ExamId, + AttemptNumber: req.AttemptNumber, + StartTime: req.StartTime, + EndTime: req.EndTime, + Duration: req.Duration, + TotalScore: req.TotalScore, + CorrectCount: req.CorrectCount, + WrongCount: req.WrongCount, + Status: req.Status, + IsPassed: req.IsPassed, + SubmittedAt: req.SubmittedAt, + } + + err = service.ServiceGroupApp.LearningServiceGroup.UserExamService.CreateUserExam(c.Request.Context(), &userExam) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败", c) + return + } + response.OkWithMessage("创建成功", c) +} + +// DeleteUserExam 删除用户考试记录 +// @Tags UserExam +// @Summary 删除用户考试记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除用户考试记录" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /userExam/deleteUserExam [delete] +func (userExamApi *UserExamApi) DeleteUserExam(c *gin.Context) { + ID := c.Query("ID") + err := service.ServiceGroupApp.LearningServiceGroup.UserExamService.DeleteUserExam(c.Request.Context(), ID) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// DeleteUserExamByIds 批量删除用户考试记录 +// @Tags UserExam +// @Summary 批量删除用户考试记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /userExam/deleteUserExamByIds [delete] +func (userExamApi *UserExamApi) DeleteUserExamByIds(c *gin.Context) { + IDs := c.QueryArray("IDs[]") + err := service.ServiceGroupApp.LearningServiceGroup.UserExamService.DeleteUserExamByIds(c.Request.Context(), IDs) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败", c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// UpdateUserExam 更新用户考试记录 +// @Tags UserExam +// @Summary 更新用户考试记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body learningReq.UserExamUpdate true "更新用户考试记录" +// @Success 200 {object} response.Response{msg=string} "更新成功" +// @Router /userExam/updateUserExam [put] +func (userExamApi *UserExamApi) UpdateUserExam(c *gin.Context) { + var req learningReq.UserExamUpdate + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + userExam := learning.UserExam{ + GVA_MODEL: global.GVA_MODEL{ID: req.ID}, + UserId: req.UserId, + ExamId: req.ExamId, + AttemptNumber: req.AttemptNumber, + StartTime: req.StartTime, + EndTime: req.EndTime, + Duration: req.Duration, + TotalScore: req.TotalScore, + CorrectCount: req.CorrectCount, + WrongCount: req.WrongCount, + Status: req.Status, + IsPassed: req.IsPassed, + SubmittedAt: req.SubmittedAt, + } + + err = service.ServiceGroupApp.LearningServiceGroup.UserExamService.UpdateUserExam(c.Request.Context(), userExam) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败", c) + return + } + response.OkWithMessage("更新成功", c) +} + +// FindUserExam 用id查询用户考试记录 +// @Tags UserExam +// @Summary 用id查询用户考试记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query learning.UserExam true "用id查询用户考试记录" +// @Success 200 {object} response.Response{data=learning.UserExam,msg=string} "查询成功" +// @Router /userExam/findUserExam [get] +func (userExamApi *UserExamApi) FindUserExam(c *gin.Context) { + ID := c.Query("ID") + reuserExam, err := service.ServiceGroupApp.LearningServiceGroup.UserExamService.GetUserExam(c.Request.Context(), ID) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败", c) + return + } + response.OkWithData(reuserExam, c) +} + +// GetUserExamList 分页获取用户考试记录列表 +// @Tags UserExam +// @Summary 分页获取用户考试记录列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query learningReq.UserExamSearch true "分页获取用户考试记录列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /userExam/getUserExamList [get] +func (userExamApi *UserExamApi) GetUserExamList(c *gin.Context) { + var pageInfo learningReq.UserExamSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := service.ServiceGroupApp.LearningServiceGroup.UserExamService.GetUserExamInfoList(c.Request.Context(), pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} + +// StartExam 开始考试 +// @Tags UserExam +// @Summary 开始考试 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body learningReq.StartExamRequest true "开始考试请求" +// @Success 200 {object} response.Response{data=learning.UserExam,msg=string} "开始成功" +// @Router /userExam/startExam [post] +func (userExamApi *UserExamApi) StartExam(c *gin.Context) { + var req learningReq.StartExamRequest + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + // 如果没有提供userId,从JWT中获取当前用户ID + if req.UserId == 0 { + req.UserId = uint(utils.GetUserID(c)) + } + + userExam, err := service.ServiceGroupApp.LearningServiceGroup.UserExamService.StartExam(c.Request.Context(), req.UserId, req.ExamId) + if err != nil { + global.GVA_LOG.Error("开始考试失败!", zap.Error(err)) + response.FailWithMessage(err.Error(), c) + return + } + response.OkWithData(userExam, c) +} + +// SubmitExam 提交考试 +// @Tags UserExam +// @Summary 提交考试 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body learningReq.SubmitExamRequest true "提交考试请求" +// @Success 200 {object} response.Response{msg=string} "提交成功" +// @Router /userExam/submitExam [put] +func (userExamApi *UserExamApi) SubmitExam(c *gin.Context) { + var req learningReq.SubmitExamRequest + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + err = service.ServiceGroupApp.LearningServiceGroup.UserExamService.SubmitExam(c.Request.Context(), req.UserExamId, req.TotalScore, req.CorrectCount, req.WrongCount) + if err != nil { + global.GVA_LOG.Error("提交考试失败!", zap.Error(err)) + response.FailWithMessage(err.Error(), c) + return + } + response.OkWithMessage("提交成功", c) +} + +// GetUserExamHistory 获取用户考试历史 +// @Tags UserExam +// @Summary 获取用户考试历史 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param userId query uint false "用户ID" +// @Success 200 {object} response.Response{data=[]learning.UserExam,msg=string} "获取成功" +// @Router /userExam/getUserExamHistory [get] +func (userExamApi *UserExamApi) GetUserExamHistory(c *gin.Context) { + userIdStr := c.Query("userId") + + var userId uint + if userIdStr != "" { + userIdUint, err := strconv.ParseUint(userIdStr, 10, 32) + if err != nil { + response.FailWithMessage("用户ID格式错误", c) + return + } + userId = uint(userIdUint) + } else { + // 如果没有提供userId,从JWT中获取当前用户ID + userId = uint(utils.GetUserID(c)) + } + + list, err := service.ServiceGroupApp.LearningServiceGroup.UserExamService.GetUserExamHistory(c.Request.Context(), userId) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithData(list, c) +} + +// GetUserExamStatistics 获取用户考试统计 +// @Tags UserExam +// @Summary 获取用户考试统计 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param userId query uint false "用户ID" +// @Success 200 {object} response.Response{data=learningReq.UserExamStatistics,msg=string} "获取成功" +// @Router /userExam/getUserExamStatistics [get] +func (userExamApi *UserExamApi) GetUserExamStatistics(c *gin.Context) { + userIdStr := c.Query("userId") + + var userId uint + if userIdStr != "" { + userIdUint, err := strconv.ParseUint(userIdStr, 10, 32) + if err != nil { + response.FailWithMessage("用户ID格式错误", c) + return + } + userId = uint(userIdUint) + } else { + // 如果没有提供userId,从JWT中获取当前用户ID + userId = uint(utils.GetUserID(c)) + } + + stats, err := service.ServiceGroupApp.LearningServiceGroup.UserExamService.GetUserExamStatistics(c.Request.Context(), userId) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithData(stats, c) +} + +// GetExamRanking 获取考试排行榜 +// @Tags UserExam +// @Summary 获取考试排行榜 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param examId query uint true "考试ID" +// @Param limit query int false "限制数量" +// @Success 200 {object} response.Response{data=learningReq.ExamRankingResponse,msg=string} "获取成功" +// @Router /userExam/getExamRanking [get] +func (userExamApi *UserExamApi) GetExamRanking(c *gin.Context) { + examIdStr := c.Query("examId") + limitStr := c.Query("limit") + + examId, err := strconv.ParseUint(examIdStr, 10, 32) + if err != nil { + response.FailWithMessage("考试ID格式错误", c) + return + } + + limit := 10 // 默认限制10条 + if limitStr != "" { + if l, err := strconv.Atoi(limitStr); err == nil { + limit = l + } + } + + ranking, err := service.ServiceGroupApp.LearningServiceGroup.UserExamService.GetExamRanking(c.Request.Context(), uint(examId), limit) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithData(ranking, c) +} diff --git a/server/api/v1/learning/user_learning.go b/server/api/v1/learning/user_learning.go new file mode 100644 index 00000000..fa044e7e --- /dev/null +++ b/server/api/v1/learning/user_learning.go @@ -0,0 +1,262 @@ +package learning + +import ( + "strconv" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/learning" + learningReq "github.com/flipped-aurora/gin-vue-admin/server/model/learning/request" + "github.com/flipped-aurora/gin-vue-admin/server/service" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type UserLearningApi struct{} + +// CreateUserLearning 创建用户学习记录 +// @Tags UserLearning +// @Summary 创建用户学习记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body learningReq.UserLearningCreate true "创建用户学习记录" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /userLearning/createUserLearning [post] +func (userLearningApi *UserLearningApi) CreateUserLearning(c *gin.Context) { + var req learningReq.UserLearningCreate + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + userLearning := learning.UserLearning{ + UserId: req.UserId, + CourseId: req.CourseId, + ChapterId: req.ChapterId, + KnowledgePointId: req.KnowledgePointId, + Progress: req.Progress, + StudyTime: req.StudyTime, + LastStudyAt: req.LastStudyAt, + Status: req.Status, + Score: req.Score, + CompletedAt: req.CompletedAt, + } + + err = service.ServiceGroupApp.LearningServiceGroup.UserLearningService.CreateUserLearning(c.Request.Context(), &userLearning) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败", c) + return + } + response.OkWithMessage("创建成功", c) +} + +// DeleteUserLearning 删除用户学习记录 +// @Tags UserLearning +// @Summary 删除用户学习记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除用户学习记录" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /userLearning/deleteUserLearning [delete] +func (userLearningApi *UserLearningApi) DeleteUserLearning(c *gin.Context) { + ID := c.Query("ID") + err := service.ServiceGroupApp.LearningServiceGroup.UserLearningService.DeleteUserLearning(c.Request.Context(), ID) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// DeleteUserLearningByIds 批量删除用户学习记录 +// @Tags UserLearning +// @Summary 批量删除用户学习记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /userLearning/deleteUserLearningByIds [delete] +func (userLearningApi *UserLearningApi) DeleteUserLearningByIds(c *gin.Context) { + IDs := c.QueryArray("IDs[]") + err := service.ServiceGroupApp.LearningServiceGroup.UserLearningService.DeleteUserLearningByIds(c.Request.Context(), IDs) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败", c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// UpdateUserLearning 更新用户学习记录 +// @Tags UserLearning +// @Summary 更新用户学习记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body learningReq.UserLearningUpdate true "更新用户学习记录" +// @Success 200 {object} response.Response{msg=string} "更新成功" +// @Router /userLearning/updateUserLearning [put] +func (userLearningApi *UserLearningApi) UpdateUserLearning(c *gin.Context) { + var req learningReq.UserLearningUpdate + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + userLearning := learning.UserLearning{ + GVA_MODEL: global.GVA_MODEL{ID: req.ID}, + UserId: req.UserId, + CourseId: req.CourseId, + ChapterId: req.ChapterId, + KnowledgePointId: req.KnowledgePointId, + Progress: req.Progress, + StudyTime: req.StudyTime, + LastStudyAt: req.LastStudyAt, + Status: req.Status, + Score: req.Score, + CompletedAt: req.CompletedAt, + } + + err = service.ServiceGroupApp.LearningServiceGroup.UserLearningService.UpdateUserLearning(c.Request.Context(), userLearning) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败", c) + return + } + response.OkWithMessage("更新成功", c) +} + +// FindUserLearning 用id查询用户学习记录 +// @Tags UserLearning +// @Summary 用id查询用户学习记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query learning.UserLearning true "用id查询用户学习记录" +// @Success 200 {object} response.Response{data=learning.UserLearning,msg=string} "查询成功" +// @Router /userLearning/findUserLearning [get] +func (userLearningApi *UserLearningApi) FindUserLearning(c *gin.Context) { + ID := c.Query("ID") + reuserLearning, err := service.ServiceGroupApp.LearningServiceGroup.UserLearningService.GetUserLearning(c.Request.Context(), ID) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败", c) + return + } + response.OkWithData(reuserLearning, c) +} + +// GetUserLearningList 分页获取用户学习记录列表 +// @Tags UserLearning +// @Summary 分页获取用户学习记录列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query learningReq.UserLearningSearch true "分页获取用户学习记录列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /userLearning/getUserLearningList [get] +func (userLearningApi *UserLearningApi) GetUserLearningList(c *gin.Context) { + var pageInfo learningReq.UserLearningSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := service.ServiceGroupApp.LearningServiceGroup.UserLearningService.GetUserLearningInfoList(c.Request.Context(), pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} + +// GetUserProgress 获取用户学习进度 +// @Tags UserLearning +// @Summary 获取用户学习进度 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param userId query uint true "用户ID" +// @Param courseId query uint false "课程ID" +// @Success 200 {object} response.Response{data=[]learning.UserLearning,msg=string} "获取成功" +// @Router /userLearning/getUserProgress [get] +func (userLearningApi *UserLearningApi) GetUserProgress(c *gin.Context) { + userIdStr := c.Query("userId") + courseIdStr := c.Query("courseId") + + userId, err := strconv.ParseUint(userIdStr, 10, 32) + if err != nil { + response.FailWithMessage("用户ID格式错误", c) + return + } + + var courseId uint = 0 + if courseIdStr != "" { + courseIdUint, err := strconv.ParseUint(courseIdStr, 10, 32) + if err != nil { + response.FailWithMessage("课程ID格式错误", c) + return + } + courseId = uint(courseIdUint) + } + + // 如果没有提供userId,尝试从JWT中获取当前用户ID + if userId == 0 { + userId = uint64(utils.GetUserID(c)) + } + + list, err := service.ServiceGroupApp.LearningServiceGroup.UserLearningService.GetUserProgress(c.Request.Context(), uint(userId), courseId) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithData(list, c) +} + +// GetLearningStatistics 获取用户学习统计 +// @Tags UserLearning +// @Summary 获取用户学习统计 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param userId query uint false "用户ID" +// @Success 200 {object} response.Response{data=learningReq.LearningStatistics,msg=string} "获取成功" +// @Router /userLearning/getLearningStatistics [get] +func (userLearningApi *UserLearningApi) GetLearningStatistics(c *gin.Context) { + userIdStr := c.Query("userId") + + var userId uint + if userIdStr != "" { + userIdUint, err := strconv.ParseUint(userIdStr, 10, 32) + if err != nil { + response.FailWithMessage("用户ID格式错误", c) + return + } + userId = uint(userIdUint) + } else { + // 如果没有提供userId,从JWT中获取当前用户ID + userId = uint(utils.GetUserID(c)) + } + + stats, err := service.ServiceGroupApp.LearningServiceGroup.UserLearningService.GetLearningStatistics(c.Request.Context(), userId) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithData(stats, c) +} diff --git a/server/api/v1/learning/wrong_question.go b/server/api/v1/learning/wrong_question.go new file mode 100644 index 00000000..5e8fbcba --- /dev/null +++ b/server/api/v1/learning/wrong_question.go @@ -0,0 +1,278 @@ +package learning + +import ( + "strconv" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/learning" + learningReq "github.com/flipped-aurora/gin-vue-admin/server/model/learning/request" + "github.com/flipped-aurora/gin-vue-admin/server/service" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type WrongQuestionApi struct{} + +// CreateWrongQuestion 创建错题记录 +// @Tags WrongQuestion +// @Summary 创建错题记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body learningReq.WrongQuestionCreate true "创建错题记录" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /wrongQuestion/createWrongQuestion [post] +func (wrongQuestionApi *WrongQuestionApi) CreateWrongQuestion(c *gin.Context) { + var req learningReq.WrongQuestionCreate + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + wrongQuestion := learning.WrongQuestion{ + UserId: req.UserId, + QuestionId: req.QuestionId, + WrongAnswer: req.WrongAnswer, + WrongCount: req.WrongCount, + LastWrongAt: req.LastWrongAt, + IsMastered: req.IsMastered, + MasteredAt: req.MasteredAt, + Notes: req.Notes, + } + + err = service.ServiceGroupApp.LearningServiceGroup.WrongQuestionService.CreateWrongQuestion(c.Request.Context(), &wrongQuestion) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败", c) + return + } + response.OkWithMessage("创建成功", c) +} + +// DeleteWrongQuestion 删除错题记录 +// @Tags WrongQuestion +// @Summary 删除错题记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除错题记录" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /wrongQuestion/deleteWrongQuestion [delete] +func (wrongQuestionApi *WrongQuestionApi) DeleteWrongQuestion(c *gin.Context) { + ID := c.Query("ID") + err := service.ServiceGroupApp.LearningServiceGroup.WrongQuestionService.DeleteWrongQuestion(c.Request.Context(), ID) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// DeleteWrongQuestionByIds 批量删除错题记录 +// @Tags WrongQuestion +// @Summary 批量删除错题记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /wrongQuestion/deleteWrongQuestionByIds [delete] +func (wrongQuestionApi *WrongQuestionApi) DeleteWrongQuestionByIds(c *gin.Context) { + IDs := c.QueryArray("IDs[]") + err := service.ServiceGroupApp.LearningServiceGroup.WrongQuestionService.DeleteWrongQuestionByIds(c.Request.Context(), IDs) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败", c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// UpdateWrongQuestion 更新错题记录 +// @Tags WrongQuestion +// @Summary 更新错题记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body learningReq.WrongQuestionUpdate true "更新错题记录" +// @Success 200 {object} response.Response{msg=string} "更新成功" +// @Router /wrongQuestion/updateWrongQuestion [put] +func (wrongQuestionApi *WrongQuestionApi) UpdateWrongQuestion(c *gin.Context) { + var req learningReq.WrongQuestionUpdate + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + wrongQuestion := learning.WrongQuestion{ + GVA_MODEL: global.GVA_MODEL{ID: req.ID}, + UserId: req.UserId, + QuestionId: req.QuestionId, + WrongAnswer: req.WrongAnswer, + WrongCount: req.WrongCount, + LastWrongAt: req.LastWrongAt, + IsMastered: req.IsMastered, + MasteredAt: req.MasteredAt, + Notes: req.Notes, + } + + err = service.ServiceGroupApp.LearningServiceGroup.WrongQuestionService.UpdateWrongQuestion(c.Request.Context(), wrongQuestion) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败", c) + return + } + response.OkWithMessage("更新成功", c) +} + +// FindWrongQuestion 用id查询错题记录 +// @Tags WrongQuestion +// @Summary 用id查询错题记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query learning.WrongQuestion true "用id查询错题记录" +// @Success 200 {object} response.Response{data=learning.WrongQuestion,msg=string} "查询成功" +// @Router /wrongQuestion/findWrongQuestion [get] +func (wrongQuestionApi *WrongQuestionApi) FindWrongQuestion(c *gin.Context) { + ID := c.Query("ID") + rewrongQuestion, err := service.ServiceGroupApp.LearningServiceGroup.WrongQuestionService.GetWrongQuestion(c.Request.Context(), ID) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败", c) + return + } + response.OkWithData(rewrongQuestion, c) +} + +// GetWrongQuestionList 分页获取错题记录列表 +// @Tags WrongQuestion +// @Summary 分页获取错题记录列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query learningReq.WrongQuestionSearch true "分页获取错题记录列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /wrongQuestion/getWrongQuestionList [get] +func (wrongQuestionApi *WrongQuestionApi) GetWrongQuestionList(c *gin.Context) { + var pageInfo learningReq.WrongQuestionSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := service.ServiceGroupApp.LearningServiceGroup.WrongQuestionService.GetWrongQuestionInfoList(c.Request.Context(), pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} + +// GetUserWrongQuestions 获取用户错题列表 +// @Tags WrongQuestion +// @Summary 获取用户错题列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param userId query uint false "用户ID" +// @Param onlyUnmastered query bool false "只获取未掌握的题目" +// @Success 200 {object} response.Response{data=[]learning.WrongQuestion,msg=string} "获取成功" +// @Router /wrongQuestion/getUserWrongQuestions [get] +func (wrongQuestionApi *WrongQuestionApi) GetUserWrongQuestions(c *gin.Context) { + userIdStr := c.Query("userId") + onlyUnmasteredStr := c.Query("onlyUnmastered") + + var userId uint + if userIdStr != "" { + userIdUint, err := strconv.ParseUint(userIdStr, 10, 32) + if err != nil { + response.FailWithMessage("用户ID格式错误", c) + return + } + userId = uint(userIdUint) + } else { + // 如果没有提供userId,从JWT中获取当前用户ID + userId = uint(utils.GetUserID(c)) + } + + onlyUnmastered := onlyUnmasteredStr == "true" + + list, err := service.ServiceGroupApp.LearningServiceGroup.WrongQuestionService.GetUserWrongQuestions(c.Request.Context(), userId, onlyUnmastered) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithData(list, c) +} + +// MarkAsMastered 标记题目为已掌握 +// @Tags WrongQuestion +// @Summary 标记题目为已掌握 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body learningReq.MarkMasteredRequest true "标记掌握请求" +// @Success 200 {object} response.Response{msg=string} "标记成功" +// @Router /wrongQuestion/markAsMastered [put] +func (wrongQuestionApi *WrongQuestionApi) MarkAsMastered(c *gin.Context) { + var req learningReq.MarkMasteredRequest + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + err = service.ServiceGroupApp.LearningServiceGroup.WrongQuestionService.MarkAsMastered(c.Request.Context(), req.UserId, req.ID) + if err != nil { + global.GVA_LOG.Error("标记失败!", zap.Error(err)) + response.FailWithMessage("标记失败", c) + return + } + response.OkWithMessage("标记成功", c) +} + +// GetWrongQuestionStatistics 获取错题统计 +// @Tags WrongQuestion +// @Summary 获取错题统计 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param userId query uint false "用户ID" +// @Success 200 {object} response.Response{data=learningReq.WrongQuestionStatistics,msg=string} "获取成功" +// @Router /wrongQuestion/getWrongQuestionStatistics [get] +func (wrongQuestionApi *WrongQuestionApi) GetWrongQuestionStatistics(c *gin.Context) { + userIdStr := c.Query("userId") + + var userId uint + if userIdStr != "" { + userIdUint, err := strconv.ParseUint(userIdStr, 10, 32) + if err != nil { + response.FailWithMessage("用户ID格式错误", c) + return + } + userId = uint(userIdUint) + } else { + // 如果没有提供userId,从JWT中获取当前用户ID + userId = uint(utils.GetUserID(c)) + } + + stats, err := service.ServiceGroupApp.LearningServiceGroup.WrongQuestionService.GetWrongQuestionStatistics(c.Request.Context(), userId) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithData(stats, c) +} diff --git a/server/initialize/gorm_biz.go b/server/initialize/gorm_biz.go index 9316ccc8..d31420c1 100644 --- a/server/initialize/gorm_biz.go +++ b/server/initialize/gorm_biz.go @@ -2,11 +2,22 @@ package initialize import ( "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/learning" ) func bizModel() error { db := global.GVA_DB - err := db.AutoMigrate() + err := db.AutoMigrate( + // Learning 学习系统模型 + learning.Course{}, + learning.Chapter{}, + learning.KnowledgePoint{}, + learning.Question{}, + learning.UserLearning{}, + learning.WrongQuestion{}, + learning.Exam{}, + learning.UserExam{}, + ) if err != nil { return err } diff --git a/server/initialize/router_biz.go b/server/initialize/router_biz.go index 279127d3..d6d2e9a9 100644 --- a/server/initialize/router_biz.go +++ b/server/initialize/router_biz.go @@ -15,5 +15,18 @@ func initBizRouter(routers ...*gin.RouterGroup) { privateGroup := routers[0] publicGroup := routers[1] + // Learning 学习系统路由 + learningRouter := router.RouterGroupApp.Learning + { + learningRouter.InitCourseRouter(privateGroup, publicGroup) // 课程路由 + learningRouter.InitChapterRouter(privateGroup, publicGroup) // 章节路由 + learningRouter.InitKnowledgePointRouter(privateGroup, publicGroup) // 知识点路由 + learningRouter.InitQuestionRouter(privateGroup, publicGroup) // 题目路由 + learningRouter.InitUserLearningRouter(privateGroup, publicGroup) // 用户学习记录路由 + learningRouter.InitWrongQuestionRouter(privateGroup, publicGroup) // 错题本路由 + learningRouter.InitExamRouter(privateGroup, publicGroup) // 考试路由 + learningRouter.InitUserExamRouter(privateGroup, publicGroup) // 用户考试记录路由 + } + holder(publicGroup, privateGroup) } diff --git a/server/model/learning/chapter.go b/server/model/learning/chapter.go new file mode 100644 index 00000000..359c83d1 --- /dev/null +++ b/server/model/learning/chapter.go @@ -0,0 +1,24 @@ +package learning + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// Chapter 章节模型 +type Chapter struct { + global.GVA_MODEL + CourseId uint `json:"courseId" form:"courseId" gorm:"column:course_id;comment:关联课程ID;not null;" binding:"required"` + Title string `json:"title" form:"title" gorm:"column:title;comment:章节标题;size:255;not null;" binding:"required"` + Description string `json:"description" form:"description" gorm:"column:description;comment:章节描述;type:text;"` + Sort int `json:"sort" form:"sort" gorm:"column:sort;comment:排序号;default:0;"` + Status string `json:"status" form:"status" gorm:"column:status;comment:章节状态;size:50;default:draft;"` + Duration int `json:"duration" form:"duration" gorm:"column:duration;comment:预计学习时长(分钟);default:0;"` + + // 关联关系 - 去掉外键约束,只保留逻辑关联 + Course Course `json:"course" gorm:"-"` +} + +// TableName 设置表名 +func (Chapter) TableName() string { + return "learning_chapters" +} diff --git a/server/model/learning/course.go b/server/model/learning/course.go new file mode 100644 index 00000000..68cabb09 --- /dev/null +++ b/server/model/learning/course.go @@ -0,0 +1,24 @@ +package learning + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// Course 课程模型 +type Course struct { + global.GVA_MODEL + Title string `json:"title" form:"title" gorm:"column:title;comment:课程标题;size:255;not null;" binding:"required"` + Description string `json:"description" form:"description" gorm:"column:description;comment:课程描述;type:longtext;"` + Cover string `json:"cover" form:"cover" gorm:"column:cover;comment:课程封面图片URL;size:500;"` + Difficulty string `json:"difficulty" form:"difficulty" gorm:"column:difficulty;comment:难度等级;size:50;default:beginner;"` + Duration int `json:"duration" form:"duration" gorm:"column:duration;comment:预计学时(分钟);default:0;"` + Status string `json:"status" form:"status" gorm:"column:status;comment:课程状态;size:50;default:draft;"` + Price float64 `json:"price" form:"price" gorm:"column:price;comment:课程价格;type:decimal(10,2);default:0.00;"` + Tags string `json:"tags" form:"tags" gorm:"column:tags;comment:课程标签,逗号分隔;size:500;"` + Sort int `json:"sort" form:"sort" gorm:"column:sort;comment:排序权重;default:0;"` +} + +// TableName 设置表名 +func (Course) TableName() string { + return "learning_courses" +} diff --git a/server/model/learning/exam.go b/server/model/learning/exam.go new file mode 100644 index 00000000..232aa515 --- /dev/null +++ b/server/model/learning/exam.go @@ -0,0 +1,32 @@ +package learning + +import ( + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// Exam 考试模型 +type Exam struct { + global.GVA_MODEL + Title string `json:"title" form:"title" gorm:"column:title;comment:考试标题;size:255;not null;" binding:"required"` + Description string `json:"description" form:"description" gorm:"column:description;comment:考试描述;type:longtext;"` + CourseId uint `json:"courseId" form:"courseId" gorm:"column:course_id;comment:关联课程ID;"` + Duration int `json:"duration" form:"duration" gorm:"column:duration;comment:考试时长(分钟);not null;" binding:"required"` + TotalScore int `json:"totalScore" form:"totalScore" gorm:"column:total_score;comment:总分;not null;" binding:"required"` + PassScore int `json:"passScore" form:"passScore" gorm:"column:pass_score;comment:及格分数;not null;" binding:"required"` + QuestionCount int `json:"questionCount" form:"questionCount" gorm:"column:question_count;comment:题目数量;not null;" binding:"required"` + StartTime time.Time `json:"startTime" form:"startTime" gorm:"column:start_time;comment:开始时间;"` + EndTime time.Time `json:"endTime" form:"endTime" gorm:"column:end_time;comment:结束时间;"` + Status string `json:"status" form:"status" gorm:"column:status;comment:考试状态;size:50;default:draft;"` + AllowRetake bool `json:"allowRetake" form:"allowRetake" gorm:"column:allow_retake;comment:是否允许重考;default:false;"` + MaxAttempts int `json:"maxAttempts" form:"maxAttempts" gorm:"column:max_attempts;comment:最大尝试次数;default:1;"` + + // 关联关系 - 去掉外键约束,只保留逻辑关联 + Course Course `json:"course" gorm:"-"` +} + +// TableName 设置表名 +func (Exam) TableName() string { + return "learning_exams" +} diff --git a/server/model/learning/knowledge_point.go b/server/model/learning/knowledge_point.go new file mode 100644 index 00000000..ac572b45 --- /dev/null +++ b/server/model/learning/knowledge_point.go @@ -0,0 +1,26 @@ +package learning + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// KnowledgePoint 知识点模型 +type KnowledgePoint struct { + global.GVA_MODEL + ChapterId uint `json:"chapterId" form:"chapterId" gorm:"column:chapter_id;comment:关联章节ID;not null;" binding:"required"` + Title string `json:"title" form:"title" gorm:"column:title;comment:知识点标题;size:255;not null;" binding:"required"` + Content string `json:"content" form:"content" gorm:"column:content;comment:知识点内容;type:longtext;"` + VideoUrl string `json:"videoUrl" form:"videoUrl" gorm:"column:video_url;comment:视频链接;size:500;"` + Attachments string `json:"attachments" form:"attachments" gorm:"column:attachments;comment:附件信息JSON;type:text;"` + Sort int `json:"sort" form:"sort" gorm:"column:sort;comment:排序号;default:0;"` + Duration int `json:"duration" form:"duration" gorm:"column:duration;comment:预计学习时长(分钟);default:0;"` + Status string `json:"status" form:"status" gorm:"column:status;comment:知识点状态;size:50;default:draft;"` + + // 关联关系 - 去掉外键约束,只保留逻辑关联 + Chapter Chapter `json:"chapter" gorm:"-"` +} + +// TableName 设置表名 +func (KnowledgePoint) TableName() string { + return "learning_knowledge_points" +} diff --git a/server/model/learning/question.go b/server/model/learning/question.go new file mode 100644 index 00000000..38a8169e --- /dev/null +++ b/server/model/learning/question.go @@ -0,0 +1,27 @@ +package learning + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// Question 题目模型 +type Question struct { + global.GVA_MODEL + KnowledgePointId uint `json:"knowledgePointId" form:"knowledgePointId" gorm:"column:knowledge_point_id;comment:关联知识点ID;not null;" binding:"required"` + Title string `json:"title" form:"title" gorm:"column:title;comment:题目标题;size:255;not null;" binding:"required"` + Content string `json:"content" form:"content" gorm:"column:content;comment:题目内容;type:longtext;"` + Type string `json:"type" form:"type" gorm:"column:type;comment:题目类型;size:50;not null;" binding:"required"` + Options string `json:"options" form:"options" gorm:"column:options;comment:选项内容JSON;type:text;"` + Answer string `json:"answer" form:"answer" gorm:"column:answer;comment:正确答案;type:text;not null;" binding:"required"` + Explanation string `json:"explanation" form:"explanation" gorm:"column:explanation;comment:答案解析;type:longtext;"` + Difficulty string `json:"difficulty" form:"difficulty" gorm:"column:difficulty;comment:难度等级;size:50;default:medium;"` + Score int `json:"score" form:"score" gorm:"column:score;comment:题目分值;default:5;"` + + // 关联关系 - 去掉外键约束,只保留逻辑关联 + KnowledgePoint KnowledgePoint `json:"knowledgePoint" gorm:"-"` +} + +// TableName 设置表名 +func (Question) TableName() string { + return "learning_questions" +} diff --git a/server/model/learning/request/chapter.go b/server/model/learning/request/chapter.go new file mode 100644 index 00000000..266c49d0 --- /dev/null +++ b/server/model/learning/request/chapter.go @@ -0,0 +1,36 @@ +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/learning" +) + +type ChapterSearch struct { + learning.Chapter + request.PageInfo + StartCreatedAt *string `json:"startCreatedAt" form:"startCreatedAt"` + EndCreatedAt *string `json:"endCreatedAt" form:"endCreatedAt"` +} + +type ChapterCreate struct { + CourseId uint `json:"courseId" binding:"required"` + Title string `json:"title" binding:"required"` + Description string `json:"description"` + Sort int `json:"sort"` + Status string `json:"status"` + Duration int `json:"duration"` +} + +type ChapterUpdate struct { + ID uint `json:"id" binding:"required"` + CourseId uint `json:"courseId" binding:"required"` + Title string `json:"title" binding:"required"` + Description string `json:"description"` + Sort int `json:"sort"` + Status string `json:"status"` + Duration int `json:"duration"` +} + +type ChaptersByCourseRequest struct { + CourseId uint `json:"courseId" form:"courseId" binding:"required"` +} diff --git a/server/model/learning/request/course.go b/server/model/learning/request/course.go new file mode 100644 index 00000000..31701a61 --- /dev/null +++ b/server/model/learning/request/course.go @@ -0,0 +1,38 @@ +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/learning" +) + +type CourseSearch struct { + learning.Course + request.PageInfo + StartCreatedAt *string `json:"startCreatedAt" form:"startCreatedAt"` + EndCreatedAt *string `json:"endCreatedAt" form:"endCreatedAt"` +} + +type CourseCreate struct { + Title string `json:"title" binding:"required"` + Description string `json:"description"` + Cover string `json:"cover"` + Difficulty string `json:"difficulty"` + Duration int `json:"duration"` + Status string `json:"status"` + Price float64 `json:"price"` + Tags string `json:"tags"` + Sort int `json:"sort"` +} + +type CourseUpdate struct { + ID uint `json:"id" binding:"required"` + Title string `json:"title" binding:"required"` + Description string `json:"description"` + Cover string `json:"cover"` + Difficulty string `json:"difficulty"` + Duration int `json:"duration"` + Status string `json:"status"` + Price float64 `json:"price"` + Tags string `json:"tags"` + Sort int `json:"sort"` +} diff --git a/server/model/learning/request/exam.go b/server/model/learning/request/exam.go new file mode 100644 index 00000000..66f252e2 --- /dev/null +++ b/server/model/learning/request/exam.go @@ -0,0 +1,64 @@ +package request + +import ( + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/learning" +) + +type ExamSearch struct { + learning.Exam + request.PageInfo + StartCreatedAt *string `json:"startCreatedAt" form:"startCreatedAt"` + EndCreatedAt *string `json:"endCreatedAt" form:"endCreatedAt"` +} + +type ExamCreate struct { + Title string `json:"title" binding:"required"` + Description string `json:"description"` + CourseId uint `json:"courseId"` + Duration int `json:"duration" binding:"required"` + TotalScore int `json:"totalScore" binding:"required"` + PassScore int `json:"passScore" binding:"required"` + QuestionCount int `json:"questionCount" binding:"required"` + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` + Status string `json:"status"` + AllowRetake bool `json:"allowRetake"` + MaxAttempts int `json:"maxAttempts"` +} + +type ExamUpdate struct { + ID uint `json:"id" binding:"required"` + Title string `json:"title" binding:"required"` + Description string `json:"description"` + CourseId uint `json:"courseId"` + Duration int `json:"duration" binding:"required"` + TotalScore int `json:"totalScore" binding:"required"` + PassScore int `json:"passScore" binding:"required"` + QuestionCount int `json:"questionCount" binding:"required"` + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` + Status string `json:"status"` + AllowRetake bool `json:"allowRetake"` + MaxAttempts int `json:"maxAttempts"` +} + +type ExamStatistics struct { + TotalExams int `json:"totalExams"` // 总考试数 + ActiveExams int `json:"activeExams"` // 进行中的考试数 + CompletedExams int `json:"completedExams"` // 已结束的考试数 + DraftExams int `json:"draftExams"` // 草稿考试数 +} + +type PublishExamRequest struct { + ID uint `json:"id" binding:"required"` +} + +type ExamQuestionConfig struct { + ExamId uint `json:"examId" binding:"required"` + QuestionId uint `json:"questionId" binding:"required"` + Score int `json:"score" binding:"required"` + QuestionOrder int `json:"questionOrder"` +} diff --git a/server/model/learning/request/knowledge_point.go b/server/model/learning/request/knowledge_point.go new file mode 100644 index 00000000..38ddc82b --- /dev/null +++ b/server/model/learning/request/knowledge_point.go @@ -0,0 +1,40 @@ +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/learning" +) + +type KnowledgePointSearch struct { + learning.KnowledgePoint + request.PageInfo + StartCreatedAt *string `json:"startCreatedAt" form:"startCreatedAt"` + EndCreatedAt *string `json:"endCreatedAt" form:"endCreatedAt"` +} + +type KnowledgePointCreate struct { + ChapterId uint `json:"chapterId" binding:"required"` + Title string `json:"title" binding:"required"` + Content string `json:"content"` + VideoUrl string `json:"videoUrl"` + Attachments string `json:"attachments"` + Sort int `json:"sort"` + Duration int `json:"duration"` + Status string `json:"status"` +} + +type KnowledgePointUpdate struct { + ID uint `json:"id" binding:"required"` + ChapterId uint `json:"chapterId" binding:"required"` + Title string `json:"title" binding:"required"` + Content string `json:"content"` + VideoUrl string `json:"videoUrl"` + Attachments string `json:"attachments"` + Sort int `json:"sort"` + Duration int `json:"duration"` + Status string `json:"status"` +} + +type KnowledgePointsByChapterRequest struct { + ChapterId uint `json:"chapterId" form:"chapterId" binding:"required"` +} diff --git a/server/model/learning/request/question.go b/server/model/learning/request/question.go new file mode 100644 index 00000000..31697ba1 --- /dev/null +++ b/server/model/learning/request/question.go @@ -0,0 +1,48 @@ +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/learning" +) + +type QuestionSearch struct { + learning.Question + request.PageInfo + StartCreatedAt *string `json:"startCreatedAt" form:"startCreatedAt"` + EndCreatedAt *string `json:"endCreatedAt" form:"endCreatedAt"` +} + +type QuestionCreate struct { + KnowledgePointId uint `json:"knowledgePointId" binding:"required"` + Title string `json:"title" binding:"required"` + Content string `json:"content"` + Type string `json:"type" binding:"required"` + Options string `json:"options"` + Answer string `json:"answer" binding:"required"` + Explanation string `json:"explanation"` + Difficulty string `json:"difficulty"` + Score int `json:"score"` +} + +type QuestionUpdate struct { + ID uint `json:"id" binding:"required"` + KnowledgePointId uint `json:"knowledgePointId" binding:"required"` + Title string `json:"title" binding:"required"` + Content string `json:"content"` + Type string `json:"type" binding:"required"` + Options string `json:"options"` + Answer string `json:"answer" binding:"required"` + Explanation string `json:"explanation"` + Difficulty string `json:"difficulty"` + Score int `json:"score"` +} + +type QuestionsByKnowledgePointRequest struct { + KnowledgePointId uint `json:"knowledgePointId" form:"knowledgePointId" binding:"required"` +} + +// QuestionOption 题目选项结构 +type QuestionOption struct { + Key string `json:"key"` // 选项标识 A、B、C、D + Value string `json:"value"` // 选项内容 +} diff --git a/server/model/learning/request/user_exam.go b/server/model/learning/request/user_exam.go new file mode 100644 index 00000000..6a92b6fc --- /dev/null +++ b/server/model/learning/request/user_exam.go @@ -0,0 +1,85 @@ +package request + +import ( + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/learning" +) + +type UserExamSearch struct { + learning.UserExam + request.PageInfo + StartCreatedAt *string `json:"startCreatedAt" form:"startCreatedAt"` + EndCreatedAt *string `json:"endCreatedAt" form:"endCreatedAt"` +} + +type UserExamCreate struct { + UserId uint `json:"userId" binding:"required"` + ExamId uint `json:"examId" binding:"required"` + AttemptNumber int `json:"attemptNumber"` + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` + Duration int `json:"duration"` + TotalScore int `json:"totalScore"` + CorrectCount int `json:"correctCount"` + WrongCount int `json:"wrongCount"` + Status string `json:"status"` + IsPassed bool `json:"isPassed"` + SubmittedAt time.Time `json:"submittedAt"` +} + +type UserExamUpdate struct { + ID uint `json:"id" binding:"required"` + UserId uint `json:"userId" binding:"required"` + ExamId uint `json:"examId" binding:"required"` + AttemptNumber int `json:"attemptNumber"` + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` + Duration int `json:"duration"` + TotalScore int `json:"totalScore"` + CorrectCount int `json:"correctCount"` + WrongCount int `json:"wrongCount"` + Status string `json:"status"` + IsPassed bool `json:"isPassed"` + SubmittedAt time.Time `json:"submittedAt"` +} + +type StartExamRequest struct { + ExamId uint `json:"examId" binding:"required"` + UserId uint `json:"userId" binding:"required"` +} + +type SubmitExamRequest struct { + UserExamId uint `json:"userExamId" binding:"required"` + TotalScore int `json:"totalScore"` + CorrectCount int `json:"correctCount"` + WrongCount int `json:"wrongCount"` +} + +type UserExamStatistics struct { + TotalExams int `json:"totalExams"` // 总考试次数 + PassedExams int `json:"passedExams"` // 通过考试次数 + FailedExams int `json:"failedExams"` // 未通过考试次数 + AverageScore float64 `json:"averageScore"` // 平均分数 + HighestScore int `json:"highestScore"` // 最高分数 + TotalStudyTime int `json:"totalStudyTime"` // 总学习时间(分钟) + PassRate float64 `json:"passRate"` // 通过率 +} + +type ExamRankingItem struct { + UserId uint `json:"userId"` + UserName string `json:"userName"` + Score int `json:"score"` + Duration int `json:"duration"` + Rank int `json:"rank"` + IsPassed bool `json:"isPassed"` + SubmittedAt time.Time `json:"submittedAt"` +} + +type ExamRankingResponse struct { + ExamId uint `json:"examId"` + ExamTitle string `json:"examTitle"` + Rankings []ExamRankingItem `json:"rankings"` + Total int64 `json:"total"` +} diff --git a/server/model/learning/request/user_learning.go b/server/model/learning/request/user_learning.go new file mode 100644 index 00000000..3d78104d --- /dev/null +++ b/server/model/learning/request/user_learning.go @@ -0,0 +1,55 @@ +package request + +import ( + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/learning" +) + +type UserLearningSearch struct { + learning.UserLearning + request.PageInfo + StartCreatedAt *string `json:"startCreatedAt" form:"startCreatedAt"` + EndCreatedAt *string `json:"endCreatedAt" form:"endCreatedAt"` +} + +type UserLearningCreate struct { + UserId uint `json:"userId" binding:"required"` + CourseId uint `json:"courseId" binding:"required"` + ChapterId uint `json:"chapterId"` + KnowledgePointId uint `json:"knowledgePointId"` + Progress int `json:"progress"` + StudyTime int `json:"studyTime"` + LastStudyAt time.Time `json:"lastStudyAt"` + Status string `json:"status"` + Score int `json:"score"` + CompletedAt time.Time `json:"completedAt"` +} + +type UserLearningUpdate struct { + ID uint `json:"id" binding:"required"` + UserId uint `json:"userId" binding:"required"` + CourseId uint `json:"courseId" binding:"required"` + ChapterId uint `json:"chapterId"` + KnowledgePointId uint `json:"knowledgePointId"` + Progress int `json:"progress"` + StudyTime int `json:"studyTime"` + LastStudyAt time.Time `json:"lastStudyAt"` + Status string `json:"status"` + Score int `json:"score"` + CompletedAt time.Time `json:"completedAt"` +} + +type UserProgressRequest struct { + UserId uint `json:"userId" form:"userId" binding:"required"` + CourseId uint `json:"courseId" form:"courseId"` +} + +type LearningStatistics struct { + TotalCourses int `json:"totalCourses"` // 总课程数 + CompletedCourses int `json:"completedCourses"` // 已完成课程数 + InProgressCourses int `json:"inProgressCourses"` // 学习中课程数 + TotalStudyTime int `json:"totalStudyTime"` // 总学习时长 + AverageScore int `json:"averageScore"` // 平均分数 +} diff --git a/server/model/learning/request/wrong_question.go b/server/model/learning/request/wrong_question.go new file mode 100644 index 00000000..a4ec7417 --- /dev/null +++ b/server/model/learning/request/wrong_question.go @@ -0,0 +1,50 @@ +package request + +import ( + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/learning" +) + +type WrongQuestionSearch struct { + learning.WrongQuestion + request.PageInfo + StartCreatedAt *string `json:"startCreatedAt" form:"startCreatedAt"` + EndCreatedAt *string `json:"endCreatedAt" form:"endCreatedAt"` +} + +type WrongQuestionCreate struct { + UserId uint `json:"userId" binding:"required"` + QuestionId uint `json:"questionId" binding:"required"` + WrongAnswer string `json:"wrongAnswer"` + WrongCount int `json:"wrongCount"` + LastWrongAt time.Time `json:"lastWrongAt"` + IsMastered bool `json:"isMastered"` + MasteredAt time.Time `json:"masteredAt"` + Notes string `json:"notes"` +} + +type WrongQuestionUpdate struct { + ID uint `json:"id" binding:"required"` + UserId uint `json:"userId" binding:"required"` + QuestionId uint `json:"questionId" binding:"required"` + WrongAnswer string `json:"wrongAnswer"` + WrongCount int `json:"wrongCount"` + LastWrongAt time.Time `json:"lastWrongAt"` + IsMastered bool `json:"isMastered"` + MasteredAt time.Time `json:"masteredAt"` + Notes string `json:"notes"` +} + +type WrongQuestionStatistics struct { + TotalWrongQuestions int `json:"totalWrongQuestions"` // 总错题数 + MasteredQuestions int `json:"masteredQuestions"` // 已掌握题目数 + UnmasteredQuestions int `json:"unmasteredQuestions"` // 未掌握题目数 + AverageWrongCount int `json:"averageWrongCount"` // 平均错误次数 +} + +type MarkMasteredRequest struct { + ID uint `json:"id" binding:"required"` + UserId uint `json:"userId" binding:"required"` +} diff --git a/server/model/learning/user_exam.go b/server/model/learning/user_exam.go new file mode 100644 index 00000000..39a71ed6 --- /dev/null +++ b/server/model/learning/user_exam.go @@ -0,0 +1,32 @@ +package learning + +import ( + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// UserExam 用户考试记录模型 +type UserExam struct { + global.GVA_MODEL + UserId uint `json:"userId" form:"userId" gorm:"column:user_id;comment:关联用户ID;not null;" binding:"required"` + ExamId uint `json:"examId" form:"examId" gorm:"column:exam_id;comment:关联考试ID;not null;" binding:"required"` + AttemptNumber int `json:"attemptNumber" form:"attemptNumber" gorm:"column:attempt_number;comment:尝试次数;default:1;"` + StartTime time.Time `json:"startTime" form:"startTime" gorm:"column:start_time;comment:开始时间;"` + EndTime time.Time `json:"endTime" form:"endTime" gorm:"column:end_time;comment:结束时间;"` + Duration int `json:"duration" form:"duration" gorm:"column:duration;comment:实际用时(分钟);"` + TotalScore int `json:"totalScore" form:"totalScore" gorm:"column:total_score;comment:总得分;"` + CorrectCount int `json:"correctCount" form:"correctCount" gorm:"column:correct_count;comment:正确题目数;"` + WrongCount int `json:"wrongCount" form:"wrongCount" gorm:"column:wrong_count;comment:错误题目数;"` + Status string `json:"status" form:"status" gorm:"column:status;comment:状态;size:50;default:in_progress;"` + IsPassed bool `json:"isPassed" form:"isPassed" gorm:"column:is_passed;comment:是否通过;default:false;"` + SubmittedAt time.Time `json:"submittedAt" form:"submittedAt" gorm:"column:submitted_at;comment:提交时间;"` + + // 关联关系 - 去掉外键约束,只保留逻辑关联 + Exam Exam `json:"exam" gorm:"-"` +} + +// TableName 设置表名 +func (UserExam) TableName() string { + return "learning_user_exams" +} diff --git a/server/model/learning/user_learning.go b/server/model/learning/user_learning.go new file mode 100644 index 00000000..f74317a9 --- /dev/null +++ b/server/model/learning/user_learning.go @@ -0,0 +1,32 @@ +package learning + +import ( + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// UserLearning 用户学习记录模型 +type UserLearning struct { + global.GVA_MODEL + UserId uint `json:"userId" form:"userId" gorm:"column:user_id;comment:关联用户ID;not null;" binding:"required"` + CourseId uint `json:"courseId" form:"courseId" gorm:"column:course_id;comment:关联课程ID;not null;" binding:"required"` + ChapterId uint `json:"chapterId" form:"chapterId" gorm:"column:chapter_id;comment:当前学习章节ID;"` + KnowledgePointId uint `json:"knowledgePointId" form:"knowledgePointId" gorm:"column:knowledge_point_id;comment:当前学习知识点ID;"` + Progress int `json:"progress" form:"progress" gorm:"column:progress;comment:学习进度百分比;default:0;"` + StudyTime int `json:"studyTime" form:"studyTime" gorm:"column:study_time;comment:累计学习时长(分钟);default:0;"` + LastStudyAt time.Time `json:"lastStudyAt" form:"lastStudyAt" gorm:"column:last_study_at;comment:最后学习时间;"` + Status string `json:"status" form:"status" gorm:"column:status;comment:学习状态;size:50;default:not_started;"` + Score int `json:"score" form:"score" gorm:"column:score;comment:总分数;default:0;"` + CompletedAt time.Time `json:"completedAt" form:"completedAt" gorm:"column:completed_at;comment:完成时间;"` + + // 关联关系 - 去掉外键约束,只保留逻辑关联 + Course Course `json:"course" gorm:"-"` + Chapter Chapter `json:"chapter" gorm:"-"` + KnowledgePoint KnowledgePoint `json:"knowledgePoint" gorm:"-"` +} + +// TableName 设置表名 +func (UserLearning) TableName() string { + return "learning_user_learning" +} diff --git a/server/model/learning/wrong_question.go b/server/model/learning/wrong_question.go new file mode 100644 index 00000000..b6958ab5 --- /dev/null +++ b/server/model/learning/wrong_question.go @@ -0,0 +1,28 @@ +package learning + +import ( + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// WrongQuestion 错题本模型 +type WrongQuestion struct { + global.GVA_MODEL + UserId uint `json:"userId" form:"userId" gorm:"column:user_id;comment:关联用户ID;not null;" binding:"required"` + QuestionId uint `json:"questionId" form:"questionId" gorm:"column:question_id;comment:关联题目ID;not null;" binding:"required"` + WrongAnswer string `json:"wrongAnswer" form:"wrongAnswer" gorm:"column:wrong_answer;comment:错误答案;type:text;"` + WrongCount int `json:"wrongCount" form:"wrongCount" gorm:"column:wrong_count;comment:错误次数;default:1;"` + LastWrongAt time.Time `json:"lastWrongAt" form:"lastWrongAt" gorm:"column:last_wrong_at;comment:最后错误时间;"` + IsMastered bool `json:"isMastered" form:"isMastered" gorm:"column:is_mastered;comment:是否已掌握;default:false;"` + MasteredAt time.Time `json:"masteredAt" form:"masteredAt" gorm:"column:mastered_at;comment:掌握时间;"` + Notes string `json:"notes" form:"notes" gorm:"column:notes;comment:用户笔记;type:longtext;"` + + // 关联关系 - 去掉外键约束,只保留逻辑关联 + Question Question `json:"question" gorm:"-"` +} + +// TableName 设置表名 +func (WrongQuestion) TableName() string { + return "learning_wrong_questions" +} diff --git a/server/router/enter.go b/server/router/enter.go index 6e6d8116..b4a39c60 100644 --- a/server/router/enter.go +++ b/server/router/enter.go @@ -2,12 +2,14 @@ package router import ( "github.com/flipped-aurora/gin-vue-admin/server/router/example" + "github.com/flipped-aurora/gin-vue-admin/server/router/learning" "github.com/flipped-aurora/gin-vue-admin/server/router/system" ) var RouterGroupApp = new(RouterGroup) type RouterGroup struct { - System system.RouterGroup - Example example.RouterGroup + System system.RouterGroup + Example example.RouterGroup + Learning learning.RouterGroup } diff --git a/server/router/learning/chapter.go b/server/router/learning/chapter.go new file mode 100644 index 00000000..3b95f640 --- /dev/null +++ b/server/router/learning/chapter.go @@ -0,0 +1,31 @@ +package learning + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/api/v1" + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type ChapterRouter struct{} + +// InitChapterRouter 初始化 章节 路由信息 +func (s *ChapterRouter) InitChapterRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) { + chapterRouter := Router.Group("chapter").Use(middleware.OperationRecord()) + chapterRouterWithoutRecord := Router.Group("chapter") + chapterRouterWithoutAuth := PublicRouter.Group("chapter") + + var chapterApi = v1.ApiGroupApp.LearningApiGroup.ChapterApi + { + chapterRouter.POST("createChapter", chapterApi.CreateChapter) // 新建章节 + chapterRouter.DELETE("deleteChapter", chapterApi.DeleteChapter) // 删除章节 + chapterRouter.DELETE("deleteChapterByIds", chapterApi.DeleteChapterByIds) // 批量删除章节 + chapterRouter.PUT("updateChapter", chapterApi.UpdateChapter) // 更新章节 + } + { + chapterRouterWithoutRecord.GET("findChapter", chapterApi.FindChapter) // 根据ID获取章节 + chapterRouterWithoutRecord.GET("getChapterList", chapterApi.GetChapterList) // 获取章节列表 + } + { + chapterRouterWithoutAuth.GET("getChaptersByCourse", chapterApi.GetChaptersByCourse) // 根据课程获取章节列表(无需认证) + } +} diff --git a/server/router/learning/course.go b/server/router/learning/course.go new file mode 100644 index 00000000..e4fcf921 --- /dev/null +++ b/server/router/learning/course.go @@ -0,0 +1,31 @@ +package learning + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/api/v1" + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type CourseRouter struct{} + +// InitCourseRouter 初始化 课程 路由信息 +func (s *CourseRouter) InitCourseRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) { + courseRouter := Router.Group("course").Use(middleware.OperationRecord()) + courseRouterWithoutRecord := Router.Group("course") + courseRouterWithoutAuth := PublicRouter.Group("course") + + var courseApi = v1.ApiGroupApp.LearningApiGroup.CourseApi + { + courseRouter.POST("createCourse", courseApi.CreateCourse) // 新建课程 + courseRouter.DELETE("deleteCourse", courseApi.DeleteCourse) // 删除课程 + courseRouter.DELETE("deleteCourseByIds", courseApi.DeleteCourseByIds) // 批量删除课程 + courseRouter.PUT("updateCourse", courseApi.UpdateCourse) // 更新课程 + } + { + courseRouterWithoutRecord.GET("findCourse", courseApi.FindCourse) // 根据ID获取课程 + courseRouterWithoutRecord.GET("getCourseList", courseApi.GetCourseList) // 获取课程列表 + } + { + courseRouterWithoutAuth.GET("getPublishedCourses", courseApi.GetPublishedCourses) // 获取已发布课程列表(无需认证) + } +} diff --git a/server/router/learning/enter.go b/server/router/learning/enter.go new file mode 100644 index 00000000..7f4c649f --- /dev/null +++ b/server/router/learning/enter.go @@ -0,0 +1,12 @@ +package learning + +type RouterGroup struct { + CourseRouter + ChapterRouter + KnowledgePointRouter + QuestionRouter + UserLearningRouter + WrongQuestionRouter + ExamRouter + UserExamRouter +} diff --git a/server/router/learning/exam.go b/server/router/learning/exam.go new file mode 100644 index 00000000..1bed220f --- /dev/null +++ b/server/router/learning/exam.go @@ -0,0 +1,36 @@ +package learning + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/api/v1" + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type ExamRouter struct{} + +// InitExamRouter 初始化 考试 路由信息 +func (s *ExamRouter) InitExamRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) { + examRouter := Router.Group("exam").Use(middleware.OperationRecord()) + examRouterWithoutRecord := Router.Group("exam") + examRouterWithoutAuth := PublicRouter.Group("exam") + + var examApi = v1.ApiGroupApp.LearningApiGroup.ExamApi + { + examRouter.POST("createExam", examApi.CreateExam) // 新建考试 + examRouter.DELETE("deleteExam", examApi.DeleteExam) // 删除考试 + examRouter.DELETE("deleteExamByIds", examApi.DeleteExamByIds) // 批量删除考试 + examRouter.PUT("updateExam", examApi.UpdateExam) // 更新考试 + examRouter.PUT("publishExam", examApi.PublishExam) // 发布考试 + } + { + examRouterWithoutRecord.GET("findExam", examApi.FindExam) // 根据ID获取考试 + examRouterWithoutRecord.GET("getExamList", examApi.GetExamList) // 获取考试列表 + examRouterWithoutRecord.GET("getActiveExams", examApi.GetActiveExams) // 获取进行中的考试列表 + examRouterWithoutRecord.GET("getExamsByCourse", examApi.GetExamsByCourse) // 根据课程获取考试列表 + examRouterWithoutRecord.GET("getExamStatistics", examApi.GetExamStatistics) // 获取考试统计 + } + { + // 公开接口,不需要认证 + examRouterWithoutAuth.GET("getPublishedExams", examApi.GetActiveExams) // 获取已发布的考试列表 + } +} diff --git a/server/router/learning/knowledge_point.go b/server/router/learning/knowledge_point.go new file mode 100644 index 00000000..6716100f --- /dev/null +++ b/server/router/learning/knowledge_point.go @@ -0,0 +1,31 @@ +package learning + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/api/v1" + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type KnowledgePointRouter struct{} + +// InitKnowledgePointRouter 初始化 知识点 路由信息 +func (s *KnowledgePointRouter) InitKnowledgePointRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) { + knowledgePointRouter := Router.Group("knowledgePoint").Use(middleware.OperationRecord()) + knowledgePointRouterWithoutRecord := Router.Group("knowledgePoint") + knowledgePointRouterWithoutAuth := PublicRouter.Group("knowledgePoint") + + var knowledgePointApi = v1.ApiGroupApp.LearningApiGroup.KnowledgePointApi + { + knowledgePointRouter.POST("createKnowledgePoint", knowledgePointApi.CreateKnowledgePoint) // 新建知识点 + knowledgePointRouter.DELETE("deleteKnowledgePoint", knowledgePointApi.DeleteKnowledgePoint) // 删除知识点 + knowledgePointRouter.DELETE("deleteKnowledgePointByIds", knowledgePointApi.DeleteKnowledgePointByIds) // 批量删除知识点 + knowledgePointRouter.PUT("updateKnowledgePoint", knowledgePointApi.UpdateKnowledgePoint) // 更新知识点 + } + { + knowledgePointRouterWithoutRecord.GET("findKnowledgePoint", knowledgePointApi.FindKnowledgePoint) // 根据ID获取知识点 + knowledgePointRouterWithoutRecord.GET("getKnowledgePointList", knowledgePointApi.GetKnowledgePointList) // 获取知识点列表 + } + { + knowledgePointRouterWithoutAuth.GET("getKnowledgePointsByChapter", knowledgePointApi.GetKnowledgePointsByChapter) // 根据章节获取知识点列表(无需认证) + } +} diff --git a/server/router/learning/question.go b/server/router/learning/question.go new file mode 100644 index 00000000..c578a0a6 --- /dev/null +++ b/server/router/learning/question.go @@ -0,0 +1,31 @@ +package learning + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/api/v1" + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type QuestionRouter struct{} + +// InitQuestionRouter 初始化 题目 路由信息 +func (s *QuestionRouter) InitQuestionRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) { + questionRouter := Router.Group("question").Use(middleware.OperationRecord()) + questionRouterWithoutRecord := Router.Group("question") + questionRouterWithoutAuth := PublicRouter.Group("question") + + var questionApi = v1.ApiGroupApp.LearningApiGroup.QuestionApi + { + questionRouter.POST("createQuestion", questionApi.CreateQuestion) // 新建题目 + questionRouter.DELETE("deleteQuestion", questionApi.DeleteQuestion) // 删除题目 + questionRouter.DELETE("deleteQuestionByIds", questionApi.DeleteQuestionByIds) // 批量删除题目 + questionRouter.PUT("updateQuestion", questionApi.UpdateQuestion) // 更新题目 + } + { + questionRouterWithoutRecord.GET("findQuestion", questionApi.FindQuestion) // 根据ID获取题目 + questionRouterWithoutRecord.GET("getQuestionList", questionApi.GetQuestionList) // 获取题目列表 + } + { + questionRouterWithoutAuth.GET("getQuestionsByKnowledgePoint", questionApi.GetQuestionsByKnowledgePoint) // 根据知识点获取题目列表(无需认证) + } +} diff --git a/server/router/learning/user_exam.go b/server/router/learning/user_exam.go new file mode 100644 index 00000000..3fad68ac --- /dev/null +++ b/server/router/learning/user_exam.go @@ -0,0 +1,32 @@ +package learning + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/api/v1" + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type UserExamRouter struct{} + +// InitUserExamRouter 初始化 用户考试记录 路由信息 +func (s *UserExamRouter) InitUserExamRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) { + userExamRouter := Router.Group("userExam").Use(middleware.OperationRecord()) + userExamRouterWithoutRecord := Router.Group("userExam") + + var userExamApi = v1.ApiGroupApp.LearningApiGroup.UserExamApi + { + userExamRouter.POST("createUserExam", userExamApi.CreateUserExam) // 新建用户考试记录 + userExamRouter.DELETE("deleteUserExam", userExamApi.DeleteUserExam) // 删除用户考试记录 + userExamRouter.DELETE("deleteUserExamByIds", userExamApi.DeleteUserExamByIds) // 批量删除用户考试记录 + userExamRouter.PUT("updateUserExam", userExamApi.UpdateUserExam) // 更新用户考试记录 + userExamRouter.POST("startExam", userExamApi.StartExam) // 开始考试 + userExamRouter.PUT("submitExam", userExamApi.SubmitExam) // 提交考试 + } + { + userExamRouterWithoutRecord.GET("findUserExam", userExamApi.FindUserExam) // 根据ID获取用户考试记录 + userExamRouterWithoutRecord.GET("getUserExamList", userExamApi.GetUserExamList) // 获取用户考试记录列表 + userExamRouterWithoutRecord.GET("getUserExamHistory", userExamApi.GetUserExamHistory) // 获取用户考试历史 + userExamRouterWithoutRecord.GET("getUserExamStatistics", userExamApi.GetUserExamStatistics) // 获取用户考试统计 + userExamRouterWithoutRecord.GET("getExamRanking", userExamApi.GetExamRanking) // 获取考试排行榜 + } +} diff --git a/server/router/learning/user_learning.go b/server/router/learning/user_learning.go new file mode 100644 index 00000000..5fde6280 --- /dev/null +++ b/server/router/learning/user_learning.go @@ -0,0 +1,29 @@ +package learning + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/api/v1" + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type UserLearningRouter struct{} + +// InitUserLearningRouter 初始化 用户学习记录 路由信息 +func (s *UserLearningRouter) InitUserLearningRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) { + userLearningRouter := Router.Group("userLearning").Use(middleware.OperationRecord()) + userLearningRouterWithoutRecord := Router.Group("userLearning") + + var userLearningApi = v1.ApiGroupApp.LearningApiGroup.UserLearningApi + { + userLearningRouter.POST("createUserLearning", userLearningApi.CreateUserLearning) // 新建用户学习记录 + userLearningRouter.DELETE("deleteUserLearning", userLearningApi.DeleteUserLearning) // 删除用户学习记录 + userLearningRouter.DELETE("deleteUserLearningByIds", userLearningApi.DeleteUserLearningByIds) // 批量删除用户学习记录 + userLearningRouter.PUT("updateUserLearning", userLearningApi.UpdateUserLearning) // 更新用户学习记录 + } + { + userLearningRouterWithoutRecord.GET("findUserLearning", userLearningApi.FindUserLearning) // 根据ID获取用户学习记录 + userLearningRouterWithoutRecord.GET("getUserLearningList", userLearningApi.GetUserLearningList) // 获取用户学习记录列表 + userLearningRouterWithoutRecord.GET("getUserProgress", userLearningApi.GetUserProgress) // 获取用户学习进度 + userLearningRouterWithoutRecord.GET("getLearningStatistics", userLearningApi.GetLearningStatistics) // 获取用户学习统计 + } +} diff --git a/server/router/learning/wrong_question.go b/server/router/learning/wrong_question.go new file mode 100644 index 00000000..0595549c --- /dev/null +++ b/server/router/learning/wrong_question.go @@ -0,0 +1,30 @@ +package learning + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/api/v1" + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type WrongQuestionRouter struct{} + +// InitWrongQuestionRouter 初始化 错题本 路由信息 +func (s *WrongQuestionRouter) InitWrongQuestionRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) { + wrongQuestionRouter := Router.Group("wrongQuestion").Use(middleware.OperationRecord()) + wrongQuestionRouterWithoutRecord := Router.Group("wrongQuestion") + + var wrongQuestionApi = v1.ApiGroupApp.LearningApiGroup.WrongQuestionApi + { + wrongQuestionRouter.POST("createWrongQuestion", wrongQuestionApi.CreateWrongQuestion) // 新建错题记录 + wrongQuestionRouter.DELETE("deleteWrongQuestion", wrongQuestionApi.DeleteWrongQuestion) // 删除错题记录 + wrongQuestionRouter.DELETE("deleteWrongQuestionByIds", wrongQuestionApi.DeleteWrongQuestionByIds) // 批量删除错题记录 + wrongQuestionRouter.PUT("updateWrongQuestion", wrongQuestionApi.UpdateWrongQuestion) // 更新错题记录 + wrongQuestionRouter.PUT("markAsMastered", wrongQuestionApi.MarkAsMastered) // 标记为已掌握 + } + { + wrongQuestionRouterWithoutRecord.GET("findWrongQuestion", wrongQuestionApi.FindWrongQuestion) // 根据ID获取错题记录 + wrongQuestionRouterWithoutRecord.GET("getWrongQuestionList", wrongQuestionApi.GetWrongQuestionList) // 获取错题记录列表 + wrongQuestionRouterWithoutRecord.GET("getUserWrongQuestions", wrongQuestionApi.GetUserWrongQuestions) // 获取用户错题列表 + wrongQuestionRouterWithoutRecord.GET("getWrongQuestionStatistics", wrongQuestionApi.GetWrongQuestionStatistics) // 获取错题统计 + } +} diff --git a/server/service/enter.go b/server/service/enter.go index 4dc990e7..63f704a7 100644 --- a/server/service/enter.go +++ b/server/service/enter.go @@ -2,12 +2,14 @@ package service import ( "github.com/flipped-aurora/gin-vue-admin/server/service/example" + "github.com/flipped-aurora/gin-vue-admin/server/service/learning" "github.com/flipped-aurora/gin-vue-admin/server/service/system" ) var ServiceGroupApp = new(ServiceGroup) type ServiceGroup struct { - SystemServiceGroup system.ServiceGroup - ExampleServiceGroup example.ServiceGroup + SystemServiceGroup system.ServiceGroup + ExampleServiceGroup example.ServiceGroup + LearningServiceGroup learning.ServiceGroup } diff --git a/server/service/learning/chapter.go b/server/service/learning/chapter.go new file mode 100644 index 00000000..29c9d4cf --- /dev/null +++ b/server/service/learning/chapter.go @@ -0,0 +1,126 @@ +package learning + +import ( + "context" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/learning" + learningReq "github.com/flipped-aurora/gin-vue-admin/server/model/learning/request" +) + +type ChapterService struct{} + +// CreateChapter 创建章节 +func (chapterService *ChapterService) CreateChapter(ctx context.Context, chapter *learning.Chapter) (err error) { + err = global.GVA_DB.WithContext(ctx).Create(chapter).Error + return err +} + +// DeleteChapter 删除章节 +func (chapterService *ChapterService) DeleteChapter(ctx context.Context, ID string) (err error) { + err = global.GVA_DB.WithContext(ctx).Delete(&learning.Chapter{}, ID).Error + return err +} + +// DeleteChapterByIds 批量删除章节 +func (chapterService *ChapterService) DeleteChapterByIds(ctx context.Context, IDs []string) (err error) { + err = global.GVA_DB.WithContext(ctx).Delete(&[]learning.Chapter{}, "id in ?", IDs).Error + return err +} + +// UpdateChapter 更新章节 +func (chapterService *ChapterService) UpdateChapter(ctx context.Context, chapter learning.Chapter) (err error) { + err = global.GVA_DB.WithContext(ctx).Model(&learning.Chapter{}).Where("id = ?", chapter.ID).Updates(&chapter).Error + return err +} + +// GetChapter 根据ID获取章节 +func (chapterService *ChapterService) GetChapter(ctx context.Context, ID string) (chapter learning.Chapter, err error) { + err = global.GVA_DB.WithContext(ctx).Where("id = ?", ID).First(&chapter).Error + if err != nil { + return + } + // 手动查询关联的课程信息 + if chapter.CourseId != 0 { + var course learning.Course + courseErr := global.GVA_DB.WithContext(ctx).Where("id = ?", chapter.CourseId).First(&course).Error + if courseErr == nil { + chapter.Course = course + } + } + return +} + +// GetChapterInfoList 分页获取章节列表 +func (chapterService *ChapterService) GetChapterInfoList(ctx context.Context, info learningReq.ChapterSearch) (list []learning.Chapter, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.WithContext(ctx).Model(&learning.Chapter{}) + var chapters []learning.Chapter + // 如果有条件搜索 下方会自动创建搜索语句 + if info.StartCreatedAt != nil && info.EndCreatedAt != nil { + db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt) + } + if info.Title != "" { + db = db.Where("title LIKE ?", "%"+info.Title+"%") + } + if info.Status != "" { + db = db.Where("status = ?", info.Status) + } + if info.CourseId != 0 { + db = db.Where("course_id = ?", info.CourseId) + } + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Order("sort asc, created_at desc").Find(&chapters).Error + if err != nil { + return chapters, total, err + } + + // 手动查询关联的课程信息 + courseIds := make([]uint, 0) + courseMap := make(map[uint]learning.Course) + + for _, chapter := range chapters { + if chapter.CourseId != 0 { + courseIds = append(courseIds, chapter.CourseId) + } + } + + if len(courseIds) > 0 { + var courses []learning.Course + global.GVA_DB.WithContext(ctx).Where("id in ?", courseIds).Find(&courses) + for _, course := range courses { + courseMap[course.ID] = course + } + + // 为每个章节设置课程信息 + for i := range chapters { + if course, exists := courseMap[chapters[i].CourseId]; exists { + chapters[i].Course = course + } + } + } + + return chapters, total, err +} + +// GetChaptersByCourse 根据课程ID获取章节列表 +func (chapterService *ChapterService) GetChaptersByCourse(ctx context.Context, courseId uint) (list []learning.Chapter, err error) { + err = global.GVA_DB.WithContext(ctx).Where("course_id = ? AND status = ?", courseId, "published").Order("sort asc, created_at asc").Find(&list).Error + return +} + +// UpdateChapterStatus 更新章节状态 +func (chapterService *ChapterService) UpdateChapterStatus(ctx context.Context, ID string, status string) (err error) { + err = global.GVA_DB.WithContext(ctx).Model(&learning.Chapter{}).Where("id = ?", ID).Update("status", status).Error + return err +} diff --git a/server/service/learning/course.go b/server/service/learning/course.go new file mode 100644 index 00000000..5dc4b588 --- /dev/null +++ b/server/service/learning/course.go @@ -0,0 +1,86 @@ +package learning + +import ( + "context" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/learning" + learningReq "github.com/flipped-aurora/gin-vue-admin/server/model/learning/request" +) + +type CourseService struct{} + +// CreateCourse 创建课程 +func (courseService *CourseService) CreateCourse(ctx context.Context, course *learning.Course) (err error) { + err = global.GVA_DB.WithContext(ctx).Create(course).Error + return err +} + +// DeleteCourse 删除课程 +func (courseService *CourseService) DeleteCourse(ctx context.Context, ID string) (err error) { + err = global.GVA_DB.WithContext(ctx).Delete(&learning.Course{}, ID).Error + return err +} + +// DeleteCourseByIds 批量删除课程 +func (courseService *CourseService) DeleteCourseByIds(ctx context.Context, IDs []string) (err error) { + err = global.GVA_DB.WithContext(ctx).Delete(&[]learning.Course{}, "id in ?", IDs).Error + return err +} + +// UpdateCourse 更新课程 +func (courseService *CourseService) UpdateCourse(ctx context.Context, course learning.Course) (err error) { + err = global.GVA_DB.WithContext(ctx).Model(&learning.Course{}).Where("id = ?", course.ID).Updates(&course).Error + return err +} + +// GetCourse 根据ID获取课程 +func (courseService *CourseService) GetCourse(ctx context.Context, ID string) (course learning.Course, err error) { + err = global.GVA_DB.WithContext(ctx).Where("id = ?", ID).First(&course).Error + return +} + +// GetCourseInfoList 分页获取课程列表 +func (courseService *CourseService) GetCourseInfoList(ctx context.Context, info learningReq.CourseSearch) (list []learning.Course, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.WithContext(ctx).Model(&learning.Course{}) + var courses []learning.Course + // 如果有条件搜索 下方会自动创建搜索语句 + if info.StartCreatedAt != nil && info.EndCreatedAt != nil { + db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt) + } + if info.Title != "" { + db = db.Where("title LIKE ?", "%"+info.Title+"%") + } + if info.Status != "" { + db = db.Where("status = ?", info.Status) + } + if info.Difficulty != "" { + db = db.Where("difficulty = ?", info.Difficulty) + } + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Order("sort desc, created_at desc").Find(&courses).Error + return courses, total, err +} + +// GetPublishedCourses 获取已发布的课程列表(供学员选择) +func (courseService *CourseService) GetPublishedCourses(ctx context.Context) (list []learning.Course, err error) { + err = global.GVA_DB.WithContext(ctx).Where("status = ?", "published").Order("sort desc, created_at desc").Find(&list).Error + return +} + +// UpdateCourseStatus 更新课程状态 +func (courseService *CourseService) UpdateCourseStatus(ctx context.Context, ID string, status string) (err error) { + err = global.GVA_DB.WithContext(ctx).Model(&learning.Course{}).Where("id = ?", ID).Update("status", status).Error + return err +} diff --git a/server/service/learning/enter.go b/server/service/learning/enter.go new file mode 100644 index 00000000..44b3d5cc --- /dev/null +++ b/server/service/learning/enter.go @@ -0,0 +1,12 @@ +package learning + +type ServiceGroup struct { + CourseService + ChapterService + KnowledgePointService + QuestionService + UserLearningService + WrongQuestionService + ExamService + UserExamService +} diff --git a/server/service/learning/exam.go b/server/service/learning/exam.go new file mode 100644 index 00000000..45db99b3 --- /dev/null +++ b/server/service/learning/exam.go @@ -0,0 +1,193 @@ +package learning + +import ( + "context" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/learning" + learningReq "github.com/flipped-aurora/gin-vue-admin/server/model/learning/request" +) + +type ExamService struct{} + +// CreateExam 创建考试 +func (examService *ExamService) CreateExam(ctx context.Context, exam *learning.Exam) (err error) { + // 设置默认值 + if exam.Status == "" { + exam.Status = "draft" + } + if exam.MaxAttempts == 0 { + exam.MaxAttempts = 1 + } + + err = global.GVA_DB.WithContext(ctx).Create(exam).Error + return err +} + +// DeleteExam 删除考试 +func (examService *ExamService) DeleteExam(ctx context.Context, ID string) (err error) { + err = global.GVA_DB.WithContext(ctx).Delete(&learning.Exam{}, ID).Error + return err +} + +// DeleteExamByIds 批量删除考试 +func (examService *ExamService) DeleteExamByIds(ctx context.Context, IDs []string) (err error) { + err = global.GVA_DB.WithContext(ctx).Delete(&[]learning.Exam{}, "id in ?", IDs).Error + return err +} + +// UpdateExam 更新考试 +func (examService *ExamService) UpdateExam(ctx context.Context, exam learning.Exam) (err error) { + err = global.GVA_DB.WithContext(ctx).Model(&learning.Exam{}).Where("id = ?", exam.ID).Updates(&exam).Error + return err +} + +// GetExam 根据ID获取考试 +func (examService *ExamService) GetExam(ctx context.Context, ID string) (exam learning.Exam, err error) { + err = global.GVA_DB.WithContext(ctx).Where("id = ?", ID).First(&exam).Error + if err != nil { + return + } + // 手动查询关联的课程信息 + examService.loadCourseInfo(ctx, &exam) + return +} + +// GetExamInfoList 分页获取考试列表 +func (examService *ExamService) GetExamInfoList(ctx context.Context, info learningReq.ExamSearch) (list []learning.Exam, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.WithContext(ctx).Model(&learning.Exam{}) + var exams []learning.Exam + // 如果有条件搜索 下方会自动创建搜索语句 + if info.StartCreatedAt != nil && info.EndCreatedAt != nil { + db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt) + } + if info.Title != "" { + db = db.Where("title LIKE ?", "%"+info.Title+"%") + } + if info.CourseId != 0 { + db = db.Where("course_id = ?", info.CourseId) + } + if info.Status != "" { + db = db.Where("status = ?", info.Status) + } + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Order("created_at desc").Find(&exams).Error + if err != nil { + return exams, total, err + } + + // 批量加载关联的课程信息 + for i := range exams { + examService.loadCourseInfo(ctx, &exams[i]) + } + + return exams, total, err +} + +// GetActiveExams 获取进行中的考试列表 +func (examService *ExamService) GetActiveExams(ctx context.Context) (list []learning.Exam, err error) { + now := time.Now() + err = global.GVA_DB.WithContext(ctx). + Where("status = ? AND start_time <= ? AND end_time >= ?", "published", now, now). + Order("start_time asc"). + Find(&list).Error + + // 批量加载关联的课程信息 + for i := range list { + examService.loadCourseInfo(ctx, &list[i]) + } + + return +} + +// GetExamsByCourse 根据课程获取考试列表 +func (examService *ExamService) GetExamsByCourse(ctx context.Context, courseId uint) (list []learning.Exam, err error) { + err = global.GVA_DB.WithContext(ctx). + Where("course_id = ? AND status = ?", courseId, "published"). + Order("start_time asc"). + Find(&list).Error + + // 批量加载关联的课程信息 + for i := range list { + examService.loadCourseInfo(ctx, &list[i]) + } + + return +} + +// PublishExam 发布考试 +func (examService *ExamService) PublishExam(ctx context.Context, examId uint) (err error) { + updates := map[string]interface{}{ + "status": "published", + } + err = global.GVA_DB.WithContext(ctx).Model(&learning.Exam{}). + Where("id = ?", examId). + Updates(updates).Error + return err +} + +// GetExamStatistics 获取考试统计 +func (examService *ExamService) GetExamStatistics(ctx context.Context) (stats learningReq.ExamStatistics, err error) { + // 总考试数 + var totalExams int64 + err = global.GVA_DB.WithContext(ctx).Model(&learning.Exam{}).Count(&totalExams).Error + if err != nil { + return + } + stats.TotalExams = int(totalExams) + + // 进行中的考试数 + now := time.Now() + var activeExams int64 + err = global.GVA_DB.WithContext(ctx).Model(&learning.Exam{}). + Where("status = ? AND start_time <= ? AND end_time >= ?", "published", now, now). + Count(&activeExams).Error + if err != nil { + return + } + stats.ActiveExams = int(activeExams) + + // 已结束的考试数 + var completedExams int64 + err = global.GVA_DB.WithContext(ctx).Model(&learning.Exam{}). + Where("status = ? AND end_time < ?", "published", now). + Count(&completedExams).Error + if err != nil { + return + } + stats.CompletedExams = int(completedExams) + + // 草稿考试数 + var draftExams int64 + err = global.GVA_DB.WithContext(ctx).Model(&learning.Exam{}). + Where("status = ?", "draft"). + Count(&draftExams).Error + if err != nil { + return + } + stats.DraftExams = int(draftExams) + + return +} + +// loadCourseInfo 加载课程信息 +func (examService *ExamService) loadCourseInfo(ctx context.Context, exam *learning.Exam) { + if exam.CourseId != 0 { + var course learning.Course + if err := global.GVA_DB.WithContext(ctx).Where("id = ?", exam.CourseId).First(&course).Error; err == nil { + exam.Course = course + } + } +} diff --git a/server/service/learning/knowledge_point.go b/server/service/learning/knowledge_point.go new file mode 100644 index 00000000..9685e576 --- /dev/null +++ b/server/service/learning/knowledge_point.go @@ -0,0 +1,126 @@ +package learning + +import ( + "context" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/learning" + learningReq "github.com/flipped-aurora/gin-vue-admin/server/model/learning/request" +) + +type KnowledgePointService struct{} + +// CreateKnowledgePoint 创建知识点 +func (knowledgePointService *KnowledgePointService) CreateKnowledgePoint(ctx context.Context, knowledgePoint *learning.KnowledgePoint) (err error) { + err = global.GVA_DB.WithContext(ctx).Create(knowledgePoint).Error + return err +} + +// DeleteKnowledgePoint 删除知识点 +func (knowledgePointService *KnowledgePointService) DeleteKnowledgePoint(ctx context.Context, ID string) (err error) { + err = global.GVA_DB.WithContext(ctx).Delete(&learning.KnowledgePoint{}, ID).Error + return err +} + +// DeleteKnowledgePointByIds 批量删除知识点 +func (knowledgePointService *KnowledgePointService) DeleteKnowledgePointByIds(ctx context.Context, IDs []string) (err error) { + err = global.GVA_DB.WithContext(ctx).Delete(&[]learning.KnowledgePoint{}, "id in ?", IDs).Error + return err +} + +// UpdateKnowledgePoint 更新知识点 +func (knowledgePointService *KnowledgePointService) UpdateKnowledgePoint(ctx context.Context, knowledgePoint learning.KnowledgePoint) (err error) { + err = global.GVA_DB.WithContext(ctx).Model(&learning.KnowledgePoint{}).Where("id = ?", knowledgePoint.ID).Updates(&knowledgePoint).Error + return err +} + +// GetKnowledgePoint 根据ID获取知识点 +func (knowledgePointService *KnowledgePointService) GetKnowledgePoint(ctx context.Context, ID string) (knowledgePoint learning.KnowledgePoint, err error) { + err = global.GVA_DB.WithContext(ctx).Where("id = ?", ID).First(&knowledgePoint).Error + if err != nil { + return + } + // 手动查询关联的章节信息 + if knowledgePoint.ChapterId != 0 { + var chapter learning.Chapter + chapterErr := global.GVA_DB.WithContext(ctx).Where("id = ?", knowledgePoint.ChapterId).First(&chapter).Error + if chapterErr == nil { + knowledgePoint.Chapter = chapter + } + } + return +} + +// GetKnowledgePointInfoList 分页获取知识点列表 +func (knowledgePointService *KnowledgePointService) GetKnowledgePointInfoList(ctx context.Context, info learningReq.KnowledgePointSearch) (list []learning.KnowledgePoint, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.WithContext(ctx).Model(&learning.KnowledgePoint{}) + var knowledgePoints []learning.KnowledgePoint + // 如果有条件搜索 下方会自动创建搜索语句 + if info.StartCreatedAt != nil && info.EndCreatedAt != nil { + db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt) + } + if info.Title != "" { + db = db.Where("title LIKE ?", "%"+info.Title+"%") + } + if info.Status != "" { + db = db.Where("status = ?", info.Status) + } + if info.ChapterId != 0 { + db = db.Where("chapter_id = ?", info.ChapterId) + } + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Order("sort asc, created_at desc").Find(&knowledgePoints).Error + if err != nil { + return knowledgePoints, total, err + } + + // 手动查询关联的章节信息 + chapterIds := make([]uint, 0) + chapterMap := make(map[uint]learning.Chapter) + + for _, kp := range knowledgePoints { + if kp.ChapterId != 0 { + chapterIds = append(chapterIds, kp.ChapterId) + } + } + + if len(chapterIds) > 0 { + var chapters []learning.Chapter + global.GVA_DB.WithContext(ctx).Where("id in ?", chapterIds).Find(&chapters) + for _, chapter := range chapters { + chapterMap[chapter.ID] = chapter + } + + // 为每个知识点设置章节信息 + for i := range knowledgePoints { + if chapter, exists := chapterMap[knowledgePoints[i].ChapterId]; exists { + knowledgePoints[i].Chapter = chapter + } + } + } + + return knowledgePoints, total, err +} + +// GetKnowledgePointsByChapter 根据章节ID获取知识点列表 +func (knowledgePointService *KnowledgePointService) GetKnowledgePointsByChapter(ctx context.Context, chapterId uint) (list []learning.KnowledgePoint, err error) { + err = global.GVA_DB.WithContext(ctx).Where("chapter_id = ? AND status = ?", chapterId, "published").Order("sort asc, created_at asc").Find(&list).Error + return +} + +// UpdateKnowledgePointStatus 更新知识点状态 +func (knowledgePointService *KnowledgePointService) UpdateKnowledgePointStatus(ctx context.Context, ID string, status string) (err error) { + err = global.GVA_DB.WithContext(ctx).Model(&learning.KnowledgePoint{}).Where("id = ?", ID).Update("status", status).Error + return err +} diff --git a/server/service/learning/question.go b/server/service/learning/question.go new file mode 100644 index 00000000..5bb8245e --- /dev/null +++ b/server/service/learning/question.go @@ -0,0 +1,129 @@ +package learning + +import ( + "context" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/learning" + learningReq "github.com/flipped-aurora/gin-vue-admin/server/model/learning/request" +) + +type QuestionService struct{} + +// CreateQuestion 创建题目 +func (questionService *QuestionService) CreateQuestion(ctx context.Context, question *learning.Question) (err error) { + err = global.GVA_DB.WithContext(ctx).Create(question).Error + return err +} + +// DeleteQuestion 删除题目 +func (questionService *QuestionService) DeleteQuestion(ctx context.Context, ID string) (err error) { + err = global.GVA_DB.WithContext(ctx).Delete(&learning.Question{}, ID).Error + return err +} + +// DeleteQuestionByIds 批量删除题目 +func (questionService *QuestionService) DeleteQuestionByIds(ctx context.Context, IDs []string) (err error) { + err = global.GVA_DB.WithContext(ctx).Delete(&[]learning.Question{}, "id in ?", IDs).Error + return err +} + +// UpdateQuestion 更新题目 +func (questionService *QuestionService) UpdateQuestion(ctx context.Context, question learning.Question) (err error) { + err = global.GVA_DB.WithContext(ctx).Model(&learning.Question{}).Where("id = ?", question.ID).Updates(&question).Error + return err +} + +// GetQuestion 根据ID获取题目 +func (questionService *QuestionService) GetQuestion(ctx context.Context, ID string) (question learning.Question, err error) { + err = global.GVA_DB.WithContext(ctx).Where("id = ?", ID).First(&question).Error + if err != nil { + return + } + // 手动查询关联的知识点信息 + if question.KnowledgePointId != 0 { + var knowledgePoint learning.KnowledgePoint + kpErr := global.GVA_DB.WithContext(ctx).Where("id = ?", question.KnowledgePointId).First(&knowledgePoint).Error + if kpErr == nil { + question.KnowledgePoint = knowledgePoint + } + } + return +} + +// GetQuestionInfoList 分页获取题目列表 +func (questionService *QuestionService) GetQuestionInfoList(ctx context.Context, info learningReq.QuestionSearch) (list []learning.Question, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.WithContext(ctx).Model(&learning.Question{}) + var questions []learning.Question + // 如果有条件搜索 下方会自动创建搜索语句 + if info.StartCreatedAt != nil && info.EndCreatedAt != nil { + db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt) + } + if info.Title != "" { + db = db.Where("title LIKE ?", "%"+info.Title+"%") + } + if info.Type != "" { + db = db.Where("type = ?", info.Type) + } + if info.Difficulty != "" { + db = db.Where("difficulty = ?", info.Difficulty) + } + if info.KnowledgePointId != 0 { + db = db.Where("knowledge_point_id = ?", info.KnowledgePointId) + } + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Order("created_at desc").Find(&questions).Error + if err != nil { + return questions, total, err + } + + // 手动查询关联的知识点信息 + kpIds := make([]uint, 0) + kpMap := make(map[uint]learning.KnowledgePoint) + + for _, q := range questions { + if q.KnowledgePointId != 0 { + kpIds = append(kpIds, q.KnowledgePointId) + } + } + + if len(kpIds) > 0 { + var knowledgePoints []learning.KnowledgePoint + global.GVA_DB.WithContext(ctx).Where("id in ?", kpIds).Find(&knowledgePoints) + for _, kp := range knowledgePoints { + kpMap[kp.ID] = kp + } + + // 为每个题目设置知识点信息 + for i := range questions { + if kp, exists := kpMap[questions[i].KnowledgePointId]; exists { + questions[i].KnowledgePoint = kp + } + } + } + + return questions, total, err +} + +// GetQuestionsByKnowledgePoint 根据知识点ID获取题目列表 +func (questionService *QuestionService) GetQuestionsByKnowledgePoint(ctx context.Context, knowledgePointId uint) (list []learning.Question, err error) { + err = global.GVA_DB.WithContext(ctx).Where("knowledge_point_id = ?", knowledgePointId).Order("created_at asc").Find(&list).Error + return +} + +// GetRandomQuestionsByKnowledgePoint 根据知识点ID随机获取题目 +func (questionService *QuestionService) GetRandomQuestionsByKnowledgePoint(ctx context.Context, knowledgePointId uint, count int) (list []learning.Question, err error) { + err = global.GVA_DB.WithContext(ctx).Where("knowledge_point_id = ?", knowledgePointId).Order("RAND()").Limit(count).Find(&list).Error + return +} diff --git a/server/service/learning/user_exam.go b/server/service/learning/user_exam.go new file mode 100644 index 00000000..9e12eca6 --- /dev/null +++ b/server/service/learning/user_exam.go @@ -0,0 +1,327 @@ +package learning + +import ( + "context" + "errors" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/learning" + learningReq "github.com/flipped-aurora/gin-vue-admin/server/model/learning/request" +) + +type UserExamService struct{} + +// CreateUserExam 创建用户考试记录 +func (userExamService *UserExamService) CreateUserExam(ctx context.Context, userExam *learning.UserExam) (err error) { + // 设置默认值 + if userExam.Status == "" { + userExam.Status = "in_progress" + } + if userExam.AttemptNumber == 0 { + userExam.AttemptNumber = 1 + } + if userExam.StartTime.IsZero() { + userExam.StartTime = time.Now() + } + + err = global.GVA_DB.WithContext(ctx).Create(userExam).Error + return err +} + +// DeleteUserExam 删除用户考试记录 +func (userExamService *UserExamService) DeleteUserExam(ctx context.Context, ID string) (err error) { + err = global.GVA_DB.WithContext(ctx).Delete(&learning.UserExam{}, ID).Error + return err +} + +// DeleteUserExamByIds 批量删除用户考试记录 +func (userExamService *UserExamService) DeleteUserExamByIds(ctx context.Context, IDs []string) (err error) { + err = global.GVA_DB.WithContext(ctx).Delete(&[]learning.UserExam{}, "id in ?", IDs).Error + return err +} + +// UpdateUserExam 更新用户考试记录 +func (userExamService *UserExamService) UpdateUserExam(ctx context.Context, userExam learning.UserExam) (err error) { + err = global.GVA_DB.WithContext(ctx).Model(&learning.UserExam{}).Where("id = ?", userExam.ID).Updates(&userExam).Error + return err +} + +// GetUserExam 根据ID获取用户考试记录 +func (userExamService *UserExamService) GetUserExam(ctx context.Context, ID string) (userExam learning.UserExam, err error) { + err = global.GVA_DB.WithContext(ctx).Where("id = ?", ID).First(&userExam).Error + if err != nil { + return + } + // 手动查询关联的考试信息 + userExamService.loadExamInfo(ctx, &userExam) + return +} + +// GetUserExamInfoList 分页获取用户考试记录列表 +func (userExamService *UserExamService) GetUserExamInfoList(ctx context.Context, info learningReq.UserExamSearch) (list []learning.UserExam, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.WithContext(ctx).Model(&learning.UserExam{}) + var userExams []learning.UserExam + // 如果有条件搜索 下方会自动创建搜索语句 + if info.StartCreatedAt != nil && info.EndCreatedAt != nil { + db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt) + } + if info.UserId != 0 { + db = db.Where("user_id = ?", info.UserId) + } + if info.ExamId != 0 { + db = db.Where("exam_id = ?", info.ExamId) + } + if info.Status != "" { + db = db.Where("status = ?", info.Status) + } + if info.IsPassed { + db = db.Where("is_passed = ?", info.IsPassed) + } + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Order("created_at desc").Find(&userExams).Error + if err != nil { + return userExams, total, err + } + + // 批量加载关联的考试信息 + for i := range userExams { + userExamService.loadExamInfo(ctx, &userExams[i]) + } + + return userExams, total, err +} + +// StartExam 开始考试 +func (userExamService *UserExamService) StartExam(ctx context.Context, userId uint, examId uint) (userExam learning.UserExam, err error) { + // 检查考试是否存在且可以参加 + var exam learning.Exam + err = global.GVA_DB.WithContext(ctx).Where("id = ? AND status = ?", examId, "published").First(&exam).Error + if err != nil { + return userExam, errors.New("考试不存在或未发布") + } + + // 检查考试时间 + now := time.Now() + if now.Before(exam.StartTime) { + return userExam, errors.New("考试尚未开始") + } + if now.After(exam.EndTime) { + return userExam, errors.New("考试已结束") + } + + // 检查用户是否已经参加过考试 + var existingCount int64 + global.GVA_DB.WithContext(ctx).Model(&learning.UserExam{}). + Where("user_id = ? AND exam_id = ?", userId, examId). + Count(&existingCount) + + // 检查是否允许重考和尝试次数 + if existingCount > 0 && !exam.AllowRetake { + return userExam, errors.New("该考试不允许重考") + } + if existingCount >= int64(exam.MaxAttempts) { + return userExam, errors.New("已达到最大尝试次数") + } + + // 创建考试记录 + userExam = learning.UserExam{ + UserId: userId, + ExamId: examId, + AttemptNumber: int(existingCount) + 1, + StartTime: now, + Status: "in_progress", + } + + err = userExamService.CreateUserExam(ctx, &userExam) + if err != nil { + return userExam, err + } + + // 加载考试信息 + userExamService.loadExamInfo(ctx, &userExam) + + return userExam, nil +} + +// SubmitExam 提交考试 +func (userExamService *UserExamService) SubmitExam(ctx context.Context, userExamId uint, totalScore int, correctCount int, wrongCount int) (err error) { + var userExam learning.UserExam + err = global.GVA_DB.WithContext(ctx).Where("id = ?", userExamId).First(&userExam).Error + if err != nil { + return errors.New("考试记录不存在") + } + + if userExam.Status != "in_progress" { + return errors.New("考试已结束,无法重复提交") + } + + // 获取考试信息 + var exam learning.Exam + err = global.GVA_DB.WithContext(ctx).Where("id = ?", userExam.ExamId).First(&exam).Error + if err != nil { + return err + } + + // 计算考试时长 + now := time.Now() + duration := int(now.Sub(userExam.StartTime).Minutes()) + + // 判断是否通过 + isPassed := totalScore >= exam.PassScore + + // 更新考试记录 + updates := map[string]interface{}{ + "end_time": now, + "submitted_at": now, + "duration": duration, + "total_score": totalScore, + "correct_count": correctCount, + "wrong_count": wrongCount, + "status": "completed", + "is_passed": isPassed, + } + + err = global.GVA_DB.WithContext(ctx).Model(&learning.UserExam{}). + Where("id = ?", userExamId). + Updates(updates).Error + + return err +} + +// GetUserExamHistory 获取用户考试历史 +func (userExamService *UserExamService) GetUserExamHistory(ctx context.Context, userId uint) (list []learning.UserExam, err error) { + err = global.GVA_DB.WithContext(ctx). + Where("user_id = ?", userId). + Order("created_at desc"). + Find(&list).Error + + // 批量加载关联的考试信息 + for i := range list { + userExamService.loadExamInfo(ctx, &list[i]) + } + + return +} + +// GetUserExamStatistics 获取用户考试统计 +func (userExamService *UserExamService) GetUserExamStatistics(ctx context.Context, userId uint) (stats learningReq.UserExamStatistics, err error) { + // 总考试次数 + var totalExams int64 + err = global.GVA_DB.WithContext(ctx).Model(&learning.UserExam{}). + Where("user_id = ? AND status = ?", userId, "completed"). + Count(&totalExams).Error + if err != nil { + return + } + stats.TotalExams = int(totalExams) + + if totalExams == 0 { + return stats, nil + } + + // 通过考试次数 + var passedExams int64 + err = global.GVA_DB.WithContext(ctx).Model(&learning.UserExam{}). + Where("user_id = ? AND status = ? AND is_passed = ?", userId, "completed", true). + Count(&passedExams).Error + if err != nil { + return + } + stats.PassedExams = int(passedExams) + stats.FailedExams = stats.TotalExams - stats.PassedExams + + // 平均分数和最高分数 + var result struct { + AverageScore float64 + HighestScore int + TotalTime int + } + err = global.GVA_DB.WithContext(ctx).Model(&learning.UserExam{}). + Select("AVG(total_score) as average_score, MAX(total_score) as highest_score, SUM(duration) as total_time"). + Where("user_id = ? AND status = ?", userId, "completed"). + Scan(&result).Error + if err != nil { + return + } + + stats.AverageScore = result.AverageScore + stats.HighestScore = result.HighestScore + stats.TotalStudyTime = result.TotalTime + + // 通过率 + if stats.TotalExams > 0 { + stats.PassRate = float64(stats.PassedExams) / float64(stats.TotalExams) * 100 + } + + return +} + +// GetExamRanking 获取考试排行榜 +func (userExamService *UserExamService) GetExamRanking(ctx context.Context, examId uint, limit int) (ranking learningReq.ExamRankingResponse, err error) { + // 获取考试信息 + var exam learning.Exam + err = global.GVA_DB.WithContext(ctx).Where("id = ?", examId).First(&exam).Error + if err != nil { + return + } + + ranking.ExamId = examId + ranking.ExamTitle = exam.Title + + // 获取排行榜数据 + var userExams []learning.UserExam + db := global.GVA_DB.WithContext(ctx). + Where("exam_id = ? AND status = ?", examId, "completed"). + Order("total_score desc, duration asc") + + if limit > 0 { + db = db.Limit(limit) + } + + err = db.Find(&userExams).Error + if err != nil { + return + } + + // 构建排行榜 + for i, userExam := range userExams { + item := learningReq.ExamRankingItem{ + UserId: userExam.UserId, + Score: userExam.TotalScore, + Duration: userExam.Duration, + Rank: i + 1, + IsPassed: userExam.IsPassed, + SubmittedAt: userExam.SubmittedAt, + } + ranking.Rankings = append(ranking.Rankings, item) + } + + // 获取总数 + global.GVA_DB.WithContext(ctx).Model(&learning.UserExam{}). + Where("exam_id = ? AND status = ?", examId, "completed"). + Count(&ranking.Total) + + return +} + +// loadExamInfo 加载考试信息 +func (userExamService *UserExamService) loadExamInfo(ctx context.Context, userExam *learning.UserExam) { + if userExam.ExamId != 0 { + var exam learning.Exam + if err := global.GVA_DB.WithContext(ctx).Where("id = ?", userExam.ExamId).First(&exam).Error; err == nil { + userExam.Exam = exam + } + } +} diff --git a/server/service/learning/user_learning.go b/server/service/learning/user_learning.go new file mode 100644 index 00000000..ccf69534 --- /dev/null +++ b/server/service/learning/user_learning.go @@ -0,0 +1,202 @@ +package learning + +import ( + "context" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/learning" + learningReq "github.com/flipped-aurora/gin-vue-admin/server/model/learning/request" +) + +type UserLearningService struct{} + +// CreateUserLearning 创建用户学习记录 +func (userLearningService *UserLearningService) CreateUserLearning(ctx context.Context, userLearning *learning.UserLearning) (err error) { + // 检查是否已存在该用户对该课程的学习记录 + var existingRecord learning.UserLearning + err = global.GVA_DB.WithContext(ctx).Where("user_id = ? AND course_id = ?", userLearning.UserId, userLearning.CourseId).First(&existingRecord).Error + if err == nil { + // 如果已存在,更新记录 + return userLearningService.UpdateUserLearning(ctx, *userLearning) + } + + // 设置默认值 + if userLearning.LastStudyAt.IsZero() { + userLearning.LastStudyAt = time.Now() + } + if userLearning.Status == "" { + userLearning.Status = "learning" + } + + err = global.GVA_DB.WithContext(ctx).Create(userLearning).Error + return err +} + +// DeleteUserLearning 删除用户学习记录 +func (userLearningService *UserLearningService) DeleteUserLearning(ctx context.Context, ID string) (err error) { + err = global.GVA_DB.WithContext(ctx).Delete(&learning.UserLearning{}, ID).Error + return err +} + +// DeleteUserLearningByIds 批量删除用户学习记录 +func (userLearningService *UserLearningService) DeleteUserLearningByIds(ctx context.Context, IDs []string) (err error) { + err = global.GVA_DB.WithContext(ctx).Delete(&[]learning.UserLearning{}, "id in ?", IDs).Error + return err +} + +// UpdateUserLearning 更新用户学习记录 +func (userLearningService *UserLearningService) UpdateUserLearning(ctx context.Context, userLearning learning.UserLearning) (err error) { + // 更新最后学习时间 + userLearning.LastStudyAt = time.Now() + + // 如果进度达到100%,设置完成状态和完成时间 + if userLearning.Progress >= 100 && userLearning.Status != "completed" { + userLearning.Status = "completed" + userLearning.CompletedAt = time.Now() + } + + err = global.GVA_DB.WithContext(ctx).Model(&learning.UserLearning{}).Where("id = ?", userLearning.ID).Updates(&userLearning).Error + return err +} + +// GetUserLearning 根据ID获取用户学习记录 +func (userLearningService *UserLearningService) GetUserLearning(ctx context.Context, ID string) (userLearning learning.UserLearning, err error) { + err = global.GVA_DB.WithContext(ctx).Where("id = ?", ID).First(&userLearning).Error + if err != nil { + return + } + // 手动查询关联信息 + userLearningService.loadAssociations(ctx, &userLearning) + return +} + +// GetUserLearningInfoList 分页获取用户学习记录列表 +func (userLearningService *UserLearningService) GetUserLearningInfoList(ctx context.Context, info learningReq.UserLearningSearch) (list []learning.UserLearning, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.WithContext(ctx).Model(&learning.UserLearning{}) + var userLearnings []learning.UserLearning + // 如果有条件搜索 下方会自动创建搜索语句 + if info.StartCreatedAt != nil && info.EndCreatedAt != nil { + db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt) + } + if info.UserId != 0 { + db = db.Where("user_id = ?", info.UserId) + } + if info.CourseId != 0 { + db = db.Where("course_id = ?", info.CourseId) + } + if info.Status != "" { + db = db.Where("status = ?", info.Status) + } + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Order("last_study_at desc").Find(&userLearnings).Error + if err != nil { + return userLearnings, total, err + } + + // 批量加载关联信息 + for i := range userLearnings { + userLearningService.loadAssociations(ctx, &userLearnings[i]) + } + + return userLearnings, total, err +} + +// GetUserProgress 获取用户学习进度 +func (userLearningService *UserLearningService) GetUserProgress(ctx context.Context, userId uint, courseId uint) (progress []learning.UserLearning, err error) { + db := global.GVA_DB.WithContext(ctx).Where("user_id = ?", userId) + if courseId != 0 { + db = db.Where("course_id = ?", courseId) + } + err = db.Find(&progress).Error + + // 批量加载关联信息 + for i := range progress { + userLearningService.loadAssociations(ctx, &progress[i]) + } + + return +} + +// GetLearningStatistics 获取用户学习统计 +func (userLearningService *UserLearningService) GetLearningStatistics(ctx context.Context, userId uint) (stats learningReq.LearningStatistics, err error) { + // 总课程数 + var totalCourses int64 + err = global.GVA_DB.WithContext(ctx).Model(&learning.UserLearning{}).Where("user_id = ?", userId).Count(&totalCourses).Error + if err != nil { + return + } + stats.TotalCourses = int(totalCourses) + + // 已完成课程数 + var completedCourses int64 + err = global.GVA_DB.WithContext(ctx).Model(&learning.UserLearning{}).Where("user_id = ? AND status = ?", userId, "completed").Count(&completedCourses).Error + if err != nil { + return + } + stats.CompletedCourses = int(completedCourses) + + // 学习中课程数 + var inProgressCourses int64 + err = global.GVA_DB.WithContext(ctx).Model(&learning.UserLearning{}).Where("user_id = ? AND status = ?", userId, "learning").Count(&inProgressCourses).Error + if err != nil { + return + } + stats.InProgressCourses = int(inProgressCourses) + + // 总学习时长和平均分数 + var result struct { + TotalStudyTime int + AverageScore int + } + err = global.GVA_DB.WithContext(ctx).Model(&learning.UserLearning{}). + Select("SUM(study_time) as total_study_time, AVG(score) as average_score"). + Where("user_id = ?", userId). + Scan(&result).Error + if err != nil { + return + } + + stats.TotalStudyTime = result.TotalStudyTime + stats.AverageScore = result.AverageScore + + return +} + +// loadAssociations 加载关联信息 +func (userLearningService *UserLearningService) loadAssociations(ctx context.Context, userLearning *learning.UserLearning) { + // 加载课程信息 + if userLearning.CourseId != 0 { + var course learning.Course + if err := global.GVA_DB.WithContext(ctx).Where("id = ?", userLearning.CourseId).First(&course).Error; err == nil { + userLearning.Course = course + } + } + + // 加载章节信息 + if userLearning.ChapterId != 0 { + var chapter learning.Chapter + if err := global.GVA_DB.WithContext(ctx).Where("id = ?", userLearning.ChapterId).First(&chapter).Error; err == nil { + userLearning.Chapter = chapter + } + } + + // 加载知识点信息 + if userLearning.KnowledgePointId != 0 { + var knowledgePoint learning.KnowledgePoint + if err := global.GVA_DB.WithContext(ctx).Where("id = ?", userLearning.KnowledgePointId).First(&knowledgePoint).Error; err == nil { + userLearning.KnowledgePoint = knowledgePoint + } + } +} diff --git a/server/service/learning/wrong_question.go b/server/service/learning/wrong_question.go new file mode 100644 index 00000000..11b43ab4 --- /dev/null +++ b/server/service/learning/wrong_question.go @@ -0,0 +1,186 @@ +package learning + +import ( + "context" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/learning" + learningReq "github.com/flipped-aurora/gin-vue-admin/server/model/learning/request" +) + +type WrongQuestionService struct{} + +// CreateWrongQuestion 创建错题记录 +func (wrongQuestionService *WrongQuestionService) CreateWrongQuestion(ctx context.Context, wrongQuestion *learning.WrongQuestion) (err error) { + // 检查是否已存在该用户对该题目的错题记录 + var existingRecord learning.WrongQuestion + err = global.GVA_DB.WithContext(ctx).Where("user_id = ? AND question_id = ?", wrongQuestion.UserId, wrongQuestion.QuestionId).First(&existingRecord).Error + if err == nil { + // 如果已存在,更新错误次数和最后错误时间 + existingRecord.WrongCount++ + existingRecord.LastWrongAt = time.Now() + existingRecord.WrongAnswer = wrongQuestion.WrongAnswer + existingRecord.IsMastered = false // 重新答错,标记为未掌握 + existingRecord.MasteredAt = time.Time{} + return wrongQuestionService.UpdateWrongQuestion(ctx, existingRecord) + } + + // 设置默认值 + if wrongQuestion.LastWrongAt.IsZero() { + wrongQuestion.LastWrongAt = time.Now() + } + if wrongQuestion.WrongCount == 0 { + wrongQuestion.WrongCount = 1 + } + + err = global.GVA_DB.WithContext(ctx).Create(wrongQuestion).Error + return err +} + +// DeleteWrongQuestion 删除错题记录 +func (wrongQuestionService *WrongQuestionService) DeleteWrongQuestion(ctx context.Context, ID string) (err error) { + err = global.GVA_DB.WithContext(ctx).Delete(&learning.WrongQuestion{}, ID).Error + return err +} + +// DeleteWrongQuestionByIds 批量删除错题记录 +func (wrongQuestionService *WrongQuestionService) DeleteWrongQuestionByIds(ctx context.Context, IDs []string) (err error) { + err = global.GVA_DB.WithContext(ctx).Delete(&[]learning.WrongQuestion{}, "id in ?", IDs).Error + return err +} + +// UpdateWrongQuestion 更新错题记录 +func (wrongQuestionService *WrongQuestionService) UpdateWrongQuestion(ctx context.Context, wrongQuestion learning.WrongQuestion) (err error) { + err = global.GVA_DB.WithContext(ctx).Model(&learning.WrongQuestion{}).Where("id = ?", wrongQuestion.ID).Updates(&wrongQuestion).Error + return err +} + +// GetWrongQuestion 根据ID获取错题记录 +func (wrongQuestionService *WrongQuestionService) GetWrongQuestion(ctx context.Context, ID string) (wrongQuestion learning.WrongQuestion, err error) { + err = global.GVA_DB.WithContext(ctx).Where("id = ?", ID).First(&wrongQuestion).Error + if err != nil { + return + } + // 手动查询关联的题目信息 + wrongQuestionService.loadQuestionInfo(ctx, &wrongQuestion) + return +} + +// GetWrongQuestionInfoList 分页获取错题记录列表 +func (wrongQuestionService *WrongQuestionService) GetWrongQuestionInfoList(ctx context.Context, info learningReq.WrongQuestionSearch) (list []learning.WrongQuestion, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.WithContext(ctx).Model(&learning.WrongQuestion{}) + var wrongQuestions []learning.WrongQuestion + // 如果有条件搜索 下方会自动创建搜索语句 + if info.StartCreatedAt != nil && info.EndCreatedAt != nil { + db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt) + } + if info.UserId != 0 { + db = db.Where("user_id = ?", info.UserId) + } + if info.QuestionId != 0 { + db = db.Where("question_id = ?", info.QuestionId) + } + if info.IsMastered { + db = db.Where("is_mastered = ?", info.IsMastered) + } + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Order("last_wrong_at desc").Find(&wrongQuestions).Error + if err != nil { + return wrongQuestions, total, err + } + + // 批量加载关联的题目信息 + for i := range wrongQuestions { + wrongQuestionService.loadQuestionInfo(ctx, &wrongQuestions[i]) + } + + return wrongQuestions, total, err +} + +// GetUserWrongQuestions 获取用户的错题列表 +func (wrongQuestionService *WrongQuestionService) GetUserWrongQuestions(ctx context.Context, userId uint, onlyUnmastered bool) (list []learning.WrongQuestion, err error) { + db := global.GVA_DB.WithContext(ctx).Where("user_id = ?", userId) + if onlyUnmastered { + db = db.Where("is_mastered = ?", false) + } + err = db.Order("last_wrong_at desc").Find(&list).Error + + // 批量加载关联的题目信息 + for i := range list { + wrongQuestionService.loadQuestionInfo(ctx, &list[i]) + } + + return +} + +// MarkAsMastered 标记题目为已掌握 +func (wrongQuestionService *WrongQuestionService) MarkAsMastered(ctx context.Context, userId uint, wrongQuestionId uint) (err error) { + updates := map[string]interface{}{ + "is_mastered": true, + "mastered_at": time.Now(), + } + err = global.GVA_DB.WithContext(ctx).Model(&learning.WrongQuestion{}). + Where("id = ? AND user_id = ?", wrongQuestionId, userId). + Updates(updates).Error + return err +} + +// GetWrongQuestionStatistics 获取用户错题统计 +func (wrongQuestionService *WrongQuestionService) GetWrongQuestionStatistics(ctx context.Context, userId uint) (stats learningReq.WrongQuestionStatistics, err error) { + // 总错题数 + var totalWrongQuestions int64 + err = global.GVA_DB.WithContext(ctx).Model(&learning.WrongQuestion{}).Where("user_id = ?", userId).Count(&totalWrongQuestions).Error + if err != nil { + return + } + stats.TotalWrongQuestions = int(totalWrongQuestions) + + // 已掌握题目数 + var masteredQuestions int64 + err = global.GVA_DB.WithContext(ctx).Model(&learning.WrongQuestion{}).Where("user_id = ? AND is_mastered = ?", userId, true).Count(&masteredQuestions).Error + if err != nil { + return + } + stats.MasteredQuestions = int(masteredQuestions) + + // 未掌握题目数 + stats.UnmasteredQuestions = stats.TotalWrongQuestions - stats.MasteredQuestions + + // 平均错误次数 + var result struct { + AverageWrongCount float64 + } + err = global.GVA_DB.WithContext(ctx).Model(&learning.WrongQuestion{}). + Select("AVG(wrong_count) as average_wrong_count"). + Where("user_id = ?", userId). + Scan(&result).Error + if err != nil { + return + } + + stats.AverageWrongCount = int(result.AverageWrongCount) + + return +} + +// loadQuestionInfo 加载题目信息 +func (wrongQuestionService *WrongQuestionService) loadQuestionInfo(ctx context.Context, wrongQuestion *learning.WrongQuestion) { + if wrongQuestion.QuestionId != 0 { + var question learning.Question + if err := global.GVA_DB.WithContext(ctx).Where("id = ?", wrongQuestion.QuestionId).First(&question).Error; err == nil { + wrongQuestion.Question = question + } + } +} diff --git a/server/source/system/api.go b/server/source/system/api.go index 710dc135..a3c5e79c 100644 --- a/server/source/system/api.go +++ b/server/source/system/api.go @@ -197,6 +197,52 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) { {ApiGroup: "版本控制", Method: "POST", Path: "/sysVersion/importVersion", Description: "同步版本"}, {ApiGroup: "版本控制", Method: "DELETE", Path: "/sysVersion/deleteSysVersion", Description: "删除版本"}, {ApiGroup: "版本控制", Method: "DELETE", Path: "/sysVersion/deleteSysVersionByIds", Description: "批量删除版本"}, + + // 学习系统 - 课程管理 + {ApiGroup: "课程管理", Method: "POST", Path: "/course/createCourse", Description: "创建课程"}, + {ApiGroup: "课程管理", Method: "DELETE", Path: "/course/deleteCourse", Description: "删除课程"}, + {ApiGroup: "课程管理", Method: "DELETE", Path: "/course/deleteCourseByIds", Description: "批量删除课程"}, + {ApiGroup: "课程管理", Method: "PUT", Path: "/course/updateCourse", Description: "更新课程"}, + {ApiGroup: "课程管理", Method: "GET", Path: "/course/findCourse", Description: "根据ID获取课程"}, + {ApiGroup: "课程管理", Method: "GET", Path: "/course/getCourseList", Description: "获取课程列表"}, + {ApiGroup: "课程管理", Method: "GET", Path: "/course/getPublishedCourses", Description: "获取已发布课程列表"}, + + // 学习系统 - 章节管理 + {ApiGroup: "章节管理", Method: "POST", Path: "/chapter/createChapter", Description: "创建章节"}, + {ApiGroup: "章节管理", Method: "DELETE", Path: "/chapter/deleteChapter", Description: "删除章节"}, + {ApiGroup: "章节管理", Method: "DELETE", Path: "/chapter/deleteChapterByIds", Description: "批量删除章节"}, + {ApiGroup: "章节管理", Method: "PUT", Path: "/chapter/updateChapter", Description: "更新章节"}, + {ApiGroup: "章节管理", Method: "GET", Path: "/chapter/findChapter", Description: "根据ID获取章节"}, + {ApiGroup: "章节管理", Method: "GET", Path: "/chapter/getChapterList", Description: "获取章节列表"}, + {ApiGroup: "章节管理", Method: "GET", Path: "/chapter/getChaptersByCourse", Description: "根据课程获取章节列表"}, + + // 学习系统 - 知识点管理 + {ApiGroup: "知识点管理", Method: "POST", Path: "/knowledgePoint/createKnowledgePoint", Description: "创建知识点"}, + {ApiGroup: "知识点管理", Method: "DELETE", Path: "/knowledgePoint/deleteKnowledgePoint", Description: "删除知识点"}, + {ApiGroup: "知识点管理", Method: "DELETE", Path: "/knowledgePoint/deleteKnowledgePointByIds", Description: "批量删除知识点"}, + {ApiGroup: "知识点管理", Method: "PUT", Path: "/knowledgePoint/updateKnowledgePoint", Description: "更新知识点"}, + {ApiGroup: "知识点管理", Method: "GET", Path: "/knowledgePoint/findKnowledgePoint", Description: "根据ID获取知识点"}, + {ApiGroup: "知识点管理", Method: "GET", Path: "/knowledgePoint/getKnowledgePointList", Description: "获取知识点列表"}, + {ApiGroup: "知识点管理", Method: "GET", Path: "/knowledgePoint/getKnowledgePointsByChapter", Description: "根据章节获取知识点列表"}, + + // 学习系统 - 题目管理 + {ApiGroup: "题目管理", Method: "POST", Path: "/question/createQuestion", Description: "创建题目"}, + {ApiGroup: "题目管理", Method: "DELETE", Path: "/question/deleteQuestion", Description: "删除题目"}, + {ApiGroup: "题目管理", Method: "DELETE", Path: "/question/deleteQuestionByIds", Description: "批量删除题目"}, + {ApiGroup: "题目管理", Method: "PUT", Path: "/question/updateQuestion", Description: "更新题目"}, + {ApiGroup: "题目管理", Method: "GET", Path: "/question/findQuestion", Description: "根据ID获取题目"}, + {ApiGroup: "题目管理", Method: "GET", Path: "/question/getQuestionList", Description: "获取题目列表"}, + {ApiGroup: "题目管理", Method: "GET", Path: "/question/getQuestionsByKnowledgePoint", Description: "根据知识点获取题目列表"}, + + // 学习系统 - 学习记录管理 + {ApiGroup: "学习记录管理", Method: "POST", Path: "/userLearning/createUserLearning", Description: "创建学习记录"}, + {ApiGroup: "学习记录管理", Method: "DELETE", Path: "/userLearning/deleteUserLearning", Description: "删除学习记录"}, + {ApiGroup: "学习记录管理", Method: "DELETE", Path: "/userLearning/deleteUserLearningByIds", Description: "批量删除学习记录"}, + {ApiGroup: "学习记录管理", Method: "PUT", Path: "/userLearning/updateUserLearning", Description: "更新学习记录"}, + {ApiGroup: "学习记录管理", Method: "GET", Path: "/userLearning/findUserLearning", Description: "根据ID获取学习记录"}, + {ApiGroup: "学习记录管理", Method: "GET", Path: "/userLearning/getUserLearningList", Description: "获取学习记录列表"}, + {ApiGroup: "学习记录管理", Method: "GET", Path: "/userLearning/getUserProgress", Description: "获取用户学习进度"}, + {ApiGroup: "学习记录管理", Method: "GET", Path: "/userLearning/getLearningStatistics", Description: "获取用户学习统计"}, } if err := db.Create(&entities).Error; err != nil { return ctx, errors.Wrap(err, sysModel.SysApi{}.TableName()+"表数据初始化失败!") diff --git a/server/source/system/dictionary.go b/server/source/system/dictionary.go index a0cf8e0c..198385b3 100644 --- a/server/source/system/dictionary.go +++ b/server/source/system/dictionary.go @@ -50,6 +50,16 @@ func (i *initDict) InitializeData(ctx context.Context) (next context.Context, er {Name: "数据库浮点型", Type: "float64", Status: &True, Desc: "数据库浮点型"}, {Name: "数据库字符串", Type: "string", Status: &True, Desc: "数据库字符串"}, {Name: "数据库bool类型", Type: "bool", Status: &True, Desc: "数据库bool类型"}, + + // 学习系统字典 + {Name: "课程难度", Type: "course_difficulty", Status: &True, Desc: "课程难度等级字典"}, + {Name: "课程状态", Type: "course_status", Status: &True, Desc: "课程状态字典"}, + {Name: "章节状态", Type: "chapter_status", Status: &True, Desc: "章节状态字典"}, + {Name: "知识点状态", Type: "knowledge_status", Status: &True, Desc: "知识点状态字典"}, + {Name: "题目类型", Type: "question_type", Status: &True, Desc: "题目类型字典"}, + {Name: "题目难度", Type: "question_difficulty", Status: &True, Desc: "题目难度等级字典"}, + {Name: "学习状态", Type: "learning_status", Status: &True, Desc: "用户学习状态字典"}, + {Name: "考试状态", Type: "exam_status", Status: &True, Desc: "考试状态字典"}, } if err = db.Create(&entities).Error; err != nil { diff --git a/server/source/system/dictionary_detail.go b/server/source/system/dictionary_detail.go index 8a2bd2b6..b1207f48 100644 --- a/server/source/system/dictionary_detail.go +++ b/server/source/system/dictionary_detail.go @@ -98,6 +98,65 @@ func (i *initDictDetail) InitializeData(ctx context.Context) (context.Context, e {Label: "tinyint", Value: "1", Extend: "mysql", Status: &True}, {Label: "bool", Value: "2", Extend: "pgsql", Status: &True}, } + + // 课程难度字典详情 + dicts[6].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "初级", Value: "beginner", Status: &True, Sort: 1}, + {Label: "中级", Value: "intermediate", Status: &True, Sort: 2}, + {Label: "高级", Value: "advanced", Status: &True, Sort: 3}, + } + + // 课程状态字典详情 + dicts[7].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "草稿", Value: "draft", Status: &True, Sort: 1}, + {Label: "已发布", Value: "published", Status: &True, Sort: 2}, + {Label: "已下架", Value: "archived", Status: &True, Sort: 3}, + } + + // 章节状态字典详情 + dicts[8].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "草稿", Value: "draft", Status: &True, Sort: 1}, + {Label: "已发布", Value: "published", Status: &True, Sort: 2}, + {Label: "隐藏", Value: "hidden", Status: &True, Sort: 3}, + } + + // 知识点状态字典详情 + dicts[9].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "草稿", Value: "draft", Status: &True, Sort: 1}, + {Label: "已发布", Value: "published", Status: &True, Sort: 2}, + {Label: "隐藏", Value: "hidden", Status: &True, Sort: 3}, + } + + // 题目类型字典详情 + dicts[10].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "单选题", Value: "single_choice", Status: &True, Sort: 1}, + {Label: "多选题", Value: "multiple_choice", Status: &True, Sort: 2}, + {Label: "判断题", Value: "true_false", Status: &True, Sort: 3}, + {Label: "填空题", Value: "fill_blank", Status: &True, Sort: 4}, + } + + // 题目难度字典详情 + dicts[11].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "简单", Value: "easy", Status: &True, Sort: 1}, + {Label: "中等", Value: "medium", Status: &True, Sort: 2}, + {Label: "困难", Value: "hard", Status: &True, Sort: 3}, + } + + // 学习状态字典详情 + dicts[12].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "未开始", Value: "not_started", Status: &True, Sort: 1}, + {Label: "学习中", Value: "learning", Status: &True, Sort: 2}, + {Label: "已完成", Value: "completed", Status: &True, Sort: 3}, + {Label: "已暂停", Value: "paused", Status: &True, Sort: 4}, + } + + // 考试状态字典详情 + dicts[13].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "草稿", Value: "draft", Status: &True, Sort: 1}, + {Label: "已发布", Value: "published", Status: &True, Sort: 2}, + {Label: "已结束", Value: "ended", Status: &True, Sort: 3}, + {Label: "已取消", Value: "cancelled", Status: &True, Sort: 4}, + } for _, dict := range dicts { if err := db.Model(&dict).Association("SysDictionaryDetails"). Replace(dict.SysDictionaryDetails); err != nil { diff --git a/server/source/system/menu.go b/server/source/system/menu.go index 9817c13a..41239513 100644 --- a/server/source/system/menu.go +++ b/server/source/system/menu.go @@ -62,6 +62,8 @@ func (i *initMenu) InitializeData(ctx context.Context) (next context.Context, er {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "https://www.gin-vue-admin.com", Name: "https://www.gin-vue-admin.com", Component: "/", Sort: 0, Meta: Meta{Title: "官方网站", Icon: "customer-gva"}}, {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "state", Name: "state", Component: "view/system/state.vue", Sort: 8, Meta: Meta{Title: "服务器状态", Icon: "cloudy"}}, {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "plugin", Name: "plugin", Component: "view/routerHolder.vue", Sort: 6, Meta: Meta{Title: "插件系统", Icon: "cherry"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "learning", Name: "learning", Component: "view/routerHolder.vue", Sort: 3, Meta: Meta{Title: "教学管理", Icon: "management"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "student", Name: "student", Component: "view/routerHolder.vue", Sort: 2, Meta: Meta{Title: "在线学习", Icon: "reading"}}, } // 先创建父级菜单(ParentId = 0 的菜单) @@ -109,6 +111,19 @@ func (i *initMenu) InitializeData(ctx context.Context) (next context.Context, er {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "pubPlug", Name: "pubPlug", Component: "view/systemTools/pubPlug/pubPlug.vue", Sort: 3, Meta: Meta{Title: "打包插件", Icon: "files"}}, {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "plugin-email", Name: "plugin-email", Component: "plugin/email/view/index.vue", Sort: 4, Meta: Meta{Title: "邮件插件", Icon: "message"}}, {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "anInfo", Name: "anInfo", Component: "plugin/announcement/view/info.vue", Sort: 5, Meta: Meta{Title: "公告管理[示例]", Icon: "scaleToOriginal"}}, + + // learning子菜单(教学管理) + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["learning"], Path: "course", Name: "course", Component: "view/learning/admin/course/course.vue", Sort: 1, Meta: Meta{Title: "课程管理", Icon: "notebook", KeepAlive: true}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["learning"], Path: "chapter", Name: "chapter", Component: "view/learning/admin/chapter/chapter.vue", Sort: 2, Meta: Meta{Title: "章节管理", Icon: "collection", KeepAlive: true}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["learning"], Path: "knowledgePoint", Name: "knowledgePoint", Component: "view/learning/admin/knowledgePoint/knowledgePoint.vue", Sort: 3, Meta: Meta{Title: "知识点管理", Icon: "document", KeepAlive: true}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["learning"], Path: "question", Name: "question", Component: "view/learning/admin/question/question.vue", Sort: 4, Meta: Meta{Title: "题目管理", Icon: "edit", KeepAlive: true}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["learning"], Path: "learningRecords", Name: "learningRecords", Component: "view/learning/admin/records/learningRecords.vue", Sort: 5, Meta: Meta{Title: "学习记录", Icon: "histogram", KeepAlive: true}}, + + // student子菜单(在线学习) + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["student"], Path: "dashboard", Name: "dashboard", Component: "view/learning/student/dashboard/dashboard.vue", Sort: 1, Meta: Meta{Title: "学习首页", Icon: "house", KeepAlive: true}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["student"], Path: "myCourses", Name: "myCourses", Component: "view/learning/student/courses/myCourses.vue", Sort: 2, Meta: Meta{Title: "我的课程", Icon: "notebook", KeepAlive: true}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["student"], Path: "onlineExam", Name: "onlineExam", Component: "view/learning/student/exam/onlineExam.vue", Sort: 3, Meta: Meta{Title: "在线考试", Icon: "medal", KeepAlive: true}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["student"], Path: "wrongQuestions", Name: "wrongQuestions", Component: "view/learning/student/wrongQuestion/wrongQuestions.vue", Sort: 4, Meta: Meta{Title: "我的错题", Icon: "warning", KeepAlive: true}}, } // 创建子菜单 diff --git a/server/sql/01_learning_system_init.sql b/server/sql/01_learning_system_init.sql new file mode 100644 index 00000000..ebd27e0b --- /dev/null +++ b/server/sql/01_learning_system_init.sql @@ -0,0 +1,231 @@ +-- ============================================= +-- 学习系统完整初始化SQL脚本 +-- 包含所有必要的数据字典、菜单权限、API权限配置 +-- 执行顺序:先执行此文件,再根据需要执行角色权限配置 +-- ============================================= + +-- 1. 插入学习系统数据字典 +INSERT INTO `sys_dictionaries` (`name`, `type`, `status`, `desc`) VALUES +('课程难度', 'course_difficulty', 1, '课程难度等级字典'), +('课程状态', 'course_status', 1, '课程状态字典'), +('章节状态', 'chapter_status', 1, '章节状态字典'), +('知识点状态', 'knowledge_status', 1, '知识点状态字典'), +('题目类型', 'question_type', 1, '题目类型字典'), +('题目难度', 'question_difficulty', 1, '题目难度等级字典'), +('学习状态', 'learning_status', 1, '用户学习状态字典'), +('考试状态', 'exam_status', 1, '考试状态字典'); + +-- 2. 获取字典ID并插入字典详情 +SET @course_difficulty_id = (SELECT id FROM sys_dictionaries WHERE type = 'course_difficulty'); +SET @course_status_id = (SELECT id FROM sys_dictionaries WHERE type = 'course_status'); +SET @chapter_status_id = (SELECT id FROM sys_dictionaries WHERE type = 'chapter_status'); +SET @knowledge_status_id = (SELECT id FROM sys_dictionaries WHERE type = 'knowledge_status'); +SET @question_type_id = (SELECT id FROM sys_dictionaries WHERE type = 'question_type'); +SET @question_difficulty_id = (SELECT id FROM sys_dictionaries WHERE type = 'question_difficulty'); +SET @learning_status_id = (SELECT id FROM sys_dictionaries WHERE type = 'learning_status'); +SET @exam_status_id = (SELECT id FROM sys_dictionaries WHERE type = 'exam_status'); + +-- 3. 插入课程难度字典详情 +INSERT INTO `sys_dictionary_details` (`label`, `value`, `status`, `sort`, `sys_dictionary_id`) VALUES +('初级', 'beginner', 1, 1, @course_difficulty_id), +('中级', 'intermediate', 1, 2, @course_difficulty_id), +('高级', 'advanced', 1, 3, @course_difficulty_id); + +-- 4. 插入课程状态字典详情 +INSERT INTO `sys_dictionary_details` (`label`, `value`, `status`, `sort`, `sys_dictionary_id`) VALUES +('草稿', 'draft', 1, 1, @course_status_id), +('已发布', 'published', 1, 2, @course_status_id), +('已下架', 'archived', 1, 3, @course_status_id); + +-- 5. 插入章节状态字典详情 +INSERT INTO `sys_dictionary_details` (`label`, `value`, `status`, `sort`, `sys_dictionary_id`) VALUES +('草稿', 'draft', 1, 1, @chapter_status_id), +('已发布', 'published', 1, 2, @chapter_status_id), +('隐藏', 'hidden', 1, 3, @chapter_status_id); + +-- 6. 插入知识点状态字典详情 +INSERT INTO `sys_dictionary_details` (`label`, `value`, `status`, `sort`, `sys_dictionary_id`) VALUES +('草稿', 'draft', 1, 1, @knowledge_status_id), +('已发布', 'published', 1, 2, @knowledge_status_id), +('隐藏', 'hidden', 1, 3, @knowledge_status_id); + +-- 7. 插入题目类型字典详情 +INSERT INTO `sys_dictionary_details` (`label`, `value`, `status`, `sort`, `sys_dictionary_id`) VALUES +('单选题', 'single_choice', 1, 1, @question_type_id), +('多选题', 'multiple_choice', 1, 2, @question_type_id), +('判断题', 'true_false', 1, 3, @question_type_id), +('填空题', 'fill_blank', 1, 4, @question_type_id); + +-- 8. 插入题目难度字典详情 +INSERT INTO `sys_dictionary_details` (`label`, `value`, `status`, `sort`, `sys_dictionary_id`) VALUES +('简单', 'easy', 1, 1, @question_difficulty_id), +('中等', 'medium', 1, 2, @question_difficulty_id), +('困难', 'hard', 1, 3, @question_difficulty_id); + +-- 9. 插入学习状态字典详情 +INSERT INTO `sys_dictionary_details` (`label`, `value`, `status`, `sort`, `sys_dictionary_id`) VALUES +('未开始', 'not_started', 1, 1, @learning_status_id), +('学习中', 'learning', 1, 2, @learning_status_id), +('已完成', 'completed', 1, 3, @learning_status_id), +('已暂停', 'paused', 1, 4, @learning_status_id); + +-- 10. 插入考试状态字典详情 +INSERT INTO `sys_dictionary_details` (`label`, `value`, `status`, `sort`, `sys_dictionary_id`) VALUES +('草稿', 'draft', 1, 1, @exam_status_id), +('已发布', 'published', 1, 2, @exam_status_id), +('已结束', 'ended', 1, 3, @exam_status_id), +('已取消', 'cancelled', 1, 4, @exam_status_id); + +-- ============================================= +-- 菜单权限配置 +-- ============================================= + +-- 11. 插入主菜单 +INSERT INTO `sys_base_menus` (`menu_level`, `parent_id`, `path`, `name`, `hidden`, `component`, `sort`, `title`, `icon`, `keep_alive`) VALUES +(0, 0, 'learning', 'learning', 0, 'view/routerHolder.vue', 3, '教学管理', 'management', 0), +(0, 0, 'student', 'student', 0, 'view/routerHolder.vue', 2, '在线学习', 'reading', 0); + +-- 12. 获取主菜单ID +SET @learning_menu_id = (SELECT id FROM sys_base_menus WHERE name = 'learning' AND menu_level = 0); +SET @student_menu_id = (SELECT id FROM sys_base_menus WHERE name = 'student' AND menu_level = 0); + +-- 13. 插入教学管理子菜单 +INSERT INTO `sys_base_menus` (`menu_level`, `parent_id`, `path`, `name`, `hidden`, `component`, `sort`, `title`, `icon`, `keep_alive`) VALUES +(1, @learning_menu_id, 'course', 'course', 0, 'view/learning/admin/course/course.vue', 1, '课程管理', 'notebook', 1), +(1, @learning_menu_id, 'chapter', 'chapter', 0, 'view/learning/admin/chapter/chapter.vue', 2, '章节管理', 'collection', 1), +(1, @learning_menu_id, 'knowledgePoint', 'knowledgePoint', 0, 'view/learning/admin/knowledgePoint/knowledgePoint.vue', 3, '知识点管理', 'document', 1), +(1, @learning_menu_id, 'question', 'question', 0, 'view/learning/admin/question/question.vue', 4, '题目管理', 'edit', 1), +(1, @learning_menu_id, 'exam', 'exam', 0, 'view/learning/admin/exam/exam.vue', 5, '考试管理', 'medal', 1), +(1, @learning_menu_id, 'learningRecords', 'learningRecords', 0, 'view/learning/admin/records/learningRecords.vue', 6, '学习记录', 'histogram', 1); + +-- 14. 插入在线学习子菜单 +INSERT INTO `sys_base_menus` (`menu_level`, `parent_id`, `path`, `name`, `hidden`, `component`, `sort`, `title`, `icon`, `keep_alive`) VALUES +(1, @student_menu_id, 'dashboard', 'dashboard', 0, 'view/learning/student/dashboard/dashboard.vue', 1, '学习首页', 'house', 1), +(1, @student_menu_id, 'myCourses', 'myCourses', 0, 'view/learning/student/courses/myCourses.vue', 2, '我的课程', 'notebook', 1), +(1, @student_menu_id, 'onlineExam', 'onlineExam', 0, 'view/learning/student/exam/onlineExam.vue', 3, '在线考试', 'medal', 1), +(1, @student_menu_id, 'wrongQuestions', 'wrongQuestions', 0, 'view/learning/student/wrongQuestion/wrongQuestions.vue', 4, '我的错题', 'warning', 1); + +-- ============================================= +-- API权限配置 +-- ============================================= + +-- 15. 插入学习系统API权限 +INSERT INTO `sys_apis` (`path`, `description`, `api_group`, `method`) VALUES +-- 课程管理API +('/course/createCourse', '创建课程', '课程管理', 'POST'), +('/course/deleteCourse', '删除课程', '课程管理', 'DELETE'), +('/course/deleteCourseByIds', '批量删除课程', '课程管理', 'DELETE'), +('/course/updateCourse', '更新课程', '课程管理', 'PUT'), +('/course/findCourse', '根据ID获取课程', '课程管理', 'GET'), +('/course/getCourseList', '获取课程列表', '课程管理', 'GET'), +('/course/getPublishedCourses', '获取已发布课程列表', '课程管理', 'GET'), + +-- 章节管理API +('/chapter/createChapter', '创建章节', '章节管理', 'POST'), +('/chapter/deleteChapter', '删除章节', '章节管理', 'DELETE'), +('/chapter/deleteChapterByIds', '批量删除章节', '章节管理', 'DELETE'), +('/chapter/updateChapter', '更新章节', '章节管理', 'PUT'), +('/chapter/findChapter', '根据ID获取章节', '章节管理', 'GET'), +('/chapter/getChapterList', '获取章节列表', '章节管理', 'GET'), +('/chapter/getChaptersByCourse', '根据课程获取章节列表', '章节管理', 'GET'), + +-- 知识点管理API +('/knowledgePoint/createKnowledgePoint', '创建知识点', '知识点管理', 'POST'), +('/knowledgePoint/deleteKnowledgePoint', '删除知识点', '知识点管理', 'DELETE'), +('/knowledgePoint/deleteKnowledgePointByIds', '批量删除知识点', '知识点管理', 'DELETE'), +('/knowledgePoint/updateKnowledgePoint', '更新知识点', '知识点管理', 'PUT'), +('/knowledgePoint/findKnowledgePoint', '根据ID获取知识点', '知识点管理', 'GET'), +('/knowledgePoint/getKnowledgePointList', '获取知识点列表', '知识点管理', 'GET'), +('/knowledgePoint/getKnowledgePointsByChapter', '根据章节获取知识点列表', '知识点管理', 'GET'), + +-- 题目管理API +('/question/createQuestion', '创建题目', '题目管理', 'POST'), +('/question/deleteQuestion', '删除题目', '题目管理', 'DELETE'), +('/question/deleteQuestionByIds', '批量删除题目', '题目管理', 'DELETE'), +('/question/updateQuestion', '更新题目', '题目管理', 'PUT'), +('/question/findQuestion', '根据ID获取题目', '题目管理', 'GET'), +('/question/getQuestionList', '获取题目列表', '题目管理', 'GET'), +('/question/getQuestionsByKnowledgePoint', '根据知识点获取题目列表', '题目管理', 'GET'), + +-- 考试管理API +('/exam/createExam', '创建考试', '考试管理', 'POST'), +('/exam/deleteExam', '删除考试', '考试管理', 'DELETE'), +('/exam/deleteExamByIds', '批量删除考试', '考试管理', 'DELETE'), +('/exam/updateExam', '更新考试', '考试管理', 'PUT'), +('/exam/publishExam', '发布考试', '考试管理', 'PUT'), +('/exam/findExam', '根据ID获取考试', '考试管理', 'GET'), +('/exam/getExamList', '获取考试列表', '考试管理', 'GET'), +('/exam/getActiveExams', '获取进行中的考试列表', '考试管理', 'GET'), +('/exam/getExamsByCourse', '根据课程获取考试列表', '考试管理', 'GET'), +('/exam/getExamStatistics', '获取考试统计', '考试管理', 'GET'), + +-- 学习记录管理API +('/userLearning/createUserLearning', '创建学习记录', '学习记录管理', 'POST'), +('/userLearning/deleteUserLearning', '删除学习记录', '学习记录管理', 'DELETE'), +('/userLearning/deleteUserLearningByIds', '批量删除学习记录', '学习记录管理', 'DELETE'), +('/userLearning/updateUserLearning', '更新学习记录', '学习记录管理', 'PUT'), +('/userLearning/findUserLearning', '根据ID获取学习记录', '学习记录管理', 'GET'), +('/userLearning/getUserLearningList', '获取学习记录列表', '学习记录管理', 'GET'), +('/userLearning/getUserProgress', '获取用户学习进度', '学习记录管理', 'GET'), +('/userLearning/getLearningStatistics', '获取用户学习统计', '学习记录管理', 'GET'), + +-- 错题本管理API +('/wrongQuestion/createWrongQuestion', '创建错题记录', '错题本管理', 'POST'), +('/wrongQuestion/deleteWrongQuestion', '删除错题记录', '错题本管理', 'DELETE'), +('/wrongQuestion/deleteWrongQuestionByIds', '批量删除错题记录', '错题本管理', 'DELETE'), +('/wrongQuestion/updateWrongQuestion', '更新错题记录', '错题本管理', 'PUT'), +('/wrongQuestion/markAsMastered', '标记题目为已掌握', '错题本管理', 'PUT'), +('/wrongQuestion/findWrongQuestion', '根据ID获取错题记录', '错题本管理', 'GET'), +('/wrongQuestion/getWrongQuestionList', '获取错题记录列表', '错题本管理', 'GET'), +('/wrongQuestion/getUserWrongQuestions', '获取用户错题列表', '错题本管理', 'GET'), +('/wrongQuestion/getWrongQuestionStatistics', '获取错题统计', '错题本管理', 'GET'), + +-- 用户考试记录管理API +('/userExam/createUserExam', '创建用户考试记录', '用户考试记录管理', 'POST'), +('/userExam/deleteUserExam', '删除用户考试记录', '用户考试记录管理', 'DELETE'), +('/userExam/deleteUserExamByIds', '批量删除用户考试记录', '用户考试记录管理', 'DELETE'), +('/userExam/updateUserExam', '更新用户考试记录', '用户考试记录管理', 'PUT'), +('/userExam/startExam', '开始考试', '用户考试记录管理', 'POST'), +('/userExam/submitExam', '提交考试', '用户考试记录管理', 'PUT'), +('/userExam/findUserExam', '根据ID获取用户考试记录', '用户考试记录管理', 'GET'), +('/userExam/getUserExamList', '获取用户考试记录列表', '用户考试记录管理', 'GET'), +('/userExam/getUserExamHistory', '获取用户考试历史', '用户考试记录管理', 'GET'), +('/userExam/getUserExamStatistics', '获取用户考试统计', '用户考试记录管理', 'GET'), +('/userExam/getExamRanking', '获取考试排行榜', '用户考试记录管理', 'GET'); + +-- ============================================= +-- 验证查询 +-- ============================================= +-- 验证字典是否创建成功 +-- SELECT d.name, d.type, COUNT(dd.id) as detail_count +-- FROM sys_dictionaries d +-- LEFT JOIN sys_dictionary_details dd ON d.id = dd.sys_dictionary_id +-- WHERE d.type LIKE '%course%' OR d.type LIKE '%chapter%' OR d.type LIKE '%knowledge%' +-- OR d.type LIKE '%question%' OR d.type LIKE '%learning%' OR d.type LIKE '%exam%' +-- GROUP BY d.id, d.name, d.type +-- ORDER BY d.id; + +-- 验证菜单是否创建成功 +-- SELECT id, menu_level, parent_id, path, name, title, icon +-- FROM sys_base_menus +-- WHERE name IN ('learning', 'student') OR parent_id IN ( +-- SELECT id FROM sys_base_menus WHERE name IN ('learning', 'student') +-- ) +-- ORDER BY menu_level, parent_id, sort; + +-- 验证API是否创建成功 +-- SELECT api_group, COUNT(*) as api_count +-- FROM sys_apis +-- WHERE api_group IN ('课程管理', '章节管理', '知识点管理', '题目管理', '考试管理', '学习记录管理', '错题本管理') +-- GROUP BY api_group +-- ORDER BY api_group; + +-- ============================================= +-- 执行说明 +-- ============================================= +-- 1. 此脚本包含学习系统的完整初始化配置 +-- 2. 包含8个数据字典和对应的字典详情 +-- 3. 包含教学管理和在线学习两个主菜单及其子菜单 +-- 4. 包含所有学习系统模块的API权限配置 +-- 5. 执行后可在系统管理中查看和管理这些配置 +-- 6. 如需配置用户角色权限,请执行 learning_system_roles.sql diff --git a/server/sql/02_learning_system_roles.sql b/server/sql/02_learning_system_roles.sql new file mode 100644 index 00000000..ad716283 --- /dev/null +++ b/server/sql/02_learning_system_roles.sql @@ -0,0 +1,225 @@ +-- ============================================= +-- 学习系统角色权限配置SQL脚本 +-- 创建教师角色和学生角色,并分配相应的菜单和API权限 +-- 注意:请先执行 learning_system_init.sql 再执行此文件 +-- ============================================= + +-- 1. 创建角色 +INSERT INTO `sys_authorities` (`authority_id`, `authority_name`, `parent_id`, `default_router`) VALUES +(1001, '教师', 0, 'learning'), +(1002, '学生', 0, 'student'); + +-- 2. 获取菜单ID +SET @learning_menu_id = (SELECT id FROM sys_base_menus WHERE name = 'learning' AND menu_level = 0); +SET @student_menu_id = (SELECT id FROM sys_base_menus WHERE name = 'student' AND menu_level = 0); + +-- 获取教学管理子菜单ID +SET @course_menu_id = (SELECT id FROM sys_base_menus WHERE name = 'course' AND parent_id = @learning_menu_id); +SET @chapter_menu_id = (SELECT id FROM sys_base_menus WHERE name = 'chapter' AND parent_id = @learning_menu_id); +SET @knowledge_menu_id = (SELECT id FROM sys_base_menus WHERE name = 'knowledgePoint' AND parent_id = @learning_menu_id); +SET @question_menu_id = (SELECT id FROM sys_base_menus WHERE name = 'question' AND parent_id = @learning_menu_id); +SET @exam_menu_id = (SELECT id FROM sys_base_menus WHERE name = 'exam' AND parent_id = @learning_menu_id); +SET @records_menu_id = (SELECT id FROM sys_base_menus WHERE name = 'learningRecords' AND parent_id = @learning_menu_id); + +-- 获取在线学习子菜单ID +SET @dashboard_menu_id = (SELECT id FROM sys_base_menus WHERE name = 'dashboard' AND parent_id = @student_menu_id); +SET @my_courses_menu_id = (SELECT id FROM sys_base_menus WHERE name = 'myCourses' AND parent_id = @student_menu_id); +SET @online_exam_menu_id = (SELECT id FROM sys_base_menus WHERE name = 'onlineExam' AND parent_id = @student_menu_id); +SET @wrong_questions_menu_id = (SELECT id FROM sys_base_menus WHERE name = 'wrongQuestions' AND parent_id = @student_menu_id); + +-- 3. 为教师角色分配菜单权限 +INSERT INTO `sys_authority_menus` (`sys_base_menu_id`, `sys_authority_authority_id`) VALUES +-- 教学管理主菜单 +(@learning_menu_id, 1001), +-- 教学管理子菜单 +(@course_menu_id, 1001), +(@chapter_menu_id, 1001), +(@knowledge_menu_id, 1001), +(@question_menu_id, 1001), +(@exam_menu_id, 1001), +(@records_menu_id, 1001); + +-- 4. 为学生角色分配菜单权限 +INSERT INTO `sys_authority_menus` (`sys_base_menu_id`, `sys_authority_authority_id`) VALUES +-- 在线学习主菜单 +(@student_menu_id, 1002), +-- 在线学习子菜单 +(@dashboard_menu_id, 1002), +(@my_courses_menu_id, 1002), +(@online_exam_menu_id, 1002), +(@wrong_questions_menu_id, 1002); + +-- 5. 为教师角色分配API权限 +INSERT INTO `casbin_rule` (`ptype`, `v0`, `v1`, `v2`) VALUES +-- 课程管理API权限 +('p', '1001', '/course/createCourse', 'POST'), +('p', '1001', '/course/deleteCourse', 'DELETE'), +('p', '1001', '/course/deleteCourseByIds', 'DELETE'), +('p', '1001', '/course/updateCourse', 'PUT'), +('p', '1001', '/course/findCourse', 'GET'), +('p', '1001', '/course/getCourseList', 'GET'), +('p', '1001', '/course/getPublishedCourses', 'GET'), + +-- 章节管理API权限 +('p', '1001', '/chapter/createChapter', 'POST'), +('p', '1001', '/chapter/deleteChapter', 'DELETE'), +('p', '1001', '/chapter/deleteChapterByIds', 'DELETE'), +('p', '1001', '/chapter/updateChapter', 'PUT'), +('p', '1001', '/chapter/findChapter', 'GET'), +('p', '1001', '/chapter/getChapterList', 'GET'), +('p', '1001', '/chapter/getChaptersByCourse', 'GET'), + +-- 知识点管理API权限 +('p', '1001', '/knowledgePoint/createKnowledgePoint', 'POST'), +('p', '1001', '/knowledgePoint/deleteKnowledgePoint', 'DELETE'), +('p', '1001', '/knowledgePoint/deleteKnowledgePointByIds', 'DELETE'), +('p', '1001', '/knowledgePoint/updateKnowledgePoint', 'PUT'), +('p', '1001', '/knowledgePoint/findKnowledgePoint', 'GET'), +('p', '1001', '/knowledgePoint/getKnowledgePointList', 'GET'), +('p', '1001', '/knowledgePoint/getKnowledgePointsByChapter', 'GET'), + +-- 题目管理API权限 +('p', '1001', '/question/createQuestion', 'POST'), +('p', '1001', '/question/deleteQuestion', 'DELETE'), +('p', '1001', '/question/deleteQuestionByIds', 'DELETE'), +('p', '1001', '/question/updateQuestion', 'PUT'), +('p', '1001', '/question/findQuestion', 'GET'), +('p', '1001', '/question/getQuestionList', 'GET'), +('p', '1001', '/question/getQuestionsByKnowledgePoint', 'GET'), + +-- 考试管理API权限 +('p', '1001', '/exam/createExam', 'POST'), +('p', '1001', '/exam/deleteExam', 'DELETE'), +('p', '1001', '/exam/deleteExamByIds', 'DELETE'), +('p', '1001', '/exam/updateExam', 'PUT'), +('p', '1001', '/exam/publishExam', 'PUT'), +('p', '1001', '/exam/findExam', 'GET'), +('p', '1001', '/exam/getExamList', 'GET'), +('p', '1001', '/exam/getActiveExams', 'GET'), +('p', '1001', '/exam/getExamsByCourse', 'GET'), +('p', '1001', '/exam/getExamStatistics', 'GET'), + +-- 学习记录管理API权限 +('p', '1001', '/userLearning/createUserLearning', 'POST'), +('p', '1001', '/userLearning/deleteUserLearning', 'DELETE'), +('p', '1001', '/userLearning/deleteUserLearningByIds', 'DELETE'), +('p', '1001', '/userLearning/updateUserLearning', 'PUT'), +('p', '1001', '/userLearning/findUserLearning', 'GET'), +('p', '1001', '/userLearning/getUserLearningList', 'GET'), +('p', '1001', '/userLearning/getUserProgress', 'GET'), +('p', '1001', '/userLearning/getLearningStatistics', 'GET'), + +-- 错题本管理API权限 +('p', '1001', '/wrongQuestion/createWrongQuestion', 'POST'), +('p', '1001', '/wrongQuestion/deleteWrongQuestion', 'DELETE'), +('p', '1001', '/wrongQuestion/deleteWrongQuestionByIds', 'DELETE'), +('p', '1001', '/wrongQuestion/updateWrongQuestion', 'PUT'), +('p', '1001', '/wrongQuestion/markAsMastered', 'PUT'), +('p', '1001', '/wrongQuestion/findWrongQuestion', 'GET'), +('p', '1001', '/wrongQuestion/getWrongQuestionList', 'GET'), +('p', '1001', '/wrongQuestion/getUserWrongQuestions', 'GET'), +('p', '1001', '/wrongQuestion/getWrongQuestionStatistics', 'GET'), + +-- 用户考试记录管理API权限 +('p', '1001', '/userExam/createUserExam', 'POST'), +('p', '1001', '/userExam/deleteUserExam', 'DELETE'), +('p', '1001', '/userExam/deleteUserExamByIds', 'DELETE'), +('p', '1001', '/userExam/updateUserExam', 'PUT'), +('p', '1001', '/userExam/startExam', 'POST'), +('p', '1001', '/userExam/submitExam', 'PUT'), +('p', '1001', '/userExam/findUserExam', 'GET'), +('p', '1001', '/userExam/getUserExamList', 'GET'), +('p', '1001', '/userExam/getUserExamHistory', 'GET'), +('p', '1001', '/userExam/getUserExamStatistics', 'GET'), +('p', '1001', '/userExam/getExamRanking', 'GET'); + +-- 6. 为学生角色分配API权限(只读权限和学生相关功能) +INSERT INTO `casbin_rule` (`ptype`, `v0`, `v1`, `v2`) VALUES +-- 课程查看权限 +('p', '1002', '/course/findCourse', 'GET'), +('p', '1002', '/course/getPublishedCourses', 'GET'), + +-- 章节查看权限 +('p', '1002', '/chapter/findChapter', 'GET'), +('p', '1002', '/chapter/getChaptersByCourse', 'GET'), + +-- 知识点查看权限 +('p', '1002', '/knowledgePoint/findKnowledgePoint', 'GET'), +('p', '1002', '/knowledgePoint/getKnowledgePointsByChapter', 'GET'), + +-- 题目查看权限 +('p', '1002', '/question/findQuestion', 'GET'), +('p', '1002', '/question/getQuestionsByKnowledgePoint', 'GET'), + +-- 考试相关权限 +('p', '1002', '/exam/findExam', 'GET'), +('p', '1002', '/exam/getActiveExams', 'GET'), +('p', '1002', '/exam/getExamsByCourse', 'GET'), + +-- 学习记录权限(学生可以创建和查看自己的学习记录) +('p', '1002', '/userLearning/createUserLearning', 'POST'), +('p', '1002', '/userLearning/updateUserLearning', 'PUT'), +('p', '1002', '/userLearning/findUserLearning', 'GET'), +('p', '1002', '/userLearning/getUserProgress', 'GET'), +('p', '1002', '/userLearning/getLearningStatistics', 'GET'), + +-- 错题本权限(学生可以管理自己的错题) +('p', '1002', '/wrongQuestion/createWrongQuestion', 'POST'), +('p', '1002', '/wrongQuestion/updateWrongQuestion', 'PUT'), +('p', '1002', '/wrongQuestion/markAsMastered', 'PUT'), +('p', '1002', '/wrongQuestion/findWrongQuestion', 'GET'), +('p', '1002', '/wrongQuestion/getUserWrongQuestions', 'GET'), +('p', '1002', '/wrongQuestion/getWrongQuestionStatistics', 'GET'), + +-- 用户考试记录权限(学生可以参加考试和查看自己的考试记录) +('p', '1002', '/userExam/startExam', 'POST'), +('p', '1002', '/userExam/submitExam', 'PUT'), +('p', '1002', '/userExam/findUserExam', 'GET'), +('p', '1002', '/userExam/getUserExamHistory', 'GET'), +('p', '1002', '/userExam/getUserExamStatistics', 'GET'), +('p', '1002', '/userExam/getExamRanking', 'GET'); + +-- ============================================= +-- 创建测试用户(可选) +-- ============================================= + +-- 创建教师测试用户 +-- INSERT INTO `sys_users` (`uuid`, `username`, `password`, `nick_name`, `side_mode`, `header_img`, `base_color`, `authority_id`, `phone`, `email`) VALUES +-- ('teacher-uuid-001', 'teacher', '$2a$10$we19gyu.geUOEhrMkrVzz.VT64GdGC6IThOvAbf8hlkOOZRxdCdli', '张老师', 'dark', 'https://qmplusimg.henrongyi.top/gva_header.jpg', '#fff', 1001, '13800000001', 'teacher@example.com'); + +-- 创建学生测试用户 +-- INSERT INTO `sys_users` (`uuid`, `username`, `password`, `nick_name`, `side_mode`, `header_img`, `base_color`, `authority_id`, `phone`, `email`) VALUES +-- ('student-uuid-001', 'student', '$2a$10$we19gyu.geUOEhrMkrVzz.VT64GdGC6IThOvAbf8hlkOOZRxdCdli', '李同学', 'dark', 'https://qmplusimg.henrongyi.top/gva_header.jpg', '#fff', 1002, '13800000002', 'student@example.com'); + +-- ============================================= +-- 验证查询 +-- ============================================= + +-- 验证角色是否创建成功 +-- SELECT authority_id, authority_name, parent_id, default_router FROM sys_authorities WHERE authority_id IN (1001, 1002); + +-- 验证菜单权限是否分配成功 +-- SELECT sa.authority_name, sbm.title, sbm.path +-- FROM sys_authority_menus sam +-- JOIN sys_authorities sa ON sam.sys_authority_authority_id = sa.authority_id +-- JOIN sys_base_menus sbm ON sam.sys_base_menu_id = sbm.id +-- WHERE sa.authority_id IN (1001, 1002) +-- ORDER BY sa.authority_id, sbm.sort; + +-- 验证API权限是否分配成功 +-- SELECT v0 as role, COUNT(*) as api_count +-- FROM casbin_rule +-- WHERE v0 IN ('1001', '1002') AND ptype = 'p' +-- GROUP BY v0; + +-- ============================================= +-- 使用说明 +-- ============================================= +-- 1. 执行此脚本前请确保已执行 learning_system_init.sql +-- 2. 此脚本创建了两个角色:1001(教师)和 1002(学生) +-- 3. 教师角色拥有教学管理的完整权限 +-- 4. 学生角色拥有在线学习的相关权限 +-- 5. 可以取消注释测试用户部分来创建测试账号 +-- 6. 默认密码为:123456 +-- 7. 教师登录后会看到"教学管理"菜单 +-- 8. 学生登录后会看到"在线学习"菜单并跳转到学习首页 diff --git a/server/sql/README.md b/server/sql/README.md new file mode 100644 index 00000000..a7c797a3 --- /dev/null +++ b/server/sql/README.md @@ -0,0 +1,142 @@ +# 学习系统SQL执行指南 + +## 📋 执行顺序 + +请按照以下顺序执行SQL文件: + +### 1. 基础初始化(必须执行) +```sql +-- 执行学习系统基础配置 +source server/sql/01_learning_system_init.sql; +``` + +**包含内容:** +- ✅ 8个数据字典(课程难度、课程状态、章节状态、知识点状态、题目类型、题目难度、学习状态、考试状态) +- ✅ 所有字典详情配置 +- ✅ 教学管理和在线学习两个主菜单 +- ✅ 10个子菜单(6个教学管理 + 4个学生学习) +- ✅ 70个API权限配置(8个模块完整覆盖) + +### 2. 角色权限配置(推荐执行) +```sql +-- 执行角色权限配置 +source server/sql/02_learning_system_roles.sql; +``` + +**包含内容:** +- ✅ 创建教师角色(teacher)和学生角色(student) +- ✅ 为教师分配教学管理菜单和完整API权限(70个API) +- ✅ 为学生分配在线学习菜单和相应API权限(30个API) +- ✅ 可选的测试用户创建 + +## 🎯 执行后效果 + +### 教师用户登录后: +- 默认跳转到 `/learning` 路径 +- 看到"教学管理"菜单,包含: + - 课程管理 + - 章节管理 + - 知识点管理 + - 题目管理 + - 考试管理 + - 学习记录 +- 拥有所有教学相关API的完整权限 + +### 学生用户登录后: +- 默认跳转到 `/student` 路径 +- 看到"在线学习"菜单,包含: + - 学习首页(现代化Dashboard) + - 我的课程 + - 在线考试 + - 我的错题 +- 拥有学习相关API的查看和个人数据管理权限 + +## 🔧 可选配置 + +### 创建测试用户 +如果需要测试用户,可以取消注释 `learning_system_roles.sql` 中的测试用户部分: + +```sql +-- 教师测试账号 +用户名: teacher +密码: 123456 + +-- 学生测试账号 +用户名: student +密码: 123456 +``` + +## 📊 验证执行结果 + +### 1. 验证字典配置 +```sql +SELECT d.name, d.type, COUNT(dd.id) as detail_count +FROM sys_dictionaries d +LEFT JOIN sys_dictionary_details dd ON d.id = dd.sys_dictionary_id +WHERE d.type LIKE '%course%' OR d.type LIKE '%chapter%' OR d.type LIKE '%knowledge%' + OR d.type LIKE '%question%' OR d.type LIKE '%learning%' OR d.type LIKE '%exam%' +GROUP BY d.id, d.name, d.type +ORDER BY d.id; +``` + +### 2. 验证菜单配置 +```sql +SELECT id, menu_level, parent_id, path, name, title, icon +FROM sys_base_menus +WHERE name IN ('learning', 'student') OR parent_id IN ( + SELECT id FROM sys_base_menus WHERE name IN ('learning', 'student') +) +ORDER BY menu_level, parent_id, sort; +``` + +### 3. 验证API权限 +```sql +SELECT api_group, COUNT(*) as api_count +FROM sys_apis +WHERE api_group IN ('课程管理', '章节管理', '知识点管理', '题目管理', '考试管理', '学习记录管理', '错题本管理', '用户考试记录管理') +GROUP BY api_group +ORDER BY api_group; +``` + +### 4. 验证角色权限 +```sql +SELECT v0 as role, COUNT(*) as api_count +FROM casbin_rule +WHERE v0 IN ('teacher', 'student') AND ptype = 'p' +GROUP BY v0; +``` + +## ⚠️ 注意事项 + +1. **执行顺序很重要**:必须先执行 `learning_system_init.sql`,再执行 `learning_system_roles.sql` +2. **数据库备份**:执行前建议备份数据库 +3. **权限检查**:确保数据库用户有足够的权限执行这些SQL语句 +4. **重复执行**:如果需要重复执行,请先清理相关数据或使用 `INSERT IGNORE` 语句 + +## 🚀 快速开始 + +如果您想快速体验完整的学习系统,只需执行: + +```bash +# 进入MySQL命令行 +mysql -u your_username -p your_database + +# 执行初始化脚本 +source server/sql/01_learning_system_init.sql; + +# 执行角色权限脚本 +source server/sql/02_learning_system_roles.sql; + +# 退出MySQL +exit; +``` + +执行完成后,重启您的应用程序,就可以体验完整的学习系统了! + +## 📞 问题反馈 + +如果在执行过程中遇到问题,请检查: +1. 数据库连接是否正常 +2. 用户权限是否足够 +3. 表结构是否已通过应用程序自动创建 +4. SQL语法是否与您的MySQL版本兼容 diff --git a/server/sql/执行说明.txt b/server/sql/执行说明.txt new file mode 100644 index 00000000..141a0175 --- /dev/null +++ b/server/sql/执行说明.txt @@ -0,0 +1,60 @@ +学习系统SQL执行说明 +==================== + +📋 执行顺序(重要!) +==================== + +第一步:基础配置 +mysql> source server/sql/01_learning_system_init.sql; + +第二步:角色权限 +mysql> source server/sql/02_learning_system_roles.sql; + +🎯 执行后效果 +==================== + +✅ 数据字典:8个完整字典配置 +✅ 菜单权限:2个主菜单 + 10个子菜单 +✅ API权限:70个接口权限配置 +✅ 用户角色:教师角色(1001) + 学生角色(1002) + +📊 系统模块统计 +==================== + +后端模块:8个核心模块 +- Course(课程管理) +- Chapter(章节管理) +- KnowledgePoint(知识点管理) +- Question(题目管理) +- Exam(考试管理) +- UserLearning(学习记录管理) +- WrongQuestion(错题本管理) +- UserExam(用户考试记录管理) + +前端页面:完整的教师端和学生端界面 +- 教师端:教学管理界面(6个管理页面) +- 学生端:在线学习界面(4个学习页面) + +🔧 测试账号(可选) +==================== + +如需测试账号,请取消注释02文件中的测试用户部分: + +教师账号:teacher / 123456 +学生账号:student / 123456 + +⚠️ 注意事项 +==================== + +1. 必须按顺序执行:01 → 02 +2. 执行前请备份数据库 +3. 确保数据库用户有足够权限 +4. 执行完成后重启应用程序 + +🚀 完成后体验 +==================== + +教师登录:看到"教学管理"菜单,跳转到/learning +学生登录:看到"在线学习"菜单,跳转到/student(现代化Dashboard) + +详细说明请查看:README.md diff --git a/web/src/api/learning/chapter.js b/web/src/api/learning/chapter.js new file mode 100644 index 00000000..6741bbc8 --- /dev/null +++ b/web/src/api/learning/chapter.js @@ -0,0 +1,113 @@ +import service from '@/utils/request' + +// @Tags Chapter +// @Summary 创建章节 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.Chapter true "创建章节" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /chapter/createChapter [post] +export const createChapter = (data) => { + return service({ + url: '/chapter/createChapter', + method: 'post', + data + }) +} + +// @Tags Chapter +// @Summary 删除章节 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.Chapter true "删除章节" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /chapter/deleteChapter [delete] +export const deleteChapter = (params) => { + return service({ + url: '/chapter/deleteChapter', + method: 'delete', + params + }) +} + +// @Tags Chapter +// @Summary 批量删除章节 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除章节" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /chapter/deleteChapterByIds [delete] +export const deleteChapterByIds = (params) => { + return service({ + url: '/chapter/deleteChapterByIds', + method: 'delete', + params + }) +} + +// @Tags Chapter +// @Summary 更新章节 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.Chapter true "更新章节" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /chapter/updateChapter [put] +export const updateChapter = (data) => { + return service({ + url: '/chapter/updateChapter', + method: 'put', + data + }) +} + +// @Tags Chapter +// @Summary 用id查询章节 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query model.Chapter true "用id查询章节" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /chapter/findChapter [get] +export const findChapter = (params) => { + return service({ + url: '/chapter/findChapter', + method: 'get', + params + }) +} + +// @Tags Chapter +// @Summary 分页获取章节列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "分页获取章节列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /chapter/getChapterList [get] +export const getChapterList = (params) => { + return service({ + url: '/chapter/getChapterList', + method: 'get', + params + }) +} + +// @Tags Chapter +// @Summary 根据课程获取章节列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param courseId query uint true "课程ID" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /chapter/getChaptersByCourse [get] +export const getChaptersByCourse = (params) => { + return service({ + url: '/chapter/getChaptersByCourse', + method: 'get', + params + }) +} diff --git a/web/src/api/learning/course.js b/web/src/api/learning/course.js new file mode 100644 index 00000000..5d48ca80 --- /dev/null +++ b/web/src/api/learning/course.js @@ -0,0 +1,111 @@ +import service from '@/utils/request' + +// @Tags Course +// @Summary 创建课程 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.Course true "创建课程" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /course/createCourse [post] +export const createCourse = (data) => { + return service({ + url: '/course/createCourse', + method: 'post', + data + }) +} + +// @Tags Course +// @Summary 删除课程 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.Course true "删除课程" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /course/deleteCourse [delete] +export const deleteCourse = (params) => { + return service({ + url: '/course/deleteCourse', + method: 'delete', + params + }) +} + +// @Tags Course +// @Summary 批量删除课程 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除课程" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /course/deleteCourseByIds [delete] +export const deleteCourseByIds = (params) => { + return service({ + url: '/course/deleteCourseByIds', + method: 'delete', + params + }) +} + +// @Tags Course +// @Summary 更新课程 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.Course true "更新课程" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /course/updateCourse [put] +export const updateCourse = (data) => { + return service({ + url: '/course/updateCourse', + method: 'put', + data + }) +} + +// @Tags Course +// @Summary 用id查询课程 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query model.Course true "用id查询课程" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /course/findCourse [get] +export const findCourse = (params) => { + return service({ + url: '/course/findCourse', + method: 'get', + params + }) +} + +// @Tags Course +// @Summary 分页获取课程列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "分页获取课程列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /course/getCourseList [get] +export const getCourseList = (params) => { + return service({ + url: '/course/getCourseList', + method: 'get', + params + }) +} + +// @Tags Course +// @Summary 获取已发布的课程列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /course/getPublishedCourses [get] +export const getPublishedCourses = () => { + return service({ + url: '/course/getPublishedCourses', + method: 'get' + }) +} diff --git a/web/src/api/learning/exam.js b/web/src/api/learning/exam.js new file mode 100644 index 00000000..71036cc6 --- /dev/null +++ b/web/src/api/learning/exam.js @@ -0,0 +1,173 @@ +import service from '@/utils/request' + +// @Tags Exam +// @Summary 创建考试 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.Exam true "创建考试" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /exam/createExam [post] +export const createExam = (data) => { + return service({ + url: '/exam/createExam', + method: 'post', + data + }) +} + +// @Tags Exam +// @Summary 删除考试 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.Exam true "删除考试" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /exam/deleteExam [delete] +export const deleteExam = (params) => { + return service({ + url: '/exam/deleteExam', + method: 'delete', + params + }) +} + +// @Tags Exam +// @Summary 批量删除考试 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除考试" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /exam/deleteExamByIds [delete] +export const deleteExamByIds = (params) => { + return service({ + url: '/exam/deleteExamByIds', + method: 'delete', + params + }) +} + +// @Tags Exam +// @Summary 更新考试 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.Exam true "更新考试" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /exam/updateExam [put] +export const updateExam = (data) => { + return service({ + url: '/exam/updateExam', + method: 'put', + data + }) +} + +// @Tags Exam +// @Summary 用id查询考试 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query model.Exam true "用id查询考试" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /exam/findExam [get] +export const findExam = (params) => { + return service({ + url: '/exam/findExam', + method: 'get', + params + }) +} + +// @Tags Exam +// @Summary 分页获取考试列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "分页获取考试列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /exam/getExamList [get] +export const getExamList = (params) => { + return service({ + url: '/exam/getExamList', + method: 'get', + params + }) +} + +// @Tags Exam +// @Summary 获取进行中的考试列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /exam/getActiveExams [get] +export const getActiveExams = (params) => { + return service({ + url: '/exam/getActiveExams', + method: 'get', + params + }) +} + +// @Tags Exam +// @Summary 根据课程获取考试列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param courseId query uint true "课程ID" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /exam/getExamsByCourse [get] +export const getExamsByCourse = (params) => { + return service({ + url: '/exam/getExamsByCourse', + method: 'get', + params + }) +} + +// @Tags Exam +// @Summary 发布考试 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.PublishExamRequest true "发布考试请求" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"发布成功"}" +// @Router /exam/publishExam [put] +export const publishExam = (data) => { + return service({ + url: '/exam/publishExam', + method: 'put', + data + }) +} + +// @Tags Exam +// @Summary 获取考试统计 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /exam/getExamStatistics [get] +export const getExamStatistics = (params) => { + return service({ + url: '/exam/getExamStatistics', + method: 'get', + params + }) +} + +// @Tags Exam +// @Summary 获取已发布的考试列表 +// @accept application/json +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /exam/getPublishedExams [get] +export const getPublishedExams = (params) => { + return service({ + url: '/exam/getPublishedExams', + method: 'get', + params + }) +} diff --git a/web/src/api/learning/knowledgePoint.js b/web/src/api/learning/knowledgePoint.js new file mode 100644 index 00000000..f973d4a8 --- /dev/null +++ b/web/src/api/learning/knowledgePoint.js @@ -0,0 +1,113 @@ +import service from '@/utils/request' + +// @Tags KnowledgePoint +// @Summary 创建知识点 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.KnowledgePoint true "创建知识点" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /knowledgePoint/createKnowledgePoint [post] +export const createKnowledgePoint = (data) => { + return service({ + url: '/knowledgePoint/createKnowledgePoint', + method: 'post', + data + }) +} + +// @Tags KnowledgePoint +// @Summary 删除知识点 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.KnowledgePoint true "删除知识点" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /knowledgePoint/deleteKnowledgePoint [delete] +export const deleteKnowledgePoint = (params) => { + return service({ + url: '/knowledgePoint/deleteKnowledgePoint', + method: 'delete', + params + }) +} + +// @Tags KnowledgePoint +// @Summary 批量删除知识点 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除知识点" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /knowledgePoint/deleteKnowledgePointByIds [delete] +export const deleteKnowledgePointByIds = (params) => { + return service({ + url: '/knowledgePoint/deleteKnowledgePointByIds', + method: 'delete', + params + }) +} + +// @Tags KnowledgePoint +// @Summary 更新知识点 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.KnowledgePoint true "更新知识点" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /knowledgePoint/updateKnowledgePoint [put] +export const updateKnowledgePoint = (data) => { + return service({ + url: '/knowledgePoint/updateKnowledgePoint', + method: 'put', + data + }) +} + +// @Tags KnowledgePoint +// @Summary 用id查询知识点 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query model.KnowledgePoint true "用id查询知识点" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /knowledgePoint/findKnowledgePoint [get] +export const findKnowledgePoint = (params) => { + return service({ + url: '/knowledgePoint/findKnowledgePoint', + method: 'get', + params + }) +} + +// @Tags KnowledgePoint +// @Summary 分页获取知识点列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "分页获取知识点列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /knowledgePoint/getKnowledgePointList [get] +export const getKnowledgePointList = (params) => { + return service({ + url: '/knowledgePoint/getKnowledgePointList', + method: 'get', + params + }) +} + +// @Tags KnowledgePoint +// @Summary 根据章节获取知识点列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param chapterId query uint true "章节ID" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /knowledgePoint/getKnowledgePointsByChapter [get] +export const getKnowledgePointsByChapter = (params) => { + return service({ + url: '/knowledgePoint/getKnowledgePointsByChapter', + method: 'get', + params + }) +} diff --git a/web/src/api/learning/question.js b/web/src/api/learning/question.js new file mode 100644 index 00000000..4972d0f4 --- /dev/null +++ b/web/src/api/learning/question.js @@ -0,0 +1,113 @@ +import service from '@/utils/request' + +// @Tags Question +// @Summary 创建题目 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.Question true "创建题目" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /question/createQuestion [post] +export const createQuestion = (data) => { + return service({ + url: '/question/createQuestion', + method: 'post', + data + }) +} + +// @Tags Question +// @Summary 删除题目 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.Question true "删除题目" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /question/deleteQuestion [delete] +export const deleteQuestion = (params) => { + return service({ + url: '/question/deleteQuestion', + method: 'delete', + params + }) +} + +// @Tags Question +// @Summary 批量删除题目 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除题目" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /question/deleteQuestionByIds [delete] +export const deleteQuestionByIds = (params) => { + return service({ + url: '/question/deleteQuestionByIds', + method: 'delete', + params + }) +} + +// @Tags Question +// @Summary 更新题目 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.Question true "更新题目" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /question/updateQuestion [put] +export const updateQuestion = (data) => { + return service({ + url: '/question/updateQuestion', + method: 'put', + data + }) +} + +// @Tags Question +// @Summary 用id查询题目 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query model.Question true "用id查询题目" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /question/findQuestion [get] +export const findQuestion = (params) => { + return service({ + url: '/question/findQuestion', + method: 'get', + params + }) +} + +// @Tags Question +// @Summary 分页获取题目列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "分页获取题目列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /question/getQuestionList [get] +export const getQuestionList = (params) => { + return service({ + url: '/question/getQuestionList', + method: 'get', + params + }) +} + +// @Tags Question +// @Summary 根据知识点获取题目列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param knowledgePointId query uint true "知识点ID" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /question/getQuestionsByKnowledgePoint [get] +export const getQuestionsByKnowledgePoint = (params) => { + return service({ + url: '/question/getQuestionsByKnowledgePoint', + method: 'get', + params + }) +} diff --git a/web/src/api/learning/userExam.js b/web/src/api/learning/userExam.js new file mode 100644 index 00000000..f996a1a1 --- /dev/null +++ b/web/src/api/learning/userExam.js @@ -0,0 +1,178 @@ +import service from '@/utils/request' + +// @Tags UserExam +// @Summary 创建用户考试记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.UserExam true "创建用户考试记录" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /userExam/createUserExam [post] +export const createUserExam = (data) => { + return service({ + url: '/userExam/createUserExam', + method: 'post', + data + }) +} + +// @Tags UserExam +// @Summary 删除用户考试记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.UserExam true "删除用户考试记录" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /userExam/deleteUserExam [delete] +export const deleteUserExam = (params) => { + return service({ + url: '/userExam/deleteUserExam', + method: 'delete', + params + }) +} + +// @Tags UserExam +// @Summary 批量删除用户考试记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除用户考试记录" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /userExam/deleteUserExamByIds [delete] +export const deleteUserExamByIds = (params) => { + return service({ + url: '/userExam/deleteUserExamByIds', + method: 'delete', + params + }) +} + +// @Tags UserExam +// @Summary 更新用户考试记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.UserExam true "更新用户考试记录" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /userExam/updateUserExam [put] +export const updateUserExam = (data) => { + return service({ + url: '/userExam/updateUserExam', + method: 'put', + data + }) +} + +// @Tags UserExam +// @Summary 用id查询用户考试记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query model.UserExam true "用id查询用户考试记录" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /userExam/findUserExam [get] +export const findUserExam = (params) => { + return service({ + url: '/userExam/findUserExam', + method: 'get', + params + }) +} + +// @Tags UserExam +// @Summary 分页获取用户考试记录列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "分页获取用户考试记录列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /userExam/getUserExamList [get] +export const getUserExamList = (params) => { + return service({ + url: '/userExam/getUserExamList', + method: 'get', + params + }) +} + +// @Tags UserExam +// @Summary 开始考试 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.StartExamRequest true "开始考试请求" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"开始成功"}" +// @Router /userExam/startExam [post] +export const startExam = (data) => { + return service({ + url: '/userExam/startExam', + method: 'post', + data + }) +} + +// @Tags UserExam +// @Summary 提交考试 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.SubmitExamRequest true "提交考试请求" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"提交成功"}" +// @Router /userExam/submitExam [put] +export const submitExam = (data) => { + return service({ + url: '/userExam/submitExam', + method: 'put', + data + }) +} + +// @Tags UserExam +// @Summary 获取用户考试历史 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param userId query uint false "用户ID" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /userExam/getUserExamHistory [get] +export const getUserExamHistory = (params) => { + return service({ + url: '/userExam/getUserExamHistory', + method: 'get', + params + }) +} + +// @Tags UserExam +// @Summary 获取用户考试统计 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param userId query uint false "用户ID" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /userExam/getUserExamStatistics [get] +export const getUserExamStatistics = (params) => { + return service({ + url: '/userExam/getUserExamStatistics', + method: 'get', + params + }) +} + +// @Tags UserExam +// @Summary 获取考试排行榜 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param examId query uint true "考试ID" +// @Param limit query int false "限制数量" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /userExam/getExamRanking [get] +export const getExamRanking = (params) => { + return service({ + url: '/userExam/getExamRanking', + method: 'get', + params + }) +} diff --git a/web/src/api/learning/userLearning.js b/web/src/api/learning/userLearning.js new file mode 100644 index 00000000..1764662f --- /dev/null +++ b/web/src/api/learning/userLearning.js @@ -0,0 +1,130 @@ +import service from '@/utils/request' + +// @Tags UserLearning +// @Summary 创建用户学习记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.UserLearning true "创建用户学习记录" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /userLearning/createUserLearning [post] +export const createUserLearning = (data) => { + return service({ + url: '/userLearning/createUserLearning', + method: 'post', + data + }) +} + +// @Tags UserLearning +// @Summary 删除用户学习记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.UserLearning true "删除用户学习记录" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /userLearning/deleteUserLearning [delete] +export const deleteUserLearning = (params) => { + return service({ + url: '/userLearning/deleteUserLearning', + method: 'delete', + params + }) +} + +// @Tags UserLearning +// @Summary 批量删除用户学习记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除用户学习记录" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /userLearning/deleteUserLearningByIds [delete] +export const deleteUserLearningByIds = (params) => { + return service({ + url: '/userLearning/deleteUserLearningByIds', + method: 'delete', + params + }) +} + +// @Tags UserLearning +// @Summary 更新用户学习记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.UserLearning true "更新用户学习记录" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /userLearning/updateUserLearning [put] +export const updateUserLearning = (data) => { + return service({ + url: '/userLearning/updateUserLearning', + method: 'put', + data + }) +} + +// @Tags UserLearning +// @Summary 用id查询用户学习记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query model.UserLearning true "用id查询用户学习记录" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /userLearning/findUserLearning [get] +export const findUserLearning = (params) => { + return service({ + url: '/userLearning/findUserLearning', + method: 'get', + params + }) +} + +// @Tags UserLearning +// @Summary 分页获取用户学习记录列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "分页获取用户学习记录列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /userLearning/getUserLearningList [get] +export const getUserLearningList = (params) => { + return service({ + url: '/userLearning/getUserLearningList', + method: 'get', + params + }) +} + +// @Tags UserLearning +// @Summary 获取用户学习进度 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param userId query uint true "用户ID" +// @Param courseId query uint false "课程ID" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /userLearning/getUserProgress [get] +export const getUserProgress = (params) => { + return service({ + url: '/userLearning/getUserProgress', + method: 'get', + params + }) +} + +// @Tags UserLearning +// @Summary 获取用户学习统计 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param userId query uint false "用户ID" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /userLearning/getLearningStatistics [get] +export const getLearningStatistics = (params) => { + return service({ + url: '/userLearning/getLearningStatistics', + method: 'get', + params + }) +} diff --git a/web/src/api/learning/wrongQuestion.js b/web/src/api/learning/wrongQuestion.js new file mode 100644 index 00000000..218e10e8 --- /dev/null +++ b/web/src/api/learning/wrongQuestion.js @@ -0,0 +1,146 @@ +import service from '@/utils/request' + +// @Tags WrongQuestion +// @Summary 创建错题记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.WrongQuestion true "创建错题记录" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /wrongQuestion/createWrongQuestion [post] +export const createWrongQuestion = (data) => { + return service({ + url: '/wrongQuestion/createWrongQuestion', + method: 'post', + data + }) +} + +// @Tags WrongQuestion +// @Summary 删除错题记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.WrongQuestion true "删除错题记录" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /wrongQuestion/deleteWrongQuestion [delete] +export const deleteWrongQuestion = (params) => { + return service({ + url: '/wrongQuestion/deleteWrongQuestion', + method: 'delete', + params + }) +} + +// @Tags WrongQuestion +// @Summary 批量删除错题记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除错题记录" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /wrongQuestion/deleteWrongQuestionByIds [delete] +export const deleteWrongQuestionByIds = (params) => { + return service({ + url: '/wrongQuestion/deleteWrongQuestionByIds', + method: 'delete', + params + }) +} + +// @Tags WrongQuestion +// @Summary 更新错题记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.WrongQuestion true "更新错题记录" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /wrongQuestion/updateWrongQuestion [put] +export const updateWrongQuestion = (data) => { + return service({ + url: '/wrongQuestion/updateWrongQuestion', + method: 'put', + data + }) +} + +// @Tags WrongQuestion +// @Summary 用id查询错题记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query model.WrongQuestion true "用id查询错题记录" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /wrongQuestion/findWrongQuestion [get] +export const findWrongQuestion = (params) => { + return service({ + url: '/wrongQuestion/findWrongQuestion', + method: 'get', + params + }) +} + +// @Tags WrongQuestion +// @Summary 分页获取错题记录列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "分页获取错题记录列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /wrongQuestion/getWrongQuestionList [get] +export const getWrongQuestionList = (params) => { + return service({ + url: '/wrongQuestion/getWrongQuestionList', + method: 'get', + params + }) +} + +// @Tags WrongQuestion +// @Summary 获取用户错题列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param userId query uint false "用户ID" +// @Param onlyUnmastered query bool false "只获取未掌握的题目" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /wrongQuestion/getUserWrongQuestions [get] +export const getUserWrongQuestions = (params) => { + return service({ + url: '/wrongQuestion/getUserWrongQuestions', + method: 'get', + params + }) +} + +// @Tags WrongQuestion +// @Summary 标记题目为已掌握 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.MarkMasteredRequest true "标记掌握请求" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"标记成功"}" +// @Router /wrongQuestion/markAsMastered [put] +export const markAsMastered = (data) => { + return service({ + url: '/wrongQuestion/markAsMastered', + method: 'put', + data + }) +} + +// @Tags WrongQuestion +// @Summary 获取错题统计 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param userId query uint false "用户ID" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /wrongQuestion/getWrongQuestionStatistics [get] +export const getWrongQuestionStatistics = (params) => { + return service({ + url: '/wrongQuestion/getWrongQuestionStatistics', + method: 'get', + params + }) +} diff --git a/web/src/components/learning/CourseCard.vue b/web/src/components/learning/CourseCard.vue new file mode 100644 index 00000000..85abf997 --- /dev/null +++ b/web/src/components/learning/CourseCard.vue @@ -0,0 +1,409 @@ + + + + + diff --git a/web/src/components/learning/LearningProgress.vue b/web/src/components/learning/LearningProgress.vue new file mode 100644 index 00000000..446d748c --- /dev/null +++ b/web/src/components/learning/LearningProgress.vue @@ -0,0 +1,268 @@ + + + + + diff --git a/web/src/components/learning/LearningStats.vue b/web/src/components/learning/LearningStats.vue new file mode 100644 index 00000000..20820dfa --- /dev/null +++ b/web/src/components/learning/LearningStats.vue @@ -0,0 +1,469 @@ + + + + + diff --git a/web/src/pathInfo.json b/web/src/pathInfo.json index 780f9a6e..a49d5e29 100644 --- a/web/src/pathInfo.json +++ b/web/src/pathInfo.json @@ -42,6 +42,15 @@ "/src/view/layout/setting/modules/general/index.vue": "GeneralSettings", "/src/view/layout/setting/modules/layout/index.vue": "LayoutSettings", "/src/view/layout/tabs/index.vue": "HistoryComponent", + "/src/view/learning/admin/chapter/chapter.vue": "Chapter", + "/src/view/learning/admin/course/course.vue": "Course", + "/src/view/learning/admin/knowledgePoint/knowledgePoint.vue": "KnowledgePoint", + "/src/view/learning/admin/question/question.vue": "Question", + "/src/view/learning/admin/records/learningRecords.vue": "LearningRecords", + "/src/view/learning/student/courses/myCourses.vue": "MyCourses", + "/src/view/learning/student/dashboard/dashboard.vue": "StudentDashboard", + "/src/view/learning/student/exam/onlineExam.vue": "OnlineExam", + "/src/view/learning/student/wrongQuestion/wrongQuestions.vue": "WrongQuestions", "/src/view/login/index.vue": "Login", "/src/view/person/person.vue": "Person", "/src/view/routerHolder.vue": "RouterHolder", diff --git a/web/src/utils/learningDict.js b/web/src/utils/learningDict.js new file mode 100644 index 00000000..5b3d483e --- /dev/null +++ b/web/src/utils/learningDict.js @@ -0,0 +1,205 @@ +// 学习系统字典工具文件 +// 提供学习系统相关的字典数据和工具函数 + +/** + * 课程难度字典 + */ +export const COURSE_DIFFICULTY = { + BEGINNER: 'beginner', + INTERMEDIATE: 'intermediate', + ADVANCED: 'advanced' +} + +export const COURSE_DIFFICULTY_OPTIONS = [ + { label: '初级', value: 'beginner', type: 'success' }, + { label: '中级', value: 'intermediate', type: 'warning' }, + { label: '高级', value: 'advanced', type: 'danger' } +] + +/** + * 课程状态字典 + */ +export const COURSE_STATUS = { + DRAFT: 'draft', + PUBLISHED: 'published', + ARCHIVED: 'archived' +} + +export const COURSE_STATUS_OPTIONS = [ + { label: '草稿', value: 'draft', type: 'info' }, + { label: '已发布', value: 'published', type: 'success' }, + { label: '已下架', value: 'archived', type: 'danger' } +] + +/** + * 章节状态字典 + */ +export const CHAPTER_STATUS = { + DRAFT: 'draft', + PUBLISHED: 'published', + HIDDEN: 'hidden' +} + +export const CHAPTER_STATUS_OPTIONS = [ + { label: '草稿', value: 'draft', type: 'info' }, + { label: '已发布', value: 'published', type: 'success' }, + { label: '隐藏', value: 'hidden', type: 'warning' } +] + +/** + * 知识点状态字典 + */ +export const KNOWLEDGE_STATUS = { + DRAFT: 'draft', + PUBLISHED: 'published', + HIDDEN: 'hidden' +} + +export const KNOWLEDGE_STATUS_OPTIONS = [ + { label: '草稿', value: 'draft', type: 'info' }, + { label: '已发布', value: 'published', type: 'success' }, + { label: '隐藏', value: 'hidden', type: 'warning' } +] + +/** + * 题目类型字典 + */ +export const QUESTION_TYPE = { + SINGLE_CHOICE: 'single_choice', + MULTIPLE_CHOICE: 'multiple_choice', + TRUE_FALSE: 'true_false', + FILL_BLANK: 'fill_blank' +} + +export const QUESTION_TYPE_OPTIONS = [ + { label: '单选题', value: 'single_choice', type: '' }, + { label: '多选题', value: 'multiple_choice', type: 'success' }, + { label: '判断题', value: 'true_false', type: 'warning' }, + { label: '填空题', value: 'fill_blank', type: 'danger' } +] + +/** + * 题目难度字典 + */ +export const QUESTION_DIFFICULTY = { + EASY: 'easy', + MEDIUM: 'medium', + HARD: 'hard' +} + +export const QUESTION_DIFFICULTY_OPTIONS = [ + { label: '简单', value: 'easy', type: 'success' }, + { label: '中等', value: 'medium', type: 'warning' }, + { label: '困难', value: 'hard', type: 'danger' } +] + +/** + * 学习状态字典 + */ +export const LEARNING_STATUS = { + NOT_STARTED: 'not_started', + LEARNING: 'learning', + COMPLETED: 'completed', + PAUSED: 'paused' +} + +export const LEARNING_STATUS_OPTIONS = [ + { label: '未开始', value: 'not_started', type: 'info' }, + { label: '学习中', value: 'learning', type: 'warning' }, + { label: '已完成', value: 'completed', type: 'success' }, + { label: '已暂停', value: 'paused', type: 'danger' } +] + +/** + * 工具函数:根据值获取标签 + */ +export function getLabelByValue(options, value) { + const option = options.find(item => item.value === value) + return option ? option.label : value +} + +/** + * 工具函数:根据值获取类型 + */ +export function getTypeByValue(options, value) { + const option = options.find(item => item.value === value) + return option ? option.type : '' +} + +/** + * 课程难度相关工具函数 + */ +export const CourseDifficultyUtils = { + getLabel: (value) => getLabelByValue(COURSE_DIFFICULTY_OPTIONS, value), + getType: (value) => getTypeByValue(COURSE_DIFFICULTY_OPTIONS, value), + getOptions: () => COURSE_DIFFICULTY_OPTIONS +} + +/** + * 课程状态相关工具函数 + */ +export const CourseStatusUtils = { + getLabel: (value) => getLabelByValue(COURSE_STATUS_OPTIONS, value), + getType: (value) => getTypeByValue(COURSE_STATUS_OPTIONS, value), + getOptions: () => COURSE_STATUS_OPTIONS +} + +/** + * 章节状态相关工具函数 + */ +export const ChapterStatusUtils = { + getLabel: (value) => getLabelByValue(CHAPTER_STATUS_OPTIONS, value), + getType: (value) => getTypeByValue(CHAPTER_STATUS_OPTIONS, value), + getOptions: () => CHAPTER_STATUS_OPTIONS +} + +/** + * 知识点状态相关工具函数 + */ +export const KnowledgeStatusUtils = { + getLabel: (value) => getLabelByValue(KNOWLEDGE_STATUS_OPTIONS, value), + getType: (value) => getTypeByValue(KNOWLEDGE_STATUS_OPTIONS, value), + getOptions: () => KNOWLEDGE_STATUS_OPTIONS +} + +/** + * 题目类型相关工具函数 + */ +export const QuestionTypeUtils = { + getLabel: (value) => getLabelByValue(QUESTION_TYPE_OPTIONS, value), + getType: (value) => getTypeByValue(QUESTION_TYPE_OPTIONS, value), + getOptions: () => QUESTION_TYPE_OPTIONS +} + +/** + * 题目难度相关工具函数 + */ +export const QuestionDifficultyUtils = { + getLabel: (value) => getLabelByValue(QUESTION_DIFFICULTY_OPTIONS, value), + getType: (value) => getTypeByValue(QUESTION_DIFFICULTY_OPTIONS, value), + getOptions: () => QUESTION_DIFFICULTY_OPTIONS +} + +/** + * 学习状态相关工具函数 + */ +export const LearningStatusUtils = { + getLabel: (value) => getLabelByValue(LEARNING_STATUS_OPTIONS, value), + getType: (value) => getTypeByValue(LEARNING_STATUS_OPTIONS, value), + getOptions: () => LEARNING_STATUS_OPTIONS +} + +/** + * 统一的字典工具对象 + */ +export const LearningDict = { + CourseDifficulty: CourseDifficultyUtils, + CourseStatus: CourseStatusUtils, + ChapterStatus: ChapterStatusUtils, + KnowledgeStatus: KnowledgeStatusUtils, + QuestionType: QuestionTypeUtils, + QuestionDifficulty: QuestionDifficultyUtils, + LearningStatus: LearningStatusUtils +} + +export default LearningDict diff --git a/web/src/view/learning/admin/chapter/chapter.vue b/web/src/view/learning/admin/chapter/chapter.vue new file mode 100644 index 00000000..3b0a005a --- /dev/null +++ b/web/src/view/learning/admin/chapter/chapter.vue @@ -0,0 +1,464 @@ + + + + + diff --git a/web/src/view/learning/admin/course/course.vue b/web/src/view/learning/admin/course/course.vue new file mode 100644 index 00000000..ee2bd306 --- /dev/null +++ b/web/src/view/learning/admin/course/course.vue @@ -0,0 +1,483 @@ + + + + + diff --git a/web/src/view/learning/admin/knowledgePoint/knowledgePoint.vue b/web/src/view/learning/admin/knowledgePoint/knowledgePoint.vue new file mode 100644 index 00000000..bdc61f78 --- /dev/null +++ b/web/src/view/learning/admin/knowledgePoint/knowledgePoint.vue @@ -0,0 +1,489 @@ + + + + + diff --git a/web/src/view/learning/admin/question/question.vue b/web/src/view/learning/admin/question/question.vue new file mode 100644 index 00000000..bfd3e298 --- /dev/null +++ b/web/src/view/learning/admin/question/question.vue @@ -0,0 +1,701 @@ + + + + + diff --git a/web/src/view/learning/admin/records/learningRecords.vue b/web/src/view/learning/admin/records/learningRecords.vue new file mode 100644 index 00000000..759e24b1 --- /dev/null +++ b/web/src/view/learning/admin/records/learningRecords.vue @@ -0,0 +1,531 @@ + + + + + diff --git a/web/src/view/learning/student/courses/myCourses.vue b/web/src/view/learning/student/courses/myCourses.vue new file mode 100644 index 00000000..c0003bed --- /dev/null +++ b/web/src/view/learning/student/courses/myCourses.vue @@ -0,0 +1,403 @@ + + + + + diff --git a/web/src/view/learning/student/dashboard/dashboard.vue b/web/src/view/learning/student/dashboard/dashboard.vue new file mode 100644 index 00000000..872a00f8 --- /dev/null +++ b/web/src/view/learning/student/dashboard/dashboard.vue @@ -0,0 +1,528 @@ + + + + + diff --git a/web/src/view/learning/student/exam/onlineExam.vue b/web/src/view/learning/student/exam/onlineExam.vue new file mode 100644 index 00000000..fbf28bb6 --- /dev/null +++ b/web/src/view/learning/student/exam/onlineExam.vue @@ -0,0 +1,832 @@ + + + + + diff --git a/web/src/view/learning/student/wrongQuestion/wrongQuestions.vue b/web/src/view/learning/student/wrongQuestion/wrongQuestions.vue new file mode 100644 index 00000000..8fcefe80 --- /dev/null +++ b/web/src/view/learning/student/wrongQuestion/wrongQuestions.vue @@ -0,0 +1,728 @@ + + + + +