feat: add study session APIs and per-user review isolation
This commit is contained in:
@@ -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})
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user