6.5 添加新功能与数据库迁移
添加新功能的完整流程
以"新增**标签(Tag)**功能,允许为短链接打多个标签"为例,演示从零到上线的完整步骤。
步骤 1:设计 Model
在 app/model/tag.go 定义 GORM 实体:
package model
import "time"
type Tag struct {
ID uint64 `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:64;uniqueIndex" json:"name"`
Color string `gorm:"size:16" json:"color"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (Tag) TableName() string { return "tags" }若涉及多对多关系(短链接 ↔ 标签),增加关联表模型:
type ShortLinkTag struct {
ShortLinkID uint64 `gorm:"primaryKey"`
TagID uint64 `gorm:"primaryKey"`
}步骤 2:注册迁移
在数据库迁移服务中追加新表,通常在 internal/pkg/database/ 下的迁移注册处:
db.AutoMigrate(&model.Tag{}, &model.ShortLinkTag{})服务重启后自动执行 AutoMigrate 创建表。
步骤 3:编写 DAO
app/dao/tag_dao.go:
package dao
import (
"cnb.cool/mliev/dwz/dwz-server/app/model"
"cnb.cool/mliev/dwz/dwz-server/internal/helper"
)
type TagDAO struct{}
func (TagDAO) Create(tag *model.Tag) error {
return helper.GetDatabase().Create(tag).Error
}
func (TagDAO) List(offset, limit int) ([]model.Tag, int64, error) {
db := helper.GetDatabase().Model(&model.Tag{})
var total int64
db.Count(&total)
var items []model.Tag
err := db.Offset(offset).Limit(limit).Order("id DESC").Find(&items).Error
return items, total, err
}
func (TagDAO) Delete(id uint64) error {
return helper.GetDatabase().Delete(&model.Tag{}, id).Error
}步骤 4:编写 Service
app/service/tag_service.go:
package service
import (
"errors"
"cnb.cool/mliev/dwz/dwz-server/app/dao"
"cnb.cool/mliev/dwz/dwz-server/app/model"
)
type TagService struct {
dao dao.TagDAO
}
func (s TagService) Create(name, color string) (*model.Tag, error) {
if name == "" {
return nil, errors.New("标签名不能为空")
}
tag := &model.Tag{Name: name, Color: color}
if err := s.dao.Create(tag); err != nil {
return nil, err
}
return tag, nil
}步骤 5:定义 DTO
app/dto/tag_dto.go:
package dto
type CreateTagRequest struct {
Name string `json:"name" binding:"required,max=64"`
Color string `json:"color" binding:"omitempty,max=16"`
}
type TagResponse struct {
ID uint64 `json:"id"`
Name string `json:"name"`
Color string `json:"color"`
}步骤 6:编写 Controller
app/controller/tag_controller.go:
package controller
import (
"github.com/gin-gonic/gin"
"cnb.cool/mliev/dwz/dwz-server/app/dto"
"cnb.cool/mliev/dwz/dwz-server/app/service"
"cnb.cool/mliev/dwz/dwz-server/pkg/service/http_server/impl"
)
type TagController struct{}
func (TagController) Create(c *gin.Context, deps *impl.HttpDeps) {
var req dto.CreateTagRequest
if err := c.ShouldBindJSON(&req); err != nil {
deps.FailWithMessage(c, 40001, err.Error()); return
}
svc := service.TagService{}
tag, err := svc.Create(req.Name, req.Color)
if err != nil {
deps.FailWithMessage(c, 50001, err.Error()); return
}
deps.OkWithData(c, dto.TagResponse{ID: tag.ID, Name: tag.Name, Color: tag.Color})
}步骤 7:注册路由
在 config/autoload/router.go 的 v1 分组内新增:
tags := v1.Group("/tags")
{
tags.POST("", deps.WrapHandler(controller.TagController{}.Create))
tags.GET("", deps.WrapHandler(controller.TagController{}.List))
tags.DELETE("/:id", deps.WrapHandler(controller.TagController{}.Delete))
}步骤 8:补测试与文档
- 在
app/service/或pkg/helper/下加单元测试 - 更新本文档(API 参考新章节、FAQ 等)
步骤 9:本地验证
go run main.go
curl -X POST http://127.0.0.1:8080/api/v1/tags \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"marketing","color":"#ff0000"}'确认返回 code: 0 且数据库有新记录。