Files
rill/internal/api/notes.go
T
2026-09-19 17:26:07 +08:00

219 lines
6.3 KiB
Go

package api
import (
"errors"
"log/slog"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"rill/internal/model"
)
const (
defaultPageSize = 20
maxPageSize = 100
)
// NoteRequest 创建/更新便签请求。
type NoteRequest struct {
Title string `json:"title" binding:"required,max=200" example:"购物清单"`
Content string `json:"content" example:"牛奶、鸡蛋"`
}
// NoteListResponse 便签分页列表响应。
type NoteListResponse struct {
Items []model.Note `json:"items"`
Total int64 `json:"total" example:"42"`
Page int `json:"page" example:"1"`
PageSize int `json:"page_size" example:"20"`
}
// @Summary List notes
// @Description 分页查询便签列表,按 id 倒序返回。page 从 1 开始;page_size 取值 1-100,默认 20。
// @Tags notes
// @Produce json
// @Param page query int false "页码,默认 1" example(1)
// @Param page_size query int false "每页数量,默认 20,最大 100" example(20)
// @Success 200 {object} api.NoteListResponse
// @Failure 500 {object} api.ErrorResponse
// @Router /notes [get]
func listNotes(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
page, pageSize := parsePagination(c)
ctx := c.Request.Context()
var total int64
if err := db.WithContext(ctx).Model(&model.Note{}).Count(&total).Error; err != nil {
respondDBError(c, err)
return
}
var notes []model.Note
if err := db.WithContext(ctx).
Order("id DESC").
Offset((page - 1) * pageSize).
Limit(pageSize).
Find(&notes).Error; err != nil {
respondDBError(c, err)
return
}
c.JSON(http.StatusOK, NoteListResponse{
Items: notes,
Total: total,
Page: page,
PageSize: pageSize,
})
}
}
// @Summary Create a note
// @Description 创建便签。title 必填且最长 200 字符,content 可选。
// @Tags notes
// @Accept json
// @Produce json
// @Param note body api.NoteRequest true "便签内容"
// @Success 201 {object} model.Note
// @Failure 400 {object} api.ErrorResponse "参数无效"
// @Failure 500 {object} api.ErrorResponse
// @Router /notes [post]
func createNote(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
var req NoteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "参数无效: " + err.Error()})
return
}
note := model.Note{Title: req.Title, Content: req.Content}
if err := db.WithContext(c.Request.Context()).Create(&note).Error; err != nil {
respondDBError(c, err)
return
}
c.JSON(http.StatusCreated, note)
}
}
// @Summary Get a note
// @Description 按 id 查询单个便签。
// @Tags notes
// @Produce json
// @Param id path int true "便签 ID" example(1)
// @Success 200 {object} model.Note
// @Failure 400 {object} api.ErrorResponse "id 无效"
// @Failure 404 {object} api.ErrorResponse "记录不存在"
// @Failure 500 {object} api.ErrorResponse
// @Router /notes/{id} [get]
func getNote(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
var note model.Note
if err := db.WithContext(c.Request.Context()).First(&note, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, ErrorResponse{Error: "记录不存在"})
return
}
respondDBError(c, err)
return
}
c.JSON(http.StatusOK, note)
}
}
// @Summary Update a note
// @Description 全量更新便签的 title 与 content,字段校验规则同创建。
// @Tags notes
// @Accept json
// @Produce json
// @Param id path int true "便签 ID" example(1)
// @Param note body api.NoteRequest true "便签内容"
// @Success 200 {object} model.Note
// @Failure 400 {object} api.ErrorResponse "参数无效或 id 无效"
// @Failure 404 {object} api.ErrorResponse "记录不存在"
// @Failure 500 {object} api.ErrorResponse
// @Router /notes/{id} [put]
func updateNote(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
var req NoteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "参数无效: " + err.Error()})
return
}
ctx := c.Request.Context()
var note model.Note
if err := db.WithContext(ctx).First(&note, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, ErrorResponse{Error: "记录不存在"})
return
}
respondDBError(c, err)
return
}
note.Title = req.Title
note.Content = req.Content
if err := db.WithContext(ctx).Save(&note).Error; err != nil {
respondDBError(c, err)
return
}
c.JSON(http.StatusOK, note)
}
}
// @Summary Delete a note
// @Description 按 id 删除便签,成功时返回 204 且无响应体。
// @Tags notes
// @Produce json
// @Param id path int true "便签 ID" example(1)
// @Success 204 "删除成功"
// @Failure 400 {object} api.ErrorResponse "id 无效"
// @Failure 404 {object} api.ErrorResponse "记录不存在"
// @Failure 500 {object} api.ErrorResponse
// @Router /notes/{id} [delete]
func deleteNote(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
result := db.WithContext(c.Request.Context()).Delete(&model.Note{}, id)
if result.Error != nil {
respondDBError(c, result.Error)
return
}
if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, ErrorResponse{Error: "记录不存在"})
return
}
c.Status(http.StatusNoContent)
}
}
func parseID(c *gin.Context) (uint, bool) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil || id == 0 {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "id 无效"})
return 0, false
}
return uint(id), true
}
func respondDBError(c *gin.Context, err error) {
slog.ErrorContext(c.Request.Context(), "数据库操作失败", "err", err, "path", c.Request.URL.Path)
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "服务器内部错误"})
}