diff --git a/memora-api/internal/repository/memory_repository.go b/memora-api/internal/repository/memory_repository.go new file mode 100644 index 0000000..8b6fd20 --- /dev/null +++ b/memora-api/internal/repository/memory_repository.go @@ -0,0 +1,65 @@ +package repository + +import ( + "time" + + "memora-api/internal/model" + + "gorm.io/gorm" +) + +type MemoryRepository struct { + db *gorm.DB +} + +func NewMemoryRepository(db *gorm.DB) *MemoryRepository { + return &MemoryRepository{db: db} +} + +func (r *MemoryRepository) Create(record *model.MemoryRecord) error { + return r.db.Create(record).Error +} + +func (r *MemoryRepository) FindByWord(userID, wordID int64) (*model.MemoryRecord, error) { + var record model.MemoryRecord + if err := r.db.Where("word_id = ? AND user_id = ?", wordID, userID).First(&record).Error; err != nil { + return nil, err + } + return &record, nil +} + +func (r *MemoryRepository) FindByID(userID, recordID int64) (*model.MemoryRecord, error) { + var record model.MemoryRecord + if err := r.db.Preload("Word").Where("id = ? AND user_id = ?", recordID, userID).First(&record).Error; err != nil { + return nil, err + } + return &record, nil +} + +func (r *MemoryRepository) Save(record *model.MemoryRecord) error { + return r.db.Save(record).Error +} + +func (r *MemoryRepository) Due(userID int64, limit int) ([]model.MemoryRecord, error) { + var records []model.MemoryRecord + if err := r.db.Preload("Word").Where("user_id = ? AND (next_review_at <= ? OR next_review_at IS NULL)", userID, time.Now()).Limit(limit).Find(&records).Error; err != nil { + return nil, err + } + return records, nil +} + +func (r *MemoryRepository) ListByUser(userID int64, limit int) ([]model.MemoryRecord, error) { + var records []model.MemoryRecord + if err := r.db.Preload("Word").Where("user_id = ?", userID).Limit(limit).Find(&records).Error; err != nil { + return nil, err + } + return records, nil +} + +func (r *MemoryRepository) CountOverview(userID int64) (total, mastered, needReview, todayReviewed int64) { + r.db.Model(&model.MemoryRecord{}).Where("user_id = ?", userID).Count(&total) + r.db.Model(&model.MemoryRecord{}).Where("user_id = ? AND mastery_level >= 4", userID).Count(&mastered) + r.db.Model(&model.MemoryRecord{}).Where("user_id = ? AND next_review_at <= ?", userID, time.Now()).Count(&needReview) + r.db.Model(&model.MemoryRecord{}).Where("user_id = ? AND last_reviewed_at >= ?", userID, time.Now().Format("2006-01-02")).Count(&todayReviewed) + return +} diff --git a/memora-api/internal/repository/word_repository.go b/memora-api/internal/repository/word_repository.go new file mode 100644 index 0000000..a5b8692 --- /dev/null +++ b/memora-api/internal/repository/word_repository.go @@ -0,0 +1,45 @@ +package repository + +import ( + "memora-api/internal/model" + + "gorm.io/gorm" +) + +type WordRepository struct { + db *gorm.DB +} + +func NewWordRepository(db *gorm.DB) *WordRepository { + return &WordRepository{db: db} +} + +func (r *WordRepository) FindByWord(word string) (*model.Word, error) { + var w model.Word + if err := r.db.Where("word = ?", word).First(&w).Error; err != nil { + return nil, err + } + return &w, nil +} + +func (r *WordRepository) Create(word *model.Word) error { + return r.db.Create(word).Error +} + +func (r *WordRepository) List(limit, offset int) ([]model.Word, int64, error) { + var words []model.Word + var total int64 + r.db.Model(&model.Word{}).Count(&total) + if err := r.db.Limit(limit).Offset(offset).Order("created_at DESC").Find(&words).Error; err != nil { + return nil, 0, err + } + return words, total, nil +} + +func (r *WordRepository) Latest(limit int) ([]model.Word, error) { + var words []model.Word + if err := r.db.Order("created_at DESC").Limit(limit).Find(&words).Error; err != nil { + return nil, err + } + return words, nil +} diff --git a/memora-api/internal/service/word.go b/memora-api/internal/service/word.go index 5dbc5f5..ca38647 100644 --- a/memora-api/internal/service/word.go +++ b/memora-api/internal/service/word.go @@ -16,6 +16,7 @@ import ( "memora-api/internal/config" "memora-api/internal/model" + "memora-api/internal/repository" "memora-api/internal/request" "memora-api/internal/response" @@ -23,11 +24,17 @@ import ( ) type WordService struct { - db *gorm.DB + db *gorm.DB + wordRepo *repository.WordRepository + memoryRepo *repository.MemoryRepository } func NewWordService(db *gorm.DB) *WordService { - return &WordService{db: db} + return &WordService{ + db: db, + wordRepo: repository.NewWordRepository(db), + memoryRepo: repository.NewMemoryRepository(db), + } } type dictAPIEntry struct { @@ -170,13 +177,11 @@ func (s *WordService) DownloadAudio(url, filePath string) 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 { + if w, err := s.wordRepo.FindByWord(word); err == nil { // 单词已存在,更新记忆记录 - s.updateMemoryRecord(userID, existingWord.ID, true) - return &existingWord, nil + s.updateMemoryRecord(userID, w.ID, true) + return w, nil } // 解析有道API响应 @@ -267,7 +272,7 @@ func (s *WordService) SaveWord(userID int64, word string, youdaoResp *model.Youd ExampleSentence: exampleSentence, } - if err := s.db.Create(&newWord).Error; err != nil { + if err := s.wordRepo.Create(&newWord); err != nil { return nil, err } @@ -287,18 +292,19 @@ func (s *WordService) createMemoryRecord(userID, wordID int64) error { TotalCount: 0, MasteryLevel: 0, } - return s.db.Create(&record).Error + return s.memoryRepo.Create(&record) } // 更新记忆记录 func (s *WordService) updateMemoryRecord(userID, wordID int64, correct bool) error { - var record model.MemoryRecord - if err := s.db.Where("word_id = ? AND user_id = ?", wordID, userID).First(&record).Error; err != nil { + record, err := s.memoryRepo.FindByWord(userID, wordID) + if 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 { + record, err = s.memoryRepo.FindByWord(userID, wordID) + if err != nil { return err } } else { @@ -323,22 +329,20 @@ func (s *WordService) updateMemoryRecord(userID, wordID int64, correct bool) err nextReview := now.Add(reviewInterval) record.NextReviewAt = &nextReview - return s.db.Save(&record).Error + return s.memoryRepo.Save(record) } // 获取待复习单词 func (s *WordService) GetReviewWords(userID int64, mode string, limit int) ([]model.MemoryRecord, error) { - var records []model.MemoryRecord - - 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 { + records, err := s.memoryRepo.Due(userID, limit) + if err != nil { return nil, err } // 如果没有需要复习的,随机获取一些 if len(records) == 0 { - if err := s.db.Preload("Word").Where("user_id = ?", userID).Limit(limit).Find(&records).Error; err != nil { + records, err = s.memoryRepo.ListByUser(userID, limit) + if err != nil { return nil, err } } @@ -372,8 +376,8 @@ func containsAny(def string, ans string) bool { // 提交复习答案 func (s *WordService) SubmitReviewAnswer(userID int64, req request.ReviewAnswerRequest) (*response.ReviewResult, error) { - var record model.MemoryRecord - if err := s.db.Preload("Word").Where("id = ? AND user_id = ?", req.RecordID, userID).First(&record).Error; err != nil { + record, err := s.memoryRepo.FindByID(userID, req.RecordID) + if err != nil { return nil, err } @@ -409,29 +413,12 @@ func (s *WordService) SubmitReviewAnswer(userID int64, req request.ReviewAnswerR // 获取所有单词 func (s *WordService) GetAllWords(limit, offset int) ([]model.Word, int64, error) { - var words []model.Word - var total int64 - - s.db.Model(&model.Word{}).Count(&total) - - if err := s.db.Limit(limit).Offset(offset).Order("created_at DESC").Find(&words).Error; err != nil { - return nil, 0, err - } - - return words, total, nil + return s.wordRepo.List(limit, offset) } // 获取记忆统计 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.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) + totalWords, masteredWords, needReview, todayReviewed := s.memoryRepo.CountOverview(userID) return map[string]interface{}{ "total_words": totalWords, @@ -445,8 +432,8 @@ func (s *WordService) CreateStudySession(userID int64, limit int) ([]model.Word, 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 { + words, err := s.wordRepo.Latest(limit) + if err != nil { return nil, err } for i := range words { @@ -462,13 +449,14 @@ func (s *WordService) SubmitStudyAnswer(userID int64, req request.SubmitStudyAns 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 { + record, err := s.memoryRepo.FindByWord(userID, req.WordID) + if 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 { + record, err = s.memoryRepo.FindByWord(userID, req.WordID) + if err != nil { return nil, err } } else { diff --git a/memora-web/src/services/api/index.ts b/memora-web/src/services/api/index.ts index d1d29eb..31ae85f 100644 --- a/memora-web/src/services/api/index.ts +++ b/memora-web/src/services/api/index.ts @@ -2,3 +2,4 @@ export * from './types' export * from './stats' export * from './words' export * from './review' +export * from './study' diff --git a/memora-web/src/services/api/study.ts b/memora-web/src/services/api/study.ts new file mode 100644 index 0000000..259a15e --- /dev/null +++ b/memora-web/src/services/api/study.ts @@ -0,0 +1,16 @@ +import { http } from '../http' +import type { ReviewMode, ReviewResult, Word } from './types' + +export async function createStudySession(limit = 10) { + const res = await http.post<{ data: Word[] }>('/study/sessions', { limit }) + return res.data +} + +export async function submitStudyAnswer(payload: { wordId: number; answer: string; mode: ReviewMode }) { + const res = await http.post<{ data: ReviewResult }>('/study/answers', { + word_id: payload.wordId, + answer: payload.answer, + mode: payload.mode + }) + return res.data +}