refactor: introduce repositories and expose study api client
This commit is contained in:
65
memora-api/internal/repository/memory_repository.go
Normal file
65
memora-api/internal/repository/memory_repository.go
Normal 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
|
||||
}
|
||||
45
memora-api/internal/repository/word_repository.go
Normal file
45
memora-api/internal/repository/word_repository.go
Normal 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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -24,10 +25,16 @@ import (
|
||||
|
||||
type WordService struct {
|
||||
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 {
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from './types'
|
||||
export * from './stats'
|
||||
export * from './words'
|
||||
export * from './review'
|
||||
export * from './study'
|
||||
|
||||
16
memora-web/src/services/api/study.ts
Normal file
16
memora-web/src/services/api/study.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user