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/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"
|
||||||
|
|
||||||
@@ -23,11 +24,17 @@ 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 {
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
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