新增语言学习后台服务

- 分类管理:支持分类的增删改查
- 单词管理:支持单词的增删改查,包含音标、释义、例句等字段
- 更新 README 项目说明

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 22:38:09 +08:00
parent ea1b2569b5
commit 71a6bc4aa4
30 changed files with 1577 additions and 1 deletions
+155
View File
@@ -0,0 +1,155 @@
package services
import (
"context"
"math"
"github.com/fish/language/backend/internal/models"
"github.com/fish/language/backend/internal/repository/queries"
)
const defaultPageSize = 20
const maxPageSize = 100
// WordService handles business logic for words.
type WordService struct {
q *queries.Queries
}
// NewWordService creates a new WordService.
func NewWordService(q *queries.Queries) *WordService {
return &WordService{q: q}
}
// List returns a paginated list of words with optional filters.
func (s *WordService) List(ctx context.Context, req models.ListWordsRequest) (models.ListWordsResponse, error) {
page := req.Page
if page < 1 {
page = 1
}
pageSize := req.PageSize
if pageSize < 1 {
pageSize = defaultPageSize
}
if pageSize > maxPageSize {
pageSize = maxPageSize
}
offset := (page - 1) * pageSize
listParams := queries.ListWordsParams{
Limit: pageSize,
Offset: offset,
CategoryID: 0,
Keyword: "",
}
countParams := queries.CountWordsParams{
CategoryID: 0,
Keyword: "",
}
if req.CategoryID != nil && *req.CategoryID > 0 {
listParams.CategoryID = *req.CategoryID
countParams.CategoryID = *req.CategoryID
}
if req.Keyword != "" {
listParams.Keyword = req.Keyword
countParams.Keyword = req.Keyword
}
rows, err := s.q.ListWords(ctx, listParams)
if err != nil {
return models.ListWordsResponse{}, err
}
total, err := s.q.CountWords(ctx, countParams)
if err != nil {
return models.ListWordsResponse{}, err
}
words := make([]models.Word, len(rows))
for i, r := range rows {
words[i] = toWord(r)
}
totalPages := int32(math.Ceil(float64(total) / float64(pageSize)))
return models.ListWordsResponse{
Words: words,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
}, nil
}
// Get returns a single word by id.
func (s *WordService) Get(ctx context.Context, id int32) (models.Word, error) {
row, err := s.q.GetWord(ctx, id)
if err != nil {
return models.Word{}, err
}
return toWord(row), nil
}
// Create creates a new word.
func (s *WordService) Create(ctx context.Context, req models.CreateWordRequest) (models.Word, error) {
row, err := s.q.CreateWord(ctx, queries.CreateWordParams{
CategoryID: req.CategoryID,
Word: req.Word,
Translation: req.Translation,
Phonetic: req.Phonetic,
ExampleSentence: req.ExampleSentence,
AudioUrl: req.AudioURL,
ImageUrl: req.ImageURL,
SortOrder: req.SortOrder,
})
if err != nil {
return models.Word{}, err
}
return toWord(row), nil
}
// Update updates an existing word.
func (s *WordService) Update(ctx context.Context, id int32, req models.UpdateWordRequest) (models.Word, error) {
row, err := s.q.UpdateWord(ctx, queries.UpdateWordParams{
ID: id,
CategoryID: req.CategoryID,
Word: req.Word,
Translation: req.Translation,
Phonetic: req.Phonetic,
ExampleSentence: req.ExampleSentence,
AudioUrl: req.AudioURL,
ImageUrl: req.ImageURL,
SortOrder: req.SortOrder,
})
if err != nil {
return models.Word{}, err
}
return toWord(row), nil
}
// Delete removes a word by id.
func (s *WordService) Delete(ctx context.Context, id int32) error {
return s.q.DeleteWord(ctx, id)
}
func toWord(w queries.Word) models.Word {
return models.Word{
ID: w.ID,
CategoryID: w.CategoryID,
Word: w.Word,
Translation: w.Translation,
Phonetic: nullString(w.Phonetic),
ExampleSentence: nullString(w.ExampleSentence),
AudioURL: nullString(w.AudioUrl),
ImageURL: nullString(w.ImageUrl),
SortOrder: w.SortOrder,
CreatedAt: w.CreatedAt,
UpdatedAt: w.UpdatedAt,
}
}
func nullString(s *string) *string {
if s == nil {
return nil
}
v := *s
return &v
}