refactor: introduce repositories and expose study api client

This commit is contained in:
2026-02-27 11:19:13 +08:00
parent eb88494c5a
commit 9a5e8cb58f
5 changed files with 160 additions and 45 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -16,6 +16,7 @@ import (
"memora-api/internal/config" "memora-api/internal/config"
"memora-api/internal/model" "memora-api/internal/model"
"memora-api/internal/repository"
"memora-api/internal/request" "memora-api/internal/request"
"memora-api/internal/response" "memora-api/internal/response"
@@ -24,10 +25,16 @@ import (
type WordService struct { type WordService struct {
db *gorm.DB db *gorm.DB
wordRepo *repository.WordRepository
memoryRepo *repository.MemoryRepository
} }
func NewWordService(db *gorm.DB) *WordService { 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 { 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) { 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) s.updateMemoryRecord(userID, w.ID, true)
return &existingWord, nil return w, nil
} }
// 解析有道API响应 // 解析有道API响应
@@ -267,7 +272,7 @@ func (s *WordService) SaveWord(userID int64, word string, youdaoResp *model.Youd
ExampleSentence: exampleSentence, ExampleSentence: exampleSentence,
} }
if err := s.db.Create(&newWord).Error; err != nil { if err := s.wordRepo.Create(&newWord); err != nil {
return nil, err return nil, err
} }
@@ -287,18 +292,19 @@ func (s *WordService) createMemoryRecord(userID, wordID int64) error {
TotalCount: 0, TotalCount: 0,
MasteryLevel: 0, MasteryLevel: 0,
} }
return s.db.Create(&record).Error return s.memoryRepo.Create(&record)
} }
// 更新记忆记录 // 更新记忆记录
func (s *WordService) updateMemoryRecord(userID, wordID int64, correct bool) error { func (s *WordService) updateMemoryRecord(userID, wordID int64, correct bool) error {
var record model.MemoryRecord record, err := s.memoryRepo.FindByWord(userID, wordID)
if err := s.db.Where("word_id = ? AND user_id = ?", wordID, userID).First(&record).Error; err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
if createErr := s.createMemoryRecord(userID, wordID); createErr != nil { if createErr := s.createMemoryRecord(userID, wordID); createErr != nil {
return createErr 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 return err
} }
} else { } else {
@@ -323,22 +329,20 @@ func (s *WordService) updateMemoryRecord(userID, wordID int64, correct bool) err
nextReview := now.Add(reviewInterval) nextReview := now.Add(reviewInterval)
record.NextReviewAt = &nextReview 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) { func (s *WordService) GetReviewWords(userID int64, mode string, limit int) ([]model.MemoryRecord, error) {
var records []model.MemoryRecord records, err := s.memoryRepo.Due(userID, limit)
if err != nil {
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 return nil, err
} }
// 如果没有需要复习的,随机获取一些 // 如果没有需要复习的,随机获取一些
if len(records) == 0 { 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 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) { func (s *WordService) SubmitReviewAnswer(userID int64, req request.ReviewAnswerRequest) (*response.ReviewResult, error) {
var record model.MemoryRecord record, err := s.memoryRepo.FindByID(userID, req.RecordID)
if err := s.db.Preload("Word").Where("id = ? AND user_id = ?", req.RecordID, userID).First(&record).Error; err != nil { if err != nil {
return nil, err 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) { func (s *WordService) GetAllWords(limit, offset int) ([]model.Word, int64, error) {
var words []model.Word return s.wordRepo.List(limit, offset)
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
} }
// 获取记忆统计 // 获取记忆统计
func (s *WordService) GetStatistics(userID int64) (map[string]interface{}, error) { func (s *WordService) GetStatistics(userID int64) (map[string]interface{}, error) {
var totalWords int64 totalWords, masteredWords, needReview, todayReviewed := s.memoryRepo.CountOverview(userID)
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)
return map[string]interface{}{ return map[string]interface{}{
"total_words": totalWords, "total_words": totalWords,
@@ -445,8 +432,8 @@ func (s *WordService) CreateStudySession(userID int64, limit int) ([]model.Word,
if limit <= 0 || limit > 50 { if limit <= 0 || limit > 50 {
limit = 10 limit = 10
} }
var words []model.Word words, err := s.wordRepo.Latest(limit)
if err := s.db.Order("created_at DESC").Limit(limit).Find(&words).Error; err != nil { if err != nil {
return nil, err return nil, err
} }
for i := range words { for i := range words {
@@ -462,13 +449,14 @@ func (s *WordService) SubmitStudyAnswer(userID int64, req request.SubmitStudyAns
Mode: req.Mode, Mode: req.Mode,
} }
var record model.MemoryRecord record, err := s.memoryRepo.FindByWord(userID, req.WordID)
if err := s.db.Where("user_id = ? AND word_id = ?", userID, req.WordID).First(&record).Error; err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
if createErr := s.createMemoryRecord(userID, req.WordID); createErr != nil { if createErr := s.createMemoryRecord(userID, req.WordID); createErr != nil {
return nil, createErr 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 return nil, err
} }
} else { } else {

View File

@@ -2,3 +2,4 @@ export * from './types'
export * from './stats' export * from './stats'
export * from './words' export * from './words'
export * from './review' export * from './review'
export * from './study'

View File

@@ -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
}