From eb88494c5ae130c151f6b94807d22d60b38c96a6 Mon Sep 17 00:00:00 2001 From: wsy182 <2392948297@qq.com> Date: Fri, 27 Feb 2026 11:14:25 +0800 Subject: [PATCH] feat: add study session APIs and per-user review isolation --- memora-api/internal/handler/word.go | 49 +++++++++++++-- memora-api/internal/request/word.go | 10 ++++ memora-api/internal/router/router.go | 2 + memora-api/internal/service/word.go | 89 +++++++++++++++++++++------- 4 files changed, 126 insertions(+), 24 deletions(-) diff --git a/memora-api/internal/handler/word.go b/memora-api/internal/handler/word.go index 92d704e..7925862 100644 --- a/memora-api/internal/handler/word.go +++ b/memora-api/internal/handler/word.go @@ -16,6 +16,18 @@ type WordHandler struct { wordService *service.WordService } +func userIDFromContext(c *gin.Context) int64 { + v, ok := c.Get("user_id") + if !ok { + return 1 + } + uid, ok := v.(int64) + if !ok || uid <= 0 { + return 1 + } + return uid +} + func NewWordHandler(wordService *service.WordService) *WordHandler { return &WordHandler{wordService: wordService} } @@ -41,7 +53,7 @@ func (h *WordHandler) AddWord(c *gin.Context) { } // 保存到数据库 - word, err := h.wordService.SaveWord(req.Word, youdaoResp) + word, err := h.wordService.SaveWord(userIDFromContext(c), req.Word, youdaoResp) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "保存失败: " + err.Error()}) return @@ -58,7 +70,7 @@ func (h *WordHandler) GetReviewWords(c *gin.Context) { mode := c.DefaultQuery("mode", "spelling") limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10")) - words, err := h.wordService.GetReviewWords(mode, limit) + words, err := h.wordService.GetReviewWords(userIDFromContext(c), mode, limit) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -75,7 +87,7 @@ func (h *WordHandler) SubmitReview(c *gin.Context) { return } - result, err := h.wordService.SubmitReviewAnswer(req) + result, err := h.wordService.SubmitReviewAnswer(userIDFromContext(c), req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -103,7 +115,7 @@ func (h *WordHandler) GetWords(c *gin.Context) { // 获取统计信息 func (h *WordHandler) GetStatistics(c *gin.Context) { - stats, err := h.wordService.GetStatistics() + stats, err := h.wordService.GetStatistics(userIDFromContext(c)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -138,3 +150,32 @@ func (h *WordHandler) GetAudio(c *gin.Context) { path := "./audio/" + filename c.File(path) } + +func (h *WordHandler) CreateStudySession(c *gin.Context) { + var req request.CreateStudySessionRequest + _ = c.ShouldBindJSON(&req) + + words, err := h.wordService.CreateStudySession(userIDFromContext(c), req.Limit) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"data": words}) +} + +func (h *WordHandler) SubmitStudyAnswer(c *gin.Context) { + var req request.SubmitStudyAnswerRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + result, err := h.wordService.SubmitStudyAnswer(userIDFromContext(c), req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"data": result}) +} diff --git a/memora-api/internal/request/word.go b/memora-api/internal/request/word.go index cbff434..976c5e2 100644 --- a/memora-api/internal/request/word.go +++ b/memora-api/internal/request/word.go @@ -9,3 +9,13 @@ type ReviewAnswerRequest struct { Answer string `json:"answer" binding:"required"` Mode string `json:"mode" binding:"required"` // spelling, en2cn, cn2en } + +type CreateStudySessionRequest struct { + Limit int `json:"limit"` +} + +type SubmitStudyAnswerRequest struct { + WordID int64 `json:"word_id" binding:"required"` + Answer string `json:"answer" binding:"required"` + Mode string `json:"mode" binding:"required"` // spelling, en2cn, cn2en +} diff --git a/memora-api/internal/router/router.go b/memora-api/internal/router/router.go index d6b20b8..866a40a 100644 --- a/memora-api/internal/router/router.go +++ b/memora-api/internal/router/router.go @@ -22,6 +22,8 @@ func New(wordHandler *handler.WordHandler, authHandler *handler.AuthHandler) *gi { protected.POST("/words", wordHandler.AddWord) protected.GET("/words", wordHandler.GetWords) + protected.POST("/study/sessions", wordHandler.CreateStudySession) + protected.POST("/study/answers", wordHandler.SubmitStudyAnswer) protected.GET("/review", wordHandler.GetReviewWords) protected.POST("/review", wordHandler.SubmitReview) protected.GET("/stats", wordHandler.GetStatistics) diff --git a/memora-api/internal/service/word.go b/memora-api/internal/service/word.go index ec31b1d..5dbc5f5 100644 --- a/memora-api/internal/service/word.go +++ b/memora-api/internal/service/word.go @@ -4,6 +4,7 @@ import ( "crypto/md5" "encoding/hex" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -168,13 +169,13 @@ func (s *WordService) DownloadAudio(url, filePath string) error { } // 保存单词到数据库 -func (s *WordService) SaveWord(word string, youdaoResp *model.YoudaoResponse) (*model.Word, error) { +func (s *WordService) SaveWord(userID int64, word string, youdaoResp *model.YoudaoResponse) (*model.Word, error) { var existingWord model.Word // 检查单词是否已存在 if err := s.db.Where("word = ?", word).First(&existingWord).Error; err == nil { // 单词已存在,更新记忆记录 - s.updateMemoryRecord(existingWord.ID, true) + s.updateMemoryRecord(userID, existingWord.ID, true) return &existingWord, nil } @@ -271,17 +272,17 @@ func (s *WordService) SaveWord(word string, youdaoResp *model.YoudaoResponse) (* } // 创建记忆记录 + 记一次"背过" - _ = s.createMemoryRecord(newWord.ID) - _ = s.updateMemoryRecord(newWord.ID, true) + _ = s.createMemoryRecord(userID, newWord.ID) + _ = s.updateMemoryRecord(userID, newWord.ID, true) return &newWord, nil } // 创建记忆记录 -func (s *WordService) createMemoryRecord(wordID int64) error { +func (s *WordService) createMemoryRecord(userID, wordID int64) error { record := model.MemoryRecord{ WordID: wordID, - UserID: 1, + UserID: userID, CorrectCount: 0, TotalCount: 0, MasteryLevel: 0, @@ -290,10 +291,19 @@ func (s *WordService) createMemoryRecord(wordID int64) error { } // 更新记忆记录 -func (s *WordService) updateMemoryRecord(wordID int64, correct bool) error { +func (s *WordService) updateMemoryRecord(userID, wordID int64, correct bool) error { var record model.MemoryRecord - if err := s.db.Where("word_id = ?", wordID).First(&record).Error; err != nil { - return err + if err := s.db.Where("word_id = ? AND user_id = ?", wordID, userID).First(&record).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + if createErr := s.createMemoryRecord(userID, wordID); createErr != nil { + return createErr + } + if err = s.db.Where("word_id = ? AND user_id = ?", wordID, userID).First(&record).Error; err != nil { + return err + } + } else { + return err + } } record.TotalCount++ @@ -317,10 +327,10 @@ func (s *WordService) updateMemoryRecord(wordID int64, correct bool) error { } // 获取待复习单词 -func (s *WordService) GetReviewWords(mode string, limit int) ([]model.MemoryRecord, error) { +func (s *WordService) GetReviewWords(userID int64, mode string, limit int) ([]model.MemoryRecord, error) { var records []model.MemoryRecord - query := s.db.Preload("Word").Where("next_review_at <= ? OR next_review_at IS NULL", time.Now()) + query := s.db.Preload("Word").Where("user_id = ? AND (next_review_at <= ? OR next_review_at IS NULL)", userID, time.Now()) if err := query.Limit(limit).Find(&records).Error; err != nil { return nil, err @@ -328,7 +338,7 @@ func (s *WordService) GetReviewWords(mode string, limit int) ([]model.MemoryReco // 如果没有需要复习的,随机获取一些 if len(records) == 0 { - if err := s.db.Preload("Word").Limit(limit).Find(&records).Error; err != nil { + if err := s.db.Preload("Word").Where("user_id = ?", userID).Limit(limit).Find(&records).Error; err != nil { return nil, err } } @@ -361,9 +371,9 @@ func containsAny(def string, ans string) bool { } // 提交复习答案 -func (s *WordService) SubmitReviewAnswer(req request.ReviewAnswerRequest) (*response.ReviewResult, error) { +func (s *WordService) SubmitReviewAnswer(userID int64, req request.ReviewAnswerRequest) (*response.ReviewResult, error) { var record model.MemoryRecord - if err := s.db.Preload("Word").Where("id = ?", req.RecordID).First(&record).Error; err != nil { + if err := s.db.Preload("Word").Where("id = ? AND user_id = ?", req.RecordID, userID).First(&record).Error; err != nil { return nil, err } @@ -387,7 +397,7 @@ func (s *WordService) SubmitReviewAnswer(req request.ReviewAnswerRequest) (*resp } // 更新记忆记录 - s.updateMemoryRecord(record.WordID, correct) + s.updateMemoryRecord(userID, record.WordID, correct) return &response.ReviewResult{ Word: word, @@ -412,16 +422,16 @@ func (s *WordService) GetAllWords(limit, offset int) ([]model.Word, int64, error } // 获取记忆统计 -func (s *WordService) GetStatistics() (map[string]interface{}, error) { +func (s *WordService) GetStatistics(userID int64) (map[string]interface{}, error) { var totalWords int64 var masteredWords int64 var needReview int64 var todayReviewed int64 - s.db.Model(&model.Word{}).Count(&totalWords) - s.db.Model(&model.MemoryRecord{}).Where("mastery_level >= 4").Count(&masteredWords) - s.db.Model(&model.MemoryRecord{}).Where("next_review_at <= ?", time.Now()).Count(&needReview) - s.db.Model(&model.MemoryRecord{}).Where("last_reviewed_at >= ?", time.Now().Format("2006-01-02")).Count(&todayReviewed) + s.db.Model(&model.MemoryRecord{}).Where("user_id = ?", userID).Count(&totalWords) + s.db.Model(&model.MemoryRecord{}).Where("user_id = ? AND mastery_level >= 4", userID).Count(&masteredWords) + s.db.Model(&model.MemoryRecord{}).Where("user_id = ? AND next_review_at <= ?", userID, time.Now()).Count(&needReview) + s.db.Model(&model.MemoryRecord{}).Where("user_id = ? AND last_reviewed_at >= ?", userID, time.Now().Format("2006-01-02")).Count(&todayReviewed) return map[string]interface{}{ "total_words": totalWords, @@ -430,3 +440,42 @@ func (s *WordService) GetStatistics() (map[string]interface{}, error) { "today_reviewed": todayReviewed, }, nil } + +func (s *WordService) CreateStudySession(userID int64, limit int) ([]model.Word, error) { + if limit <= 0 || limit > 50 { + limit = 10 + } + var words []model.Word + if err := s.db.Order("created_at DESC").Limit(limit).Find(&words).Error; err != nil { + return nil, err + } + for i := range words { + _ = s.createMemoryRecord(userID, words[i].ID) + } + return words, nil +} + +func (s *WordService) SubmitStudyAnswer(userID int64, req request.SubmitStudyAnswerRequest) (*response.ReviewResult, error) { + reviewReq := request.ReviewAnswerRequest{ + RecordID: 0, + Answer: req.Answer, + Mode: req.Mode, + } + + var record model.MemoryRecord + if err := s.db.Where("user_id = ? AND word_id = ?", userID, req.WordID).First(&record).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + if createErr := s.createMemoryRecord(userID, req.WordID); createErr != nil { + return nil, createErr + } + if err = s.db.Where("user_id = ? AND word_id = ?", userID, req.WordID).First(&record).Error; err != nil { + return nil, err + } + } else { + return nil, err + } + } + + reviewReq.RecordID = record.ID + return s.SubmitReviewAnswer(userID, reviewReq) +}