feat: add study session APIs and per-user review isolation

This commit is contained in:
2026-02-27 11:14:25 +08:00
parent 60ce70f532
commit eb88494c5a
4 changed files with 126 additions and 24 deletions

View File

@@ -16,6 +16,18 @@ type WordHandler struct {
wordService *service.WordService 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 { func NewWordHandler(wordService *service.WordService) *WordHandler {
return &WordHandler{wordService: wordService} 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 { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存失败: " + err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": "保存失败: " + err.Error()})
return return
@@ -58,7 +70,7 @@ func (h *WordHandler) GetReviewWords(c *gin.Context) {
mode := c.DefaultQuery("mode", "spelling") mode := c.DefaultQuery("mode", "spelling")
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10")) 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 { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
@@ -75,7 +87,7 @@ func (h *WordHandler) SubmitReview(c *gin.Context) {
return return
} }
result, err := h.wordService.SubmitReviewAnswer(req) result, err := h.wordService.SubmitReviewAnswer(userIDFromContext(c), req)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
@@ -103,7 +115,7 @@ func (h *WordHandler) GetWords(c *gin.Context) {
// 获取统计信息 // 获取统计信息
func (h *WordHandler) GetStatistics(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 { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
@@ -138,3 +150,32 @@ func (h *WordHandler) GetAudio(c *gin.Context) {
path := "./audio/" + filename path := "./audio/" + filename
c.File(path) 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})
}

View File

@@ -9,3 +9,13 @@ type ReviewAnswerRequest struct {
Answer string `json:"answer" binding:"required"` Answer string `json:"answer" binding:"required"`
Mode string `json:"mode" binding:"required"` // spelling, en2cn, cn2en 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
}

View File

@@ -22,6 +22,8 @@ func New(wordHandler *handler.WordHandler, authHandler *handler.AuthHandler) *gi
{ {
protected.POST("/words", wordHandler.AddWord) protected.POST("/words", wordHandler.AddWord)
protected.GET("/words", wordHandler.GetWords) protected.GET("/words", wordHandler.GetWords)
protected.POST("/study/sessions", wordHandler.CreateStudySession)
protected.POST("/study/answers", wordHandler.SubmitStudyAnswer)
protected.GET("/review", wordHandler.GetReviewWords) protected.GET("/review", wordHandler.GetReviewWords)
protected.POST("/review", wordHandler.SubmitReview) protected.POST("/review", wordHandler.SubmitReview)
protected.GET("/stats", wordHandler.GetStatistics) protected.GET("/stats", wordHandler.GetStatistics)

View File

@@ -4,6 +4,7 @@ import (
"crypto/md5" "crypto/md5"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "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 var existingWord model.Word
// 检查单词是否已存在 // 检查单词是否已存在
if err := s.db.Where("word = ?", word).First(&existingWord).Error; err == nil { 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 return &existingWord, nil
} }
@@ -271,17 +272,17 @@ func (s *WordService) SaveWord(word string, youdaoResp *model.YoudaoResponse) (*
} }
// 创建记忆记录 + 记一次"背过" // 创建记忆记录 + 记一次"背过"
_ = s.createMemoryRecord(newWord.ID) _ = s.createMemoryRecord(userID, newWord.ID)
_ = s.updateMemoryRecord(newWord.ID, true) _ = s.updateMemoryRecord(userID, newWord.ID, true)
return &newWord, nil return &newWord, nil
} }
// 创建记忆记录 // 创建记忆记录
func (s *WordService) createMemoryRecord(wordID int64) error { func (s *WordService) createMemoryRecord(userID, wordID int64) error {
record := model.MemoryRecord{ record := model.MemoryRecord{
WordID: wordID, WordID: wordID,
UserID: 1, UserID: userID,
CorrectCount: 0, CorrectCount: 0,
TotalCount: 0, TotalCount: 0,
MasteryLevel: 0, MasteryLevel: 0,
@@ -290,11 +291,20 @@ 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 var record model.MemoryRecord
if err := s.db.Where("word_id = ?", wordID).First(&record).Error; err != nil { 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 return err
} }
} else {
return err
}
}
record.TotalCount++ record.TotalCount++
if correct { if correct {
@@ -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 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 { if err := query.Limit(limit).Find(&records).Error; err != nil {
return nil, err return nil, err
@@ -328,7 +338,7 @@ func (s *WordService) GetReviewWords(mode string, limit int) ([]model.MemoryReco
// 如果没有需要复习的,随机获取一些 // 如果没有需要复习的,随机获取一些
if len(records) == 0 { 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 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 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 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{ return &response.ReviewResult{
Word: word, 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 totalWords int64
var masteredWords int64 var masteredWords int64
var needReview int64 var needReview int64
var todayReviewed int64 var todayReviewed int64
s.db.Model(&model.Word{}).Count(&totalWords) s.db.Model(&model.MemoryRecord{}).Where("user_id = ?", userID).Count(&totalWords)
s.db.Model(&model.MemoryRecord{}).Where("mastery_level >= 4").Count(&masteredWords) s.db.Model(&model.MemoryRecord{}).Where("user_id = ? AND mastery_level >= 4", userID).Count(&masteredWords)
s.db.Model(&model.MemoryRecord{}).Where("next_review_at <= ?", time.Now()).Count(&needReview) s.db.Model(&model.MemoryRecord{}).Where("user_id = ? AND next_review_at <= ?", userID, 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 = ? AND last_reviewed_at >= ?", userID, time.Now().Format("2006-01-02")).Count(&todayReviewed)
return map[string]interface{}{ return map[string]interface{}{
"total_words": totalWords, "total_words": totalWords,
@@ -430,3 +440,42 @@ func (s *WordService) GetStatistics() (map[string]interface{}, error) {
"today_reviewed": todayReviewed, "today_reviewed": todayReviewed,
}, nil }, 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)
}