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.gov1 分组内新增:

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 且数据库有新记录。