GVM 脚手架探险记 06:跟着 GVM 学习 Gorm
配置文件
// config/gorm_mysql.go
package config
// Mysql 配置
// inline、squash tag 表示将 GeneralDB 的字段"内联"到 Mysql 结构体中
// 效果是当序列化为 YAML 时,GeneralDB 的字段会直接出现在 Mysql 的层级,而不是嵌套在 GeneralDB 字段下
type Mysql struct {
GeneralDB `yaml:",inline" mapstructure:",squash"`
}
// Dsn 获取连接数据库的 dsn 信息
func (m *Mysql) Dsn() string {
return m.Username + ":" + m.Password +
"@tcp(" + m.Path + ":" + m.Port + ")/" + m.Dbname + "?" + m.Config
}
这里采用了结构体的组合的方式,将 GeneralDB
结构体嵌入到 Mysql
结构体中。从而将一些公共的字段提取出来,避免重复编写。
另外配置这里还提供一个根据配置来获取 dsn 信息的方法。
// config/db_list.go
package config
import (
"strings"
"gorm.io/gorm/logger"
)
type DsnProvider interface {
Dsn() string
}
// GeneralDB 也被 Pgsql 和 Mysql 原样使用
type GeneralDB struct {
Prefix string `mapstructure:"prefix"` // 数据库前缀
Port string `mapstructure:"port"` // 数据库端口
Config string `mapstructure:"config"` // 高级配置
Dbname string `mapstructure:"db-name"` // 数据库名
Username string `mapstructure:"username"` // 数据库账号
Password string `mapstructure:"password"` // 数据库密码
Path string `mapstructure:"path"` // 数据库地址
Engine string `mapstructure:"engine" default:"InnoDB"` // 数据库引擎,默认InnoDB
LogMode string `mapstructure:"log-mode" default:"info"` // 是否开启Gorm全局日志
MaxIdleConns int `mapstructure:"max-idle-conns" default:"10"` // 空闲中的最大连接数
MaxOpenConns int `mapstructure:"max-open-conns" default:"100"` // 打开到数据库的最大连接数
Singular bool `mapstructure:"singular"` // 是否开启全局禁用复数,true表示开启
LogZap bool `mapstructure:"log-zap" default:"false"` // 是否通过zap写入日志文件
}
// LogLevel 将 gorm 中的日志错误级别和 zap 中的对应起来
func (c GeneralDB) LogLevel() logger.LogLevel {
switch strings.ToLower(c.LogMode) {
case "silent":
return logger.Silent
case "error":
return logger.Error
case "warn":
return logger.Warn
case "info":
return logger.Info
default:
return logger.Info
}
}
这里的 LogLevel
方法将 gorm 中的日志错误级别和 zap 中的对应起来。
GeneralDB
结构体 tag 我做了相应的删减,方便阅读。
第一次运行项目
在 GVM 的官方教程中,并不会一开始要求你配置好项目的配置文件,而是先让你运行项目。
通过阅读入口文件的配置,不难发现最后并不会运行任何关于数据库的操作。
// initialize/gorm_mysql.go
func GormMysql() *gorm.DB {
m := global.GVA_CONFIG.Mysql
return initMysqlDatabase(m)
}
func initMysqlDatabase(m config.Mysql) *gorm.DB {
if m.Dbname == "" {
return nil
}
}
在这里因为不会配置数据库,所以 initMysqlDatabase
方法会直接返回 nil
。
需要注意的是:
在 mian.go 中导入 initialize 初始化的包。就会自动导入也位于该包下的 register_init.go 文件,而 register_init.go 在其内部在对不同的模块引入导包为后面通过接口初始化数据库做准备工作。
// server/main.go
"github.com/flipped-aurora/gin-vue-admin/server/initialize"
// initialize/register_init.go
package initialize
import (
_ "github.com/flipped-aurora/gin-vue-admin/server/source/example"
_ "github.com/flipped-aurora/gin-vue-admin/server/source/system"
)
func init() {
// do nothing,only import source package so that inits can be registered
}
通过接口来初始化数据库
当你前后端项目都运行起来后,在前端的登录页面,会有一个前往初始化的相关配置。
填写完毕数据库基本信息,点击初始化按钮,会发送一个 POST 请求到 /initdb
接口。
控制器层
// api/v1/system/sys_initdb.go
type DBApi struct{}
func (i *DBApi) InitDB(c *gin.Context) {
if global.GVA_DB != nil {
global.GVA_LOG.Error("已存在数据库配置!")
response.FailWithMessage("已存在数据库配置", c)
return
}
var dbInfo request.InitDB
if err := c.ShouldBindJSON(&dbInfo); err != nil {
global.GVA_LOG.Error("参数校验不通过!", zap.Error(err))
response.FailWithMessage("参数校验不通过", c)
return
}
if err := initDBService.InitDB(dbInfo); err != nil {
global.GVA_LOG.Error("自动创建数据库失败!", zap.Error(err))
response.FailWithMessage("自动创建数据库失败,请查看后台日志,检查后在进行初始化", c)
return
}
response.OkWithMessage("自动创建数据库成功", c)
}
入口代码很容易理解
- 先判断是否已经存在数据库连接
- 再校验参数
- 调用服务层的方法来初始化数据库
- 最后返回结果
这里需要注意点的是定义的 request.InitDB
入参结构体。
// model/system/request/sys_init.go
type InitDB struct {
AdminPassword string `json:"adminPassword" binding:"required"`
DBType string `json:"dbType"` // 数据库类型
Host string `json:"host"` // 服务器地址
Port string `json:"port"` // 数据库连接端口
UserName string `json:"userName"` // 数据库用户名
Password string `json:"password"` // 数据库密码
DBName string `json:"dbName" binding:"required"` // 数据库名
DBPath string `json:"dbPath"` // sqlite数据库文件路径
Template string `json:"template"` // postgresql指定template
}
// MysqlEmptyDsn mysql 空数据库 建库链接
func (i *InitDB) MysqlEmptyDsn() string {
if i.Host == "" {
i.Host = "127.0.0.1"
}
if i.Port == "" {
i.Port = "3306"
}
return fmt.Sprintf("%s:%s@tcp(%s:%s)/", i.UserName, i.Password, i.Host, i.Port)
}
// ToMysqlConfig 将入参数据转换 Mysql 配置文件的结构体
func (i *InitDB) ToMysqlConfig() config.Mysql {
return config.Mysql{
GeneralDB: config.GeneralDB{
Path: i.Host,
Port: i.Port,
Dbname: i.DBName,
Username: i.UserName,
Password: i.Password,
MaxIdleConns: 10,
MaxOpenConns: 100,
LogMode: "error",
Config: "charset=utf8mb4&parseTime=True&loc=Local",
},
}
}
服务层
服务层入口代码预览
// service/system/sys_initdb.go
type InitDBService struct{}
// InitDB 创建数据库并初始化 总入口
func (initDBService *InitDBService) InitDB(conf request.InitDB) (err error) {
// 这里使用 todo 建立了一个临时的空白上下文
ctx := context.TODO()
// 在上下文中添加了管理员密码
ctx = context.WithValue(ctx, "adminPassword", conf.AdminPassword)
if len(initializers) == 0 {
return errors.New("无可用初始化过程,请检查初始化是否已执行完成")
}
// 对切片进行排序 order 小的在前面
// 保证有依赖的 initializer 排在后面执行
sort.Sort(&initializers)
var initHandler TypedDBInitHandler
initHandler = NewMysqlInitHandler()
ctx = context.WithValue(ctx, "dbtype", "mysql")
// 1. 在上下文中传入了数据库配置 config
// 2. 在上下文中传入了已连接的 Gorm db 对象
ctx, err = initHandler.EnsureDB(ctx, &conf)
if err != nil {
return err
}
db := ctx.Value("db").(*gorm.DB)
// 和 main 方法中的一样 将得到的 db 放入全局变量
global.GVA_DB = db
// 创建数据表
if err = initHandler.InitTables(ctx, initializers); err != nil {
return err
}
// 表中插入初始数据
if err = initHandler.InitData(ctx, initializers); err != nil {
return err
}
// 重写项目 config
if err = initHandler.WriteConfig(ctx); err != nil {
return err
}
// 清理资源
initializers = initSlice{}
cache = map[string]*orderedInitializer{}
return nil
}
0. 表初始化钩子
在这里 GVM 利用了包的 init
函数来初始化数据库。
// service/system/sys_initdb.go
func (initDBService *InitDBService) InitDB(conf request.InitDB) (err error) {
// ... 其他初始化操作
if len(initializers) == 0 {
return errors.New("无可用初始化过程,请检查初始化是否已执行完成")
}
// ... 其他初始化操作
}
GVM 定义了一套初始化数据库接口规范。
每个初始化包中都需要实现 SubInitializer
接口。
// service/system/sys_initdb.go
type SubInitializer interface {
// InitializerName 获取数据库的表名
InitializerName() string // 不一定代表单独一个表,所以改成了更宽泛的语义
// MigrateTable 自动迁移 建表
MigrateTable(ctx context.Context) (next context.Context, err error)
// InitializeData 插入默认数据
InitializeData(ctx context.Context) (next context.Context, err error)
// TableCreated 检查对应的表是否存在
TableCreated(ctx context.Context) bool
// DataInserted 是否存在数据判断
DataInserted(ctx context.Context) bool
}
定义 orderedInitializer
结构体,用于排序。
// service/system/sys_initdb.go
// orderedInitializer 组合一个顺序字段,以供排序
type orderedInitializer struct {
order int
SubInitializer
}
// initSlice 供 initializer 排序依赖时使用
type initSlice []*orderedInitializer
具体注册逻辑
// service/system/sys_initdb.go
var (
initializers initSlice
cache map[string]*orderedInitializer
)
// RegisterInit 注册要执行的初始化过程,会在 InitDB() 时调用
func RegisterInit(order int, i SubInitializer) {
if initializers == nil {
initializers = initSlice{}
}
if cache == nil {
cache = map[string]*orderedInitializer{}
}
name := i.InitializerName()
if _, existed := cache[name]; existed {
panic(fmt.Sprintf("Name conflict on %s", name))
}
ni := orderedInitializer{order, i}
initializers = append(initializers, &ni)
cache[name] = &ni
}
这里粘贴出来 数据表-api 注册 代码作为示例。
// source/system/api.go
package system
import (
"context"
sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/flipped-aurora/gin-vue-admin/server/service/system"
"github.com/pkg/errors"
"gorm.io/gorm"
)
type initApi struct{}
const initOrderApi = system.InitOrderSystem + 1
// auto run
func init() {
system.RegisterInit(initOrderApi, &initApi{})
}
func (i *initApi) InitializerName() string {
return sysModel.SysApi{}.TableName()
}
// MigrateTable 自动迁移
func (i *initApi) MigrateTable(ctx context.Context) (context.Context, error) {
db, ok := ctx.Value("db").(*gorm.DB)
if !ok {
return ctx, system.ErrMissingDBContext
}
// 自动迁移
return ctx, db.AutoMigrate(&sysModel.SysApi{})
}
// TableCreated 检查对应的表是否存在
func (i *initApi) TableCreated(ctx context.Context) bool {
db, ok := ctx.Value("db").(*gorm.DB)
if !ok {
return false
}
// 检查对应的表是否存在
return db.Migrator().HasTable(&sysModel.SysApi{})
}
// InitializeData 插数据
func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) {
db, ok := ctx.Value("db").(*gorm.DB)
if !ok {
return ctx, system.ErrMissingDBContext
}
entities := []sysModel.SysApi{
{ApiGroup: "jwt", Method: "POST", Path: "/jwt/jsonInBlacklist", Description: "jwt加入黑名单"},
}
if err := db.Create(&entities).Error; err != nil {
return ctx, errors.Wrap(err, sysModel.SysApi{}.TableName()+"表数据初始化失败!")
}
next := context.WithValue(ctx, i.InitializerName(), entities)
return next, nil
}
// DataInserted 是否存在数据判断
func (i *initApi) DataInserted(ctx context.Context) bool {
db, ok := ctx.Value("db").(*gorm.DB)
if !ok {
return false
}
if errors.Is(db.Where("path = ? AND method = ?", "/authorityBtn/canRemoveAuthorityBtn", "POST").
First(&sysModel.SysApi{}).Error, gorm.ErrRecordNotFound) {
return false
}
return true
}
1. 初始化顺序
通过自定义的排序规则,针对表的初始化顺序进行排序。order 小的在前面。
// service/system/sys_initdb.go
func (initDBService *InitDBService) InitDB(conf request.InitDB) (err error) {
// ... 其他初始化操作
sort.Sort(&initializers)
func (a initSlice) Len() int {
return len(a)
}
func (a initSlice) Less(i, j int) bool {
return a[i].order < a[j].order
}
func (a initSlice) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
// ... 其他初始化操作
}
2. 执行初始化
// service/system/sys_initdb.go
// TypedDBInitHandler 执行传入的 initializer
type TypedDBInitHandler interface {
// 建库,失败属于 fatal error,因此让它 panic
EnsureDB(ctx context.Context, conf *request.InitDB) (context.Context, error)
// 回写配置
WriteConfig(ctx context.Context) error
// 建表 handler
InitTables(ctx context.Context, inits initSlice) error
// 建数据 handler
InitData(ctx context.Context, inits initSlice) error
}
// service/system/sys_initdb.go
func (initDBService *InitDBService) InitDB(conf request.InitDB) (err error) {
// ... 其他初始化操作
var initHandler TypedDBInitHandler
initHandler = NewMysqlInitHandler()
ctx = context.WithValue(ctx, "dbtype", "mysql")
// 1. 在上下文中传入了数据库配置 config
// 2. 在上下文中传入了已连接的 Gorm db 对象
ctx, err = initHandler.EnsureDB(ctx, &conf)
if err != nil {
return err
}
db := ctx.Value("db").(*gorm.DB)
// 和 main 方法中的一样 将得到的 db 放入全局变量
global.GVA_DB = db
// 创建数据表
if err = initHandler.InitTables(ctx, initializers); err != nil {
return err
}
// 表中插入初始数据
if err = initHandler.InitData(ctx, initializers); err != nil {
return err
}
// 重写项目 config
if err = initHandler.WriteConfig(ctx); err != nil {
return err
}
// ... 其他初始化操作
}
Mysql TypedDBInitHandler 方法:
不算太复杂,就是根据配置创建数据库,创建表,插入数据,回写配置。
// service/system/sys_initdb_mysql.go
package system
import (
"context"
"errors"
"fmt"
"path/filepath"
"github.com/flipped-aurora/gin-vue-admin/server/config"
"github.com/gookit/color"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
"github.com/google/uuid"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type MysqlInitHandler struct{}
func NewMysqlInitHandler() *MysqlInitHandler {
return &MysqlInitHandler{}
}
// WriteConfig mysql回写配置
func (h MysqlInitHandler) WriteConfig(ctx context.Context) error {
c, ok := ctx.Value("config").(config.Mysql)
if !ok {
return errors.New("mysql config invalid")
}
global.GVA_CONFIG.System.DbType = "mysql"
global.GVA_CONFIG.Mysql = c
global.GVA_CONFIG.JWT.SigningKey = uuid.New().String()
cs := utils.StructToMap(global.GVA_CONFIG)
for k, v := range cs {
global.GVA_VP.Set(k, v)
}
global.GVA_ACTIVE_DBNAME = &c.Dbname
return global.GVA_VP.WriteConfig()
}
// EnsureDB 创建数据库并初始化 mysql
func (h MysqlInitHandler) EnsureDB(
ctx context.Context,
conf *request.InitDB,
) (next context.Context, err error) {
if s, ok := ctx.Value("dbtype").(string); !ok || s != "mysql" {
return ctx, ErrDBTypeMismatch
}
c := conf.ToMysqlConfig()
next = context.WithValue(ctx, "config", c)
if c.Dbname == "" {
return ctx, nil
} // 如果没有数据库名, 则跳出初始化数据
// 1. 先利用 go 官方自带的 sql 创建数据库
dsn := conf.MysqlEmptyDsn()
createSql := fmt.Sprintf("CREATE DATABASE IF NOT EXISTS "+
"`%s` DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;", c.Dbname)
if err = createDatabase(dsn, "mysql", createSql); err != nil {
return nil, err
} // 创建数据库
// 2. 创建 Gorm 数据库连接(需要数据库)
var db *gorm.DB
if db, err = gorm.Open(mysql.New(mysql.Config{
DSN: c.Dsn(), // DSN data source name
DefaultStringSize: 191, // string 类型字段的默认长度
SkipInitializeWithVersion: true, // 根据版本自动配置
}), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true}); err != nil {
return ctx, err
}
global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..")
next = context.WithValue(next, "db", db)
return next, err
}
// InitTables 创建数据表
func (h MysqlInitHandler) InitTables(ctx context.Context, inits initSlice) error {
return createTables(ctx, inits)
}
// InitData 填充数据
func (h MysqlInitHandler) InitData(ctx context.Context, inits initSlice) error {
next, cancel := context.WithCancel(ctx)
defer func(c func()) { c() }(cancel)
for _, init := range inits {
// 是否存在数据判断
if init.DataInserted(next) {
color.Info.Printf(InitDataExist, Mysql, init.InitializerName())
continue
}
// 插数据
if n, err := init.InitializeData(next); err != nil {
color.Info.Printf(InitDataFailed, Mysql, init.InitializerName(), err)
return err
} else {
next = n
color.Info.Printf(InitDataSuccess, Mysql, init.InitializerName())
}
}
color.Info.Printf(InitSuccess, Mysql)
return nil
}
// service/system/sys_initdb.go
// createDatabase 创建数据库( EnsureDB() 中调用 )
// 这个函数中的 SQL 执行使用的是 Go 标准库中的 database/sql 包
func createDatabase(dsn string, driver string, createSql string) error {
// 1. 建立数据库连接
db, err := sql.Open(driver, dsn) // 使用传入的驱动和DSN
if err != nil {
return err
}
// 2. 确保连接关闭(defer语句)
defer func(db *sql.DB) {
err = db.Close()
if err != nil {
fmt.Println(err)
}
}(db)
// 3. 测试连接是否可用
if err = db.Ping(); err != nil {
return err
}
// 4. 执行创建数据库的SQL
_, err = db.Exec(createSql)
return err
}
// createTables 创建表(默认 dbInitHandler.initTables 行为)
func createTables(ctx context.Context, inits initSlice) error {
// 创建新的可取消的上下文
next, cancel := context.WithCancel(ctx)
// 确保关闭上下文
defer func(c func()) {
c()
}(cancel)
for _, init := range inits {
// 检查对应的表是否存在
if init.TableCreated(next) {
continue
}
// 自动迁移
if n, err := init.MigrateTable(next); err != nil {
return err
} else {
next = n
}
}
return nil
}
3. 清理资源
// service/system/sys_initdb.go
func (initDBService *InitDBService) InitDB(conf request.InitDB) (err error) {
// ... 其他初始化操作
initializers = initSlice{}
cache = map[string]*orderedInitializer{}
return nil
}
到这里通过接口初始化项目就完成了。
当再次重新启动后端项目
1. 创建数据库连接
通过 main.go 文件初始化数据库部分,一路追踪代码。最终会到这里:
获取 Mysql 的配置,在通过配置去创建数据库连接。
// initialize/gorm_mysql.go
func GormMysql() *gorm.DB {
m := global.GVA_CONFIG.Mysql
return initMysqlDatabase(m)
}
核心函数,创建数据库连接
// initialize/gorm_mysql.go
// initMysqlDatabase 初始化Mysql数据库的辅助函数
func initMysqlDatabase(m config.Mysql) *gorm.DB {
if m.Dbname == "" {
return nil
}
mysqlConfig := mysql.Config{
DSN: m.Dsn(), // DSN data source name
DefaultStringSize: 191, // string 类型字段的默认长度
SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置
}
// db 和 err 是 局部变量 ,作用域为整个 if-else 结构。
// 不仅仅是所认知的作用域 在 if 中。
if db, err := gorm.Open(
mysql.New(mysqlConfig),
internal.Gorm.Config(m.Prefix, m.Singular),
); err != nil {
panic(err)
} else {
db.InstanceSet("gorm:table_options", "ENGINE="+m.Engine)
// 连接池比喻:游泳池
// 空闲泳道(MaxIdleConns)专门划出的"随时可用的泳道"数量(比如10条),没人游时也保持开放
// 泳池总容量(MaxOpenConns)整个泳池最多能容纳多少人(比如100人)
// 开始设置数据库连接池
sqlDB, _ := db.DB()
// 设置空闲连接池中的最大连接数
sqlDB.SetMaxIdleConns(m.MaxIdleConns)
// 设置数据库的最大打开连接数(包括正在使用的和空闲的)
sqlDB.SetMaxOpenConns(m.MaxOpenConns)
return db
}
}
其中, GVM 针对 Gorm 的连接做了一些定制。
参考: Gorm 配置官方文档
// initialize/internal/gorm.go
package internal
import (
"github.com/flipped-aurora/gin-vue-admin/server/config"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"time"
)
var Gorm = new(_gorm)
type _gorm struct{}
// Config gorm 自定义配置
// Author [SliverHorn](https://github.com/SliverHorn)
func (g *_gorm) Config(prefix string, singular bool) *gorm.Config {
var general config.GeneralDB
general = global.GVA_CONFIG.Mysql.GeneralDB
return &gorm.Config{
Logger: logger.New(
NewWriter(general), // log writer
logger.Config{
SlowThreshold: 200 * time.Millisecond, // 慢 SQL 阈值
LogLevel: general.LogLevel(), // 日志等级
Colorful: true, // 禁止颜色
}),
NamingStrategy: schema.NamingStrategy{
TablePrefix: prefix, // 表名前缀,“User”的表将是“t_users”`
SingularTable: singular, // 单数表名,`User`的表将是`user`,而不是`users`
},
// 禁用在 AutoMigrate 或 CreateTable 时,GORM 自动创建外键约束
DisableForeignKeyConstraintWhenMigrating: true,
}
}
2. 配置多台数据库
通过配置文件配置多台数据库配置信息,最终取到相应配置,通过方法 GormMysqlByConfig
完成初始化。
// initialize/db_list.go
package initialize
import (
"github.com/flipped-aurora/gin-vue-admin/server/config"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"gorm.io/gorm"
)
const sys = "system"
func DBList() {
dbMap := make(map[string]*gorm.DB)
for _, info := range global.GVA_CONFIG.DBList {
if info.Disable {
continue
}
switch info.Type {
case "mysql":
dbMap[info.AliasName] = GormMysqlByConfig(config.Mysql{GeneralDB: info.GeneralDB})
case "mssql":
dbMap[info.AliasName] = GormMssqlByConfig(config.Mssql{GeneralDB: info.GeneralDB})
case "pgsql":
dbMap[info.AliasName] = GormPgSqlByConfig(config.Pgsql{GeneralDB: info.GeneralDB})
case "oracle":
dbMap[info.AliasName] = GormOracleByConfig(config.Oracle{GeneralDB: info.GeneralDB})
default:
continue
}
}
// 做特殊判断,是否有迁移
// 适配低版本迁移多数据库版本
if sysDB, ok := dbMap[sys]; ok {
global.GVA_DB = sysDB
}
global.GVA_DBList = dbMap
}
3. 执行数据库迁移
// initialize/gorm.go
func RegisterTables() {
db := global.GVA_DB
// 会将指定的结构体注册到 GORM 的内部模型列表并立即执行该模型的迁移操作
err := db.AutoMigrate(
system.SysApi{},
system.SysIgnoreApi{},
system.SysUser{},
system.SysBaseMenu{},
system.JwtBlacklist{},
system.SysAuthority{},
system.SysDictionary{},
// ...
example.ExaAttachmentCategory{},
)
if err != nil {
global.GVA_LOG.Error("register table failed", zap.Error(err))
os.Exit(0)
}
err = bizModel()
if err != nil {
global.GVA_LOG.Error("register biz_table failed", zap.Error(err))
os.Exit(0)
}
global.GVA_LOG.Info("register table success")
}
// initialize/gorm_biz.go
func bizModel() error {
db := global.GVA_DB
// 确保所有已注册模型(包括插件模型)都完成迁移
err := db.AutoMigrate()
if err != nil {
return err
}
return nil
}