GoLand 项目从 0 到 1:第二天 —— 数据库自动化

发布于:2025-07-25 ⋅ 阅读:(11) ⋅ 点赞:(0)

第二天核心任务:自动化与多数据库支持

第二天的开发聚焦于数据库自动化流程构建MongoDB 业务链路扩展,通过工具化手段解决数据库操作的重复性问题,同时完善多数据库支持能力。经过一天的开发,项目已实现数据库初始化、迁移、种子数据填充的全自动化,并完成 MongoDB 接口与模型的完整适配,基础工具链也同步升级以支撑新架构。
注:项目会用到多个数据库:mysql、mangodb、Neo4j,减少建表和初始化表的工作量

一、核心模块:数据库自动化流程

关键实现与代码解析

1. /main.go

新增数据库集中式初始化(只列举了mysql)

// 新版本 - 统一初始化入口
if err := database.InitMySQL(); err != nil {
    logger.Fatal("Failed to initialize databases:", err)
}
defer func() {
    err := database.CloseDB()
    if err != nil {
        logger.Fatal("initialize databases is exception:", err)
    }
}()
func InitMySQL() error {
    // 使用新的数据库初始化器
    initializer := NewDatabaseInitializer()
    return initializer.InitAllDatabases()
}
func (di *DatabaseInitializer) InitAllDatabases() error {
    logger.Info("Starting database initialization...")

    // 1. 初始化MySQL
    if err := di.initMySQL(); err != nil {
        return fmt.Errorf("failed to initialize MySQL: %w", err)
    }

    // 初始化MongoDB迁移和种子数据
    // ...省略...

    logger.Info("All databases initialized successfully")
    return nil
}
func (di *DatabaseInitializer) initMySQL() error {
	logger.Info("Initializing MySQL database...")

	// 1. 首先连接到MySQL服务器(不指定数据库)
	if err := di.connectToMySQLServer(); err != nil {
		return fmt.Errorf("failed to connect to MySQL server: %w", err)
	}

	// 2. 创建数据库(如果不存在 CREATE DATABASE)
	if err := di.createDatabase(); err != nil {
		return fmt.Errorf("failed to create database: %w", err)
	}

	// 3. 连接到指定数据库
	if err := di.connectToDatabase(); err != nil {
		return fmt.Errorf("failed to connect to database: %w", err)
	}

	// 4. 自动迁移表结构
	if err := di.autoMigrateTables(); err != nil {
		return fmt.Errorf("failed to migrate tables: %w", err)
	}

	// 5. 运行数据库迁移(如果启用)
	if di.config.Database.AutoMigrate {
		if err := di.runMigrations(); err != nil {
			return fmt.Errorf("failed to run migrations: %w", err)
		}
	}

	// 6. 运行种子数据(INSERT INTO)
	if di.config.Database.AutoSeed {
		if err := di.runSeeders(); err != nil {
			return fmt.Errorf("failed to run seeders: %w", err)
		}
	}

	logger.Info("MySQL database initialized successfully")
	return nil
}
// connectToMySQLServer 连接到MySQL服务器
func (di *DatabaseInitializer) connectToMySQLServer() error {
	cfg := di.config.Database

	// 连接到MySQL服务器(不指定数据库)
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/?charset=utf8mb4&parseTime=True&loc=Local",
		cfg.Username,
		cfg.Password,
		cfg.Host,
		cfg.Port,
	)

	var err error
	DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: gormlogger.Default.LogMode(gormlogger.Info),
	})
	if err != nil {
		return fmt.Errorf("failed to connect to MySQL server: %w", err)
	}

	// 配置连接池
	sqlDB, err := DB.DB()
	if err != nil {
		return fmt.Errorf("failed to get sql.DB: %w", err)
	}

	sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
	sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
	sqlDB.SetConnMaxLifetime(time.Duration(cfg.MaxLifetime) * time.Second)

	return nil
}
func (di *DatabaseInitializer) createDatabase() error {
	dbName := di.config.Database.Database

	// 检查数据库是否存在
	var count int64
	DB.Raw("SELECT COUNT(*) FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?", dbName).Scan(&count)

	if count == 0 {
		logger.Info("Creating database:", dbName)

		// 创建数据库
		createSQL := fmt.Sprintf("CREATE DATABASE `%s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci", dbName)
		if err := DB.Exec(createSQL).Error; err != nil {
			return fmt.Errorf("failed to create database %s: %w", dbName, err)
		}

		logger.Info("Database created successfully:", dbName)
	} else {
		logger.Info("Database already exists:", dbName)
	}

	return nil
}
// autoMigrateTables 自动迁移表结构
func (di *DatabaseInitializer) autoMigrateTables() error {
	logger.Info("Starting table migration...")

	// 使用新的MySQL迁移管理器
	migrationManager := NewMySQLMigrationManager()
	if err := migrationManager.RunMigrations(); err != nil {
		return fmt.Errorf("failed to run migrations: %w", err)
	}

	logger.Info("Table migration completed successfully")
	return nil
}

// RunMigrations 运行所有迁移
func (mm *MySQLMigrationManager) RunMigrations() error {
	logger.Info("Starting MySQL migrations...")

	// 定义所有需要迁移的模型
	models := []interface{}{
		&mysql.User{},
		&mysql.Demo{},
		// 在这里添加更多模型
	}

	// 执行迁移
	for _, model := range models {
		if err := mm.migrateModel(model); err != nil {
			return fmt.Errorf("failed to migrate model %T: %w", model, err)
		}
	}

	logger.Info("MySQL migrations completed successfully")
	return nil
}
// runSeeders 运行种子数据
func (di *DatabaseInitializer) runSeeders() error {
	logger.Info("Starting database seeding...")

	seeder := NewSeeder()
	if err := seeder.RunSeeders(); err != nil {
		return fmt.Errorf("failed to run seeders: %w", err)
	}

	logger.Info("Database seeding completed successfully")
	return nil
}
// RunSeeders 运行所有种子数据
func (s *Seeder) RunSeeders() error {
	logger.Info("Starting database seeding...")

	// 运行各种种子数据
	seeders := []func() error{
		s.seedUsers,
		s.seedDemos,
		// 在这里添加更多种子数据
	}

	for _, seeder := range seeders {
		if err := seeder(); err != nil {
			return fmt.Errorf("failed to run seeder: %w", err)
		}
	}

	logger.Info("Database seeding completed successfully")
	return nil
}
// seedUsers 种子用户数据
func (s *Seeder) seedUsers() error {
	// 检查是否已有用户数据
	var count int64
	if err := s.db.Model(&mysql.User{}).Count(&count).Error; err != nil {
		return fmt.Errorf("failed to count users: %w", err)
	}

	if count > 0 {
		logger.Info("Users already seeded, skipping...")
		return nil
	}

	// 创建默认管理员用户
	adminPassword, err := utils.HashPassword("admin123")
	if err != nil {
		return fmt.Errorf("failed to hash admin password: %w", err)
	}

	adminUser := mysql.User{
		Username: "admin",
		Password: adminPassword,
		Email:    "admin@example.com",
		Role:     "admin",
		Status:   1,
	}

	if err := s.db.Create(&adminUser).Error; err != nil {
		return fmt.Errorf("failed to create admin user: %w", err)
	}

	logger.Info("Users seeded successfully")
	return nil
}

二、基础能力升级:工具链适配新架构

  1. 雪花 ID 生成器(pkg/utils/snowflake.go)

    • 支持配置workerID,解决分布式部署时 ID 冲突问题
    • 处理时间回拨异常:系统时间回退时暂停生成,确保 ID 单调递增
package utils

import (
	"fmt"
	"sync"
	"time"
)

const (
	// 时间戳位数
	timestampBits = 41
	// 机器ID位数
	machineIDBits = 10
	// 序列号位数
	sequenceBits = 12

	// 最大值
	maxMachineID = (1 << machineIDBits) - 1
	maxSequence  = (1 << sequenceBits) - 1

	// 偏移量
	machineIDShift = sequenceBits
	timestampShift = sequenceBits + machineIDBits

	// 起始时间戳 (2023-01-01 00:00:00 UTC)
	epoch = 1672531200000
)

// Snowflake 雪花ID生成器
type Snowflake struct {
	mutex     sync.Mutex
	machineID int64
	sequence  int64
	lastTime  int64
}

var (
	defaultSnowflake *Snowflake
	once             sync.Once
)

// NewSnowflake 创建雪花ID生成器
func NewSnowflake(machineID int64) (*Snowflake, error) {
	if machineID < 0 || machineID > maxMachineID {
		return nil, fmt.Errorf("machine ID must be between 0 and %d", maxMachineID)
	}

	return &Snowflake{
		machineID: machineID,
		sequence:  0,
		lastTime:  0,
	}, nil
}

// GetDefaultSnowflake 获取默认雪花ID生成器
func GetDefaultSnowflake() *Snowflake {
	once.Do(func() {
		var err error
		defaultSnowflake, err = NewSnowflake(1) // 默认机器ID为1
		if err != nil {
			panic(fmt.Sprintf("failed to create default snowflake: %v", err))
		}
	})
	return defaultSnowflake
}

// NextID 生成下一个ID
func (s *Snowflake) NextID() int64 {
	// 加锁保证线程安全,防止并发时序列号冲突
    s.mutex.Lock()
    defer s.mutex.Unlock() // 确保函数退出时自动解锁

    // 获取当前时间戳(毫秒级)
    now := time.Now().UnixMilli()

    // 时钟回拨检查:如果当前时间小于上次生成ID的时间
    // 说明系统时钟被回拨,返回0表示错误
    if now < s.lastTime {
        return 0
    }

    // 同一毫秒内的处理逻辑
    if now == s.lastTime {
        // 序列号递增,使用位与运算确保不超过最大值(4095)
        s.sequence = (s.sequence + 1) & maxSequence
        
        // 序列号溢出检查(当sequence从最大值加1后归零)
        if s.sequence == 0 {
            // 等待直到下一毫秒
            now = s.waitNextMillis(s.lastTime)
        }
    } else {
        // 新的时间窗口(毫秒),重置序列号为0
        s.sequence = 0
    }

    // 更新最后生成ID的时间戳
    s.lastTime = now

	// 生成ID
	id := ((now - epoch) << timestampShift) |
		(s.machineID << machineIDShift) |
		s.sequence

	return id
}

// waitNextMillis 等待下一毫秒
func (s *Snowflake) waitNextMillis(lastTimestamp int64) int64 {
	timestamp := time.Now().UnixMilli()
	for timestamp <= lastTimestamp {
		timestamp = time.Now().UnixMilli()
	}
	return timestamp
}

// GenerateID 生成雪花ID(使用默认生成器)
func GenerateID() int64 {
	return GetDefaultSnowflake().NextID()
}

// ParseID 解析雪花ID
func ParseID(id int64) map[string]int64 {
	timestamp := (id >> timestampShift) + epoch
	machineID := (id >> machineIDShift) & maxMachineID
	sequence := id & maxSequence

	return map[string]int64{
		"timestamp": timestamp,
		"machineID": machineID,
		"sequence":  sequence,
	}
}

// GetTimestampFromID 从ID中获取时间戳
func GetTimestampFromID(id int64) int64 {
	return (id >> timestampShift) + epoch
}

// GetMachineIDFromID 从ID中获取机器ID
func GetMachineIDFromID(id int64) int64 {
	return (id >> machineIDShift) & maxMachineID
}

// GetSequenceFromID 从ID中获取序列号
func GetSequenceFromID(id int64) int64 {
	return id & maxSequence
}

2. Token生成/解析逻辑(pkg/auth/jwt.go

package auth  // 认证相关功能包

import (
	"errors"
	"time"

	"golang-server/config"  // 项目配置模块

	"github.com/golang-jwt/jwt/v5"  // JWT官方库
)

// Claims 自定义JWT声明结构,包含用户信息和标准声明
type Claims struct {
	UserID   int64  `json:"user_id"`   // 用户唯一标识
	Username string `json:"username"`  // 用户名
	Role     string `json:"role"`      // 用户角色
	jwt.RegisteredClaims              // 嵌入标准声明(过期时间、签发者等)
}

// GenerateToken 生成JWT访问令牌
// @param userID 用户ID
// @param username 用户名
// @param role 用户角色
// @return 签名的令牌字符串
// @return 错误信息(如果有)
func GenerateToken(userID int64, username, role string) (string, error) {
	cfg := config.GetConfig()  // 获取应用配置

	// 初始化声明信息
	claims := Claims{
		UserID:   userID,
		Username: username,
		Role:     role,
		RegisteredClaims: jwt.RegisteredClaims{
			// 设置过期时间(从配置读取秒数)
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(
				time.Duration(cfg.JWT.ExpireTime) * time.Second)),
			// 设置签发时间
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			// 设置生效时间(立即生效)
			NotBefore: jwt.NewNumericDate(time.Now()),
		},
	}

	// 使用HS256算法创建带声明的令牌
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	// 使用配置密钥签名令牌
	return token.SignedString([]byte(cfg.JWT.Secret))
}

// ParseToken 解析并验证JWT令牌
// @param tokenString 待验证的令牌字符串
// @return 解析后的声明信息
// @return 错误信息(如果令牌无效或过期)
func ParseToken(tokenString string) (*Claims, error) {
	cfg := config.GetConfig()  // 获取应用配置

	// 带声明解析令牌
	token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
		// 验证签名算法是否正确
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, jwt.ErrSignatureInvalid
		}
		// 返回签名密钥
		return []byte(cfg.JWT.Secret), nil
	})

	if err != nil {
		return nil, err  // 返回解析错误(过期/格式错误等)
	}

	// 类型断言验证自定义声明
	if claims, ok := token.Claims.(*Claims); ok && token.Valid {
		return claims, nil  // 返回有效声明
	}

	return nil, errors.New("invalid token")  // 令牌无效
}

// RefreshToken 刷新访问令牌
// @param tokenString 旧令牌字符串
// @return 新令牌字符串
// @return 错误信息(如果旧令牌无效)
func RefreshToken(tokenString string) (string, error) {
	// 解析旧令牌获取用户信息
	claims, err := ParseToken(tokenString)
	if err != nil {
		return "", err  // 旧令牌无效时返回错误
	}

	// 使用原有用户信息生成新令牌
	return GenerateToken(claims.UserID, claims.Username, claims.Role)
}

总结与次日计划

第二天通过数据库自动化解决了手动操作的繁琐与风险,通过MongoDB 扩展丰富了数据存储能力,基础工具链的升级则为后续开发奠定了稳定基础。
次日将进行需求评审及表设计,就先不更新了


网站公告

今日签到

点亮在社区的每一天
去签到