新增语言学习后台服务

- 分类管理:支持分类的增删改查
- 单词管理:支持单词的增删改查,包含音标、释义、例句等字段
- 更新 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
@@ -0,0 +1,126 @@
package handlers
import (
"encoding/json"
"net/http"
"strconv"
"github.com/fish/language/backend/internal/models"
"github.com/fish/language/backend/internal/services"
"github.com/go-chi/chi/v5"
)
// CategoryHandler handles HTTP requests for categories.
type CategoryHandler struct {
svc *services.CategoryService
}
// NewCategoryHandler creates a new CategoryHandler.
func NewCategoryHandler(svc *services.CategoryService) *CategoryHandler {
return &CategoryHandler{svc: svc}
}
// Register mounts category routes.
func (h *CategoryHandler) Register(r chi.Router) {
r.Get("/categories", h.List)
r.Post("/categories", h.Create)
r.Get("/categories/{id}", h.Get)
r.Put("/categories/{id}", h.Update)
r.Delete("/categories/{id}", h.Delete)
}
// List returns all categories.
func (h *CategoryHandler) List(w http.ResponseWriter, r *http.Request) {
items, err := h.svc.List(r.Context())
if err != nil {
status, code, msg := MapError(err)
RespondError(w, status, code, msg)
return
}
RespondSuccess(w, items)
}
// Get returns a category by id.
func (h *CategoryHandler) Get(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r)
if err != nil {
RespondError(w, http.StatusBadRequest, 10002, "invalid id")
return
}
item, err := h.svc.Get(r.Context(), id)
if err != nil {
status, code, msg := MapError(err)
RespondError(w, status, code, msg)
return
}
RespondSuccess(w, item)
}
// Create creates a new category.
func (h *CategoryHandler) Create(w http.ResponseWriter, r *http.Request) {
var req models.CreateCategoryRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
RespondError(w, http.StatusBadRequest, 10003, "invalid request body")
return
}
if req.Name == "" {
RespondError(w, http.StatusBadRequest, 10004, "name is required")
return
}
item, err := h.svc.Create(r.Context(), req)
if err != nil {
status, code, msg := MapError(err)
RespondError(w, status, code, msg)
return
}
RespondJSON(w, http.StatusCreated, modelsAPIResponse{Code: 0, Message: "created", Data: item})
}
// Update updates a category.
func (h *CategoryHandler) Update(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r)
if err != nil {
RespondError(w, http.StatusBadRequest, 10002, "invalid id")
return
}
var req models.UpdateCategoryRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
RespondError(w, http.StatusBadRequest, 10003, "invalid request body")
return
}
if req.Name == "" {
RespondError(w, http.StatusBadRequest, 10004, "name is required")
return
}
item, err := h.svc.Update(r.Context(), id, req)
if err != nil {
status, code, msg := MapError(err)
RespondError(w, status, code, msg)
return
}
RespondSuccess(w, item)
}
// Delete removes a category.
func (h *CategoryHandler) Delete(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r)
if err != nil {
RespondError(w, http.StatusBadRequest, 10002, "invalid id")
return
}
if err := h.svc.Delete(r.Context(), id); err != nil {
status, code, msg := MapError(err)
RespondError(w, status, code, msg)
return
}
RespondSuccess(w, nil)
}
func parseID(r *http.Request) (int32, error) {
idStr := chi.URLParam(r, "id")
id64, err := strconv.ParseInt(idStr, 10, 32)
if err != nil {
return 0, err
}
return int32(id64), nil
}
+40
View File
@@ -0,0 +1,40 @@
package handlers
import (
"encoding/json"
"errors"
"net/http"
"github.com/jackc/pgx/v5"
)
// RespondJSON writes a JSON response with the given status code.
func RespondJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(data)
}
// RespondSuccess returns the common success envelope.
func RespondSuccess(w http.ResponseWriter, data any) {
RespondJSON(w, http.StatusOK, modelsAPIResponse{Code: 0, Message: "ok", Data: data})
}
// RespondError returns the common error envelope.
func RespondError(w http.ResponseWriter, status int, code int, message string) {
RespondJSON(w, status, modelsAPIResponse{Code: code, Message: message, Data: nil})
}
// MapError maps repository/ service errors to HTTP status and code.
func MapError(err error) (status int, code int, message string) {
if errors.Is(err, pgx.ErrNoRows) {
return http.StatusNotFound, 10001, "resource not found"
}
return http.StatusInternalServerError, 10000, "internal server error"
}
type modelsAPIResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data any `json:"data"`
}
+136
View File
@@ -0,0 +1,136 @@
package handlers
import (
"encoding/json"
"net/http"
"strconv"
"github.com/fish/language/backend/internal/models"
"github.com/fish/language/backend/internal/services"
"github.com/go-chi/chi/v5"
)
// WordHandler handles HTTP requests for words.
type WordHandler struct {
svc *services.WordService
}
// NewWordHandler creates a new WordHandler.
func NewWordHandler(svc *services.WordService) *WordHandler {
return &WordHandler{svc: svc}
}
// Register mounts word routes.
func (h *WordHandler) Register(r chi.Router) {
r.Get("/words", h.List)
r.Post("/words", h.Create)
r.Get("/words/{id}", h.Get)
r.Put("/words/{id}", h.Update)
r.Delete("/words/{id}", h.Delete)
}
// List returns a paginated list of words.
func (h *WordHandler) List(w http.ResponseWriter, r *http.Request) {
req := models.ListWordsRequest{}
if v := r.URL.Query().Get("category_id"); v != "" {
if id64, err := strconv.ParseInt(v, 10, 32); err == nil {
id := int32(id64)
req.CategoryID = &id
}
}
req.Keyword = r.URL.Query().Get("keyword")
if v := r.URL.Query().Get("page"); v != "" {
if page, err := strconv.ParseInt(v, 10, 32); err == nil {
req.Page = int32(page)
}
}
if v := r.URL.Query().Get("page_size"); v != "" {
if ps, err := strconv.ParseInt(v, 10, 32); err == nil {
req.PageSize = int32(ps)
}
}
resp, err := h.svc.List(r.Context(), req)
if err != nil {
status, code, msg := MapError(err)
RespondError(w, status, code, msg)
return
}
RespondSuccess(w, resp)
}
// Get returns a word by id.
func (h *WordHandler) Get(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r)
if err != nil {
RespondError(w, http.StatusBadRequest, 10002, "invalid id")
return
}
item, err := h.svc.Get(r.Context(), id)
if err != nil {
status, code, msg := MapError(err)
RespondError(w, status, code, msg)
return
}
RespondSuccess(w, item)
}
// Create creates a new word.
func (h *WordHandler) Create(w http.ResponseWriter, r *http.Request) {
var req models.CreateWordRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
RespondError(w, http.StatusBadRequest, 10003, "invalid request body")
return
}
if req.Word == "" || req.Translation == "" || req.CategoryID == 0 {
RespondError(w, http.StatusBadRequest, 10004, "word, translation and category_id are required")
return
}
item, err := h.svc.Create(r.Context(), req)
if err != nil {
status, code, msg := MapError(err)
RespondError(w, status, code, msg)
return
}
RespondJSON(w, http.StatusCreated, modelsAPIResponse{Code: 0, Message: "created", Data: item})
}
// Update updates a word.
func (h *WordHandler) Update(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r)
if err != nil {
RespondError(w, http.StatusBadRequest, 10002, "invalid id")
return
}
var req models.UpdateWordRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
RespondError(w, http.StatusBadRequest, 10003, "invalid request body")
return
}
if req.Word == "" || req.Translation == "" || req.CategoryID == 0 {
RespondError(w, http.StatusBadRequest, 10004, "word, translation and category_id are required")
return
}
item, err := h.svc.Update(r.Context(), id, req)
if err != nil {
status, code, msg := MapError(err)
RespondError(w, status, code, msg)
return
}
RespondSuccess(w, item)
}
// Delete removes a word.
func (h *WordHandler) Delete(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r)
if err != nil {
RespondError(w, http.StatusBadRequest, 10002, "invalid id")
return
}
if err := h.svc.Delete(r.Context(), id); err != nil {
status, code, msg := MapError(err)
RespondError(w, status, code, msg)
return
}
RespondSuccess(w, nil)
}