项目跑起来之前的那些事

发布于:2025-09-01 ⋅ 阅读:(18) ⋅ 点赞:(0)

小组放了十天假,闲在家里也怪无聊,正好可以读读学长写的项目代码,让我瞧一瞧学长的风姿(~ ̄▽ ̄)~

思来想去,暂时还是读 河南师范大学附属中学图书馆 这个项目 ,更贴近我此刻的水准,让我舒服点。

目录

总:

一、初始化配置

1、调用入口:

2、配置初始化函数详解(核心逻辑示例)

3、配置文件结构

4、配置结构体定义

5、全局配置储存

二、数据库初始化

1、调用入口

2、配置检查

3、构建数据库连接字符串

4、创建数据库引擎

5、配置数据库日志

6、配置数据库时区

7、配置数据库连接池

8、表结构的同步

9、组合以上函数

三、服务层依赖注入

1、服务容器:依赖管理的"中央枢纽"

2、服务供应商接口(定契约)

3、服务供应商实现(持有服务实例)

4、服务实例化(工厂模式)

5、全局注册:将服务注入容器

四、控制器层依赖注入

五、路由初始化

1、初始化路由

2、顶层路由容器(统一管理路由)

3、模块路由组

4、router.Init() 核心实现

5、路由组初始化(例)

六、定时任务的启动

1、启动入口

2、管理器结构

3、初始化管理器

4、定时任务设置

5、核心同步方法

6、获取什么样的信息?

7、定期清扫

8、优雅停止


: 为防止他人公司的权益被侵犯,本篇博客将会以自己学习总结的知识为基准,并借助伪代码进行通用的逻辑展示。

如果排除Dockerfile,这就是本项目的基本骨架:

📁 api/                    # 主要代码目录
├── 📁 controller/         # 控制器层(处理HTTP请求)
├── 📁 service/           # 业务逻辑层
├── 📁 model/             # 数据模型层
├── 📁 router/            # 路由配置
├── 📁 middleware/        # 中间件
├── 📁 db/               # 数据库连接
├── 📁 conf/             # 配置文件
└── main.go              # 程序入口

在这里,我们可以看到,以上是前后端分离后的标准的MVC架构

处理请求的途径,大致如下:

HTTP请求 → Router → Middleware → Controller → Service → Model → Database
                                     ↓
                              Response ← JSON序列化 ← 业务处理

任何事物,都会有一个先后顺序,先者会为后者铺路,两者相辅相成。

而我对本系统是如何启动的,非常感兴趣。

所以本篇博客,主要聚焦在,本系统开启后,代码层面那些发挥了作用,为什么会发挥作用。

总:

程序入口 main()
    ↓
1. 配置初始化 conf.InitConfig()
    ↓
2. 数据库初始化 db.InitDB()
    ↓
3. 服务层依赖注入 service.GroupApp
    ↓
4. 控制器层依赖注入 controller.ApiGroupApp
    ↓
5. 资源初始化 initBaseResource()
    ↓
6. 路由初始化 router.Init()
    ↓
7. HTTP服务器启动 httpServer.Start()
    ↓
8. 定时任务启动 cron.NewOperationLogSyncManager()
    ↓
9. 监控服务启动 /metrics
    ↓
10. 业务定时任务 Graduate/RemindReturn/CancelBorrow
    ↓
11. 信号监听 优雅关闭

如下为代码逻辑:

func main() {
    // 初始化配置与数据连接
    config.Init()
    db.InitDB()

    // 注册服务与控制器
    service.Init()
    controller.Init(service)

    // 初始化基础资源
    initResources()

    // 启动主服务
    router := router.Setup()
    server := httpServer.Start(router)

    // 启动后台任务
    go startBackgroundTask()

    // 启动监控服务
    startMonitorServer()

    // 注册其他定时任务
    cron.RegisterTasks()

    // 优雅退出处理
    waitForExit()
    server.Shutdown()
    log.Info("服务已停止")
}

// 初始化基础资源
func initResources() {
    if err := service.InitResources(); err != nil {
        log.Error("资源初始化失败: ", err)
    }
}

// 启动后台任务(示例)
func startBackgroundTask() {
    if err := task.Start(); err != nil {
        log.Error("后台任务启动失败: ", err)
    } else {
        log.Info("后台任务启动成功")
    }
}

// 启动监控服务(示例)
func startMonitorServer() {
    http.Handle("/metrics", monitor.Handler())
    if err := http.ListenAndServe(monitor.Addr(), nil); err != nil {
        log.Error("监控服务启动失败: ", err)
    }
}

// 等待退出信号
func waitForExit() {
    sig := make(chan os.Signal)
    signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
    <-sig
}

一、初始化配置

1、调用入口:

func main() {    
    // 传入配置文件存放目录,启动配置初始化(通用路径命名)
    conf.InitConfig("config-dir")  
    // 后续业务逻辑(如数据库初始化、服务启动等)
    // ...
}

2、配置初始化函数详解(核心逻辑示例)

// InitConfig 通用配置初始化函数:加载配置文件、绑定环境变量、初始化日志等
func InitConfig(configDir string) {    
    // 1. 配置Viper:指定配置文件的路径、类型与名称
    viper.AddConfigPath(configDir)        // 添加配置文件搜索目录
    viper.SetConfigType("yml")            // 支持YAML格式(通用配置格式)
    viper.SetConfigName("app-config")     // 配置文件名(不含扩展名,避免项目专属命名)

    // 2. 读取配置文件:失败则终止应用(基础容错逻辑)
    if err := viper.ReadInConfig(); err != nil {
    // 注意,读取成功后,会存入内存
        log.Fatalf("配置文件读取失败: %v", err)  // 通用日志函数,替换项目专属日志名
    }

    // 3. 绑定环境变量:适配容器化/多环境部署(通用变量绑定逻辑)
    _ = viper.BindEnv("server.port", "APP_PORT")       // 服务端口绑定环境变量
    _ = viper.BindEnv("server.monitorPort", "MONITOR_PORT")  // 监控端口绑定
    // 更多通用配置项的环境变量绑定(如数据库地址、超时时间等)
    // ...

    // 4. 初始化日志级别:从配置动态读取(通用日志级别适配)
    switch viper.GetString("log.level") {
    case "debug":
        log.SetLevel(log.DebugLevel)
    case "info":
        log.SetLevel(log.InfoLevel)
    case "warn":
        log.SetLevel(log.WarnLevel)
    default:
        log.SetLevel(log.InfoLevel)  // 默认级别兜底
    }

    // 5. 启用配置热更新:支持开发/调试场景,无需重启应用
    viper.WatchConfig()

    // 6. 调试日志:打印加载的配置项(开发环境用,生产可关闭)
    log.Infoln("--------- 加载的配置列表 --------")
    for _, key := range viper.AllKeys() {
        log.Infoln(key, ":", viper.Get(key))
    }

    // 7. 初始化全局配置对象:将Viper配置映射到结构体(通用类型安全处理)
    global.AppConfig = NewConfig() 
}

// 这我一般放在model中,刚好在model下方
func NewConfig() *Config {
    ...
    为结构体绑定具体内容 
    _api := &Api{
		Host:        viper.GetString("api.host"),
		....
	}
	_log := &Log{
		Level:            viper.GetString("log.level"),
		...
	}
    ...
	return &Config{
		Log:      _log,
		Api:      _api,
		...
	}
}

3、配置文件结构

咱们这里按照 “服务基础配置(端口、地址)、日志配置、第三方依赖配置(数据库、缓存)” 分层,示例格式(YAML):

# 通用配置文件示例(无业务属性)
server:
  port: 8080
  monitorPort: 9090
log:
  level: info
  output: console
db:
  host: ${DB_HOST}  # 引用环境变量,避免硬编码
  port: ${DB_PORT:3306}

4、配置结构体定义

与配置文件分层对应,确保类型安全

// Config 通用配置结构体(无业务专属字段)
type Config struct {
    Server ServerConfig `mapstructure:"server"`
    Log    LogConfig    `mapstructure:"log"`
    DB     DBConfig     `mapstructure:"db"`
}

type ServerConfig struct {
    Port        int    `mapstructure:"port"`
    MonitorPort int    `mapstructure:"monitorPort"`
}

// 其他子结构体(LogConfig、DBConfig)按通用字段定义
// ...

5、全局配置储存

通过通用包(如global)管理全局配置,提供安全访问接口,避免直接暴露全局变量。

二、数据库初始化

1、调用入口

func main() {    
    // ...
    db.InitDB()
    // ...
}

2、配置检查

// InitDB 通用PostgreSQL数据库初始化入口
func InitDB() {
    // 检查全局配置是否已加载(通用配置校验逻辑)
    if global.AppConfig == nil {
        log.Fatal("数据库初始化失败:请先初始化应用配置")
    }
    // 若数据库配置项缺失,同样终止流程(基础参数校验)
    if global.AppConfig.DB == nil {
        log.Fatal("数据库初始化失败:配置中缺少数据库相关参数")
    }
}

3、构建数据库连接字符串

// 步骤2:拼接PostgreSQL通用连接字符串(DSN)
func buildDSN() string {
    // 从全局配置中读取通用数据库参数
    dbCfg := global.AppConfig.DB
    // 通用DSN格式:适配PostgreSQL标准连接协议
    dsn := fmt.Sprintf(
        "%s://%s:%s@%s:%d/%s?sslmode=disable",
        dbCfg.Driver,   // 数据库驱动(固定为postgres)
        dbCfg.Username, // 数据库用户名(配置注入)
        dbCfg.Password, // 数据库密码(配置/环境变量注入)
        dbCfg.Host,     // 数据库主机地址
        dbCfg.Port,     // 数据库端口
        dbCfg.Name      // 数据库名
    )
    return dsn
}

4、创建数据库引擎

// 步骤3:初始化XORM引擎
func createDBEngine(dsn string) (*xorm.Engine, error) {
    // 调用XORM创建引擎,传入驱动与DSN
    engine, err := xorm.NewEngine(global.AppConfig.DB.Driver, dsn)
    if err != nil {
        // 日志仅提示错误,不暴露完整DSN
        log.Fatalf("数据库引擎创建失败:%v", err)
        return nil, err
    }
    log.Info("数据库引擎创建成功")
    return engine, nil
}

5、配置数据库日志

// 步骤4:设置数据库操作日志(通用日志级别适配)
func configDBLog(engine *xorm.Engine) {
    // 启用SQL语句打印(开发环境调试用,生产可配置关闭)
    engine.ShowSQL(true)
    
    // 从全局配置读取日志级别,映射到XORM日志级别
    logLevel := global.AppConfig.Log.Level
    switch logLevel {
    case "debug":
        engine.Logger().SetLevel(log.LOG_DEBUG) // 调试级:打印所有SQL与详情
    case "info":
        engine.Logger().SetLevel(log.LOG_INFO)  // 信息级:打印关键操作
    case "warn":
        engine.Logger().SetLevel(log.LOG_WARNING) // 警告级:仅打印警告与错误
    case "error":
        engine.Logger().SetLevel(log.LOG_ERR)    // 错误级:仅打印错误
    default:
        engine.Logger().SetLevel(log.LOG_INFO)   // 默认:信息级
    }
    log.Infof("数据库日志级别已设置为:%s", logLevel)
}

6、配置数据库时区

// 步骤5:同步数据库时区与应用时区(通用时区处理)
func configDBTimezone(engine *xorm.Engine) {
    // 1. 查询数据库当前时区(通用SQL,无业务关联)
    results, err := engine.Query("show timezone")
    if err != nil {
        log.Errorf("查询数据库时区失败:%v", err)
        return
    }
    // 提取数据库时区(简化处理,聚焦逻辑而非具体字段解析)
    dbTimezone := string(results[0]["TimeZone"])
    log.Infof("数据库当前时区:%s", dbTimezone)
    
    // 2. 加载时区并设置到引擎
    if loc, err := time.LoadLocation(dbTimezone); err != nil {
        log.Errorf("加载时区失败:%v,将使用默认时区", err)
    } else {
        engine.DatabaseTZ = loc // 设置数据库时区
    }
    
    // 3. 设置应用与数据库的时区对齐(通用本地时区配置)
    engine.TZLocation = time.Local // 或从全局配置读取本地时区
}

在这里,我详细的说一下,DatabaseTZ与TZLocation的区别与作用。
他们两者的存在,一个是为了解决数据库存储的时间,另一个是为了优化用户读取的时间。

// 数据库时区:UTC-5(数据库服务器的实际时区)
engine.DatabaseTZ = loc  // 从 "show timezone" 查询得到

存储时 :应用程序时间 → 数据库时区 → 存储到数据库

// 应用程序时区:UTC+8(用户期望看到的时区)
engine.TZLocation = g.Loc  // 从配置文件读取

读取时 :数据库时间 → 应用程序时区 → 显示给用户

实际应用场景:

- 数据库服务器在美国(UTC-5)
- 应用程序部署在中国(UTC+8)
- 用户在中国使用系统

1.
 存储时 :应用程序时间 → 数据库时区 → 存储到数据库
2.
 读取时 :数据库时间 → 应用程序时区 → 显示给用户
这样确保了:

- 数据库中的时间数据是一致的
- 用户看到的时间是符合本地习惯的
- 不同时区的用户访问同一系统时,看到的时间都是正确的本地时间

7、配置数据库连接池

// 步骤6:配置数据库连接池(通用性能参数)
func configDBPool(engine *xorm.Engine) {
    dbCfg := global.AppConfig.DB
    // 最大打开连接数:控制同时与数据库建立的连接数
    engine.SetMaxOpenConns(dbCfg.MaxOpenConns)
    // 最大空闲连接数:空闲时保留的连接数,避免频繁创建连接
    engine.SetMaxIdleConns(dbCfg.MaxIdleConns)
    // 连接最大生存时间:避免长期空闲连接失效
    connLifetime := time.Duration(dbCfg.ConnMaxLifetimeSec) * time.Second
    engine.SetConnMaxLifetime(connLifetime)
    
    log.Info("数据库连接池参数配置完成")
}

在这里我详细的说一下,最大连接数、最大空闲数、最大生存时间。

最大连接数:

1、MaxOpenConns(最大打开连接数)

- 高并发场景 :假设你的图书管理系统同时有 200 个用户在借书
- 没有限制时 :可能会创建 200 个数据库连接,数据库服务器压力巨大
- 设置为 100 后 :最多只能有 100 个连接,第 101-200 个请求需要等待
- 实际效果 :保护数据库不被过多连接压垮

// 配置最大空闲连接数为 10

2、engine.SetMaxIdleConns(10)

- 业务高峰期 :上午 9-11 点,有 50 个活跃连接处理借还书业务
- 业务低谷期 :下午 2-4 点,只有 5 个用户在使用系统
- 没有空闲连接 :每次查询都要重新建立连接(耗时 10-50ms)
- 保留 10 个空闲连接 :下次查询直接使用现有连接(耗时 1-2ms)

// 配置连接最大生存时间为 1 小时

3、connLifetime := time.Duration(3600) * time.Second

      engine.SetConnMaxLifetime(connLifetime)

- 问题场景 :数据库服务器配置了 8 小时自动断开空闲连接
- 应用程序 :保持连接 10 小时不释放
- 结果 :连接已被数据库断开,但应用程序不知道,使用时报错
- 设置 1 小时后 :应用程序主动在 1 小时后关闭连接,避免使用失效连接

8、表结构的同步

// 步骤7:(可选)表结构同步(通用逻辑,无业务关联)
// 注:生产环境建议通过迁移工具(如go-migrate)管理表结构,而非运行时同步
func syncDBTable(engine *xorm.Engine) {
    // 示例:同步通用业务表
    err := engine.Sync2(
        new(common.BaseTable),  // 通用基础表(如含ID、创建时间的父表)
        new(common.DataTable)   // 通用数据表(示例)
    )
    if err != nil {
        log.Errorf("表结构同步失败:%v", err)
        return
    }
    log.Info("表结构同步完成")
}

9、组合以上函数

// 整合所有步骤:完整初始化流程
func InitDB() {
    // 步骤1:检查配置
    if global.AppConfig == nil || global.AppConfig.DB == nil {
        log.Fatal("数据库初始化失败:配置未就绪")
    }
    
    // 步骤2:构建DSN
    dsn := buildDSN()
    
    // 步骤3:创建引擎
    engine, err := createDBEngine(dsn)
    if err != nil {
        return
    }
    
    // 步骤4:配置日志
    configDBLog(engine)
    
    // 步骤5:配置时区
    configDBTimezone(engine)
    
    // 步骤6:配置连接池
    configDBPool(engine)
    
    // 步骤7:(可选)表结构同步(根据场景启用)
    // syncDBTable(engine)
    
    // 步骤8:保存引擎到全局变量(通用全局存储,无业务属性)
    global.DBEngine = engine
    log.Info("数据库初始化完成,引擎已就绪")
}

三、服务层依赖注入

依赖注入(Dependency Injection,DI)是解耦代码、提升可测试性与可维护性的核心设计模式
直接看不懂也没关系,多看几次就会了

1、服务容器:依赖管理的"中央枢纽"

service/container.go

// ServiceContainer 通用服务容器结构体
type ServiceContainer struct {
    // BaseServiceSupplier:持有所有基础服务的供应商
    BaseServiceSupplier service.BaseSupplier
}

// GlobalContainer 全局服务容器实例:提供全应用服务访问入口
var GlobalContainer = new(ServiceContainer)

2、服务供应商接口(定契约)

通过接口规范服务的获取方式(Getter 模式),确保访问服务的一致性。同时隔离服务定义与实现

文件:service/supplier.go

// BaseSupplier 通用服务供应商接口(剔除业务专属服务名)
type BaseSupplier interface {
    // 通用业务服务:替换具体业务服务
    GetUserService() *UserService
    GetOrderService() *OrderService
    GetLogService() *LogService
    GetCacheService() *CacheService
    GetStatService() *StatService
    // 可扩展:根据通用场景增加其他基础服务
    ...
}

啥是Getter模式呢?

Getter方法的核心价值:封装的字段统一小写,仅通过方法对外暴露访问能力(正如下方所示:

3、服务供应商实现(持有服务实例)

// baseSupplier 通用服务供应商的具体实现
// 所有服务字段为「小写非导出」,仅通过Getter方法暴露
type baseSupplier struct {
    // 字段小写(非导出),外部无法直接访问/修改
    userService   *UserService
    orderService  *OrderService
    logService    *LogService
    cacheService  *CacheService
    statService   *StatService
}
// 通过大写Getter方法(符合Go接口规范)对外暴露服务
// GetUserService 实现BaseSupplier接口的Getter方法,返回用户服务实例
func (s *baseSupplier) GetUserService() *UserService {    
    ...
    return s.userService
}

// GetOrderService 实现BaseSupplier接口的Getter方法,返回订单服务实例
func (s *baseSupplier) GetOrderService() *OrderService {
    ...
    return s.orderService
}
...

4、服务实例化(工厂模式)

通过SetUp函数(工厂模式)统一初始化所有服务,集中管理服务的创建逻辑,便于后续拓展

// SetUp 通用服务初始化函数:创建并返回服务供应商实例
func SetUp() BaseSupplier {
    // 1. 用构造函数初始化服务(非导出字段只能在当前包内赋值)
    userService := NewUserService()
    orderService := NewOrderService()
    logService := NewLogService()
    cacheService := NewCacheService()
    statService := NewStatService()

    // 2. 给baseSupplier的非导出字段赋值(当前包内可访问)
    return &baseSupplier{
        userService:   userService,
        orderService:  orderService,
        logService:    logService,
        cacheService:  cacheService,
        statService:   statService,
    }
}
// 当然这些还可以在精妙些

5、全局注册:将服务注入容器

在注册阶段,将初始化好的服务供应商注入全局容器。

如下:

func main() {
    
    ...
    
    // 1. 初始化服务供应商
    baseSupplier := service.SetUp()
    
    // 2. 将供应商注入全局服务容器
    service.GlobalContainer = &service.ServiceContainer{
        BaseServiceSupplier: baseSupplier,
    }
    
    // 后续:启动服务器、初始化其他组件...
    
    ...

}

注册阶段:在main函数中注册所有服务
解析阶段:在需要时通过 GlobalContainer 获取所有服务
生命周期管理:所有服务都是单例(“准单例”),由容器管理

在我看来,单例模式,有两个要点。
1、整个程序中,只存在唯一实例(本项目据中,通过get...方法获取的服务实例,都是已存在全局变量中的,指向同一个内存)。
2、提供全局访问点,来获取实例。

四、控制器层依赖注入

服务器通过 “接收服务容器” 的方式获取所需服务,避免了直接创建服务实例,降低了耦合。

// ControllerSupplier 通用控制器服务供应商(无业务属性)
type ControllerSupplier struct {
    UserApi  *UserApi  // 控制器实例
    OrderApi *OrderApi // 控制器实例
}

// SetUp 控制器层服务注入:从全局容器获取服务并绑定到控制器
func SetUp(container *service.ServiceContainer) *ControllerSupplier {
    return &ControllerSupplier{
        // 为UserApi注入UserService依赖
        UserApi: &UserApi{
            UserService: container.BaseServiceSupplier.GetUserService(),
        },
        // 为OrderApi注入OrderService依赖
        OrderApi: &OrderApi{
            OrderService: container.BaseServiceSupplier.GetOrderService(),
        },
    }
}

通过这种方式,可以很轻松的实现DI依赖注入。
控制器不在主动依赖new,而是通过容器来被动接收依赖。
因此就更容易维护,mock也更方便。

五、路由初始化

1、初始化路由

在最初router.Init()会创建Gin引擎实例,并完成路由配置,最终传递给HTTP服务器启动

func main() {
    ....   
    // 1. 初始化路由:获取配置完成的Gin引擎
    ginEngine := router.Init()
    // 2. 基于路由引擎创建HTTP服务器并启动
    httpServer := server.GetServerInstance(ginEngine)
    httpServer.Start()
    ....
}

2、顶层路由容器(统一管理路由)

文件:router/router.go

// Routers 顶层路由容器:聚合所有功能模块的路由组
type Routers struct {
    HealthRouterGroup  health.RouterGroup  // 健康检查模块路由组
    BusinessRouterGroup business.RouterGroup // 核心业务模块路由组
}

// GlobalRouters 全局路由容器实例:提供模块路由访问入口
var GlobalRouters = new(Routers)

3、模块路由组

健康检查:

// RouterGroup 健康检查模块路由组:仅包含健康检查相关路由逻辑
type RouterGroup struct {
    BaseHealthRouter // 基础健康检查路由(如存活检测、就绪检测)
}

核心业务:

// RouterGroup 核心业务模块路由组:聚合通用业务路由
type RouterGroup struct {
    UserRouter    // 用户相关路由(通用模块)
    OrderRouter   // 订单相关路由(通用模块)
    ResourceRouter// 资源相关路由(通用模块)
    LogRouter     // 日志相关路由(通用模块)
}

4、router.Init() 核心实现

router.Init() 是路由初始化的核心函数,负责如下四个责任:
1、创建Gin引擎
2、注册中间件
3、划分路由
4、绑定接口

(伪代码:)

// Init 通用路由初始化函数:返回配置完成的Gin引擎
func Init() *gin.Engine {
    // 3.1 步骤1:创建Gin引擎 + 注册基础中间件
    // - 新建Gin引擎(默认模式,可根据环境切换debug/release)
    ginEngine := gin.New()
    
    // 注册通用中间件:跨域、日志、异常恢复
    ginEngine.Use(
        middleware.Cors(), // 跨域中间件(适配前后端分离场景)
        // 日志中间件:跳过健康检查路径,避免日志冗余
        gin.LoggerWithConfig(gin.LoggerConfig{
            SkipPaths: []string{"/health/live", "/health/ready"},
        }),
        gin.Recovery(), // 异常恢复中间件:防止panic导致服务崩溃
    )

    // 3.2 步骤2:注册性能分析工具(通用调试能力)
    pprof.Register(ginEngine) // 注册pprof,支持/debug/pprof路径分析性能

    // 3.3 步骤3:划分路由分组(按访问权限隔离)
    // (1)公共路由组:无需认证,如健康检查、公开注册接口
    publicGroup := ginEngine.Group("")
    {
        // 绑定健康检查路由(来自健康检查模块)
        healthRouter := GlobalRouters.HealthRouterGroup
        healthRouter.InitBaseHealthRouter(publicGroup)
        
        // 绑定公共业务路由(如用户注册)
        businessRouter := GlobalRouters.BusinessRouterGroup
        businessRouter.InitPublicUserRouter(publicGroup)
    }

    // (2)认证路由组:需登录/权限校验,包含核心业务接口
    authGroup := ginEngine.Group("")
    // 注册认证中间件:拦截未登录请求,跳过健康检查路径
    authGroup.Use(middleware.AuthWithConfig(middleware.AuthConfig{
        SkipPaths: []string{"/health/live", "/health/ready"},
    }))
    {
        // 绑定核心业务路由(来自业务模块)
        businessRouter := GlobalRouters.BusinessRouterGroup
        businessRouter.InitUserRouter(authGroup)    // 用户管理路由
        businessRouter.InitOrderRouter(authGroup)   // 订单管理路由
        businessRouter.InitResourceRouter(authGroup)// 资源管理路由
        businessRouter.InitLogRouter(authGroup)     // 日志查询路由
    }

    // 3.4 步骤4:注册文档与静态资源路由
    // (1)API文档路由:集成Swagger,便于接口调试
    authGroup.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
    
    // (2)静态资源路由:提供文件访问能力(如用户上传的图片)
    ginEngine.Static("/static/files", "./static/files")

    // 3.5 步骤5:配置通用参数验证器
    // 为Gin绑定自定义参数验证规则(如ID格式校验、时间格式校验)
    if validatorEngine, ok := binding.Validator.Engine().(*validator.Validate); ok {
        // 注册“ID格式验证”规则
        _ = validatorEngine.RegisterValidation("common_id_check", validator.CommonIDValidator)
        // 注册“时间格式验证”规则
        _ = validatorEngine.RegisterValidation("time_format_check", validator.TimeFormatValidator)
    }

    return ginEngine
}

5、路由组初始化(例)

每个模块的路由会通过独立的InitXxxRouter方法绑定到对应的路由组 "实现路由逻辑与容器解耦"

// UserRouter 用户模块路由:封装用户相关接口注册逻辑
type UserRouter struct{}

// InitUserRouter 将用户接口绑定到认证路由组
func (ur *UserRouter) InitUserRouter(routerGroup *gin.RouterGroup) {
    // 1. 创建用户模块路由子分组(路径前缀:/user)
    userSubGroup := routerGroup.Group("user")
    
    // 2. 获取用户控制器实例(通过依赖注入容器)
    userApi := controller.GlobalApiContainer.BusinessApiGroup.GetUserApi()
    
    // 3. 绑定具体接口(HTTP方法+路径+控制器处理函数)
    userSubGroup.POST("/add", userApi.CreateUser)    // 创建用户
    userSubGroup.GET("/info", userApi.GetUserInfo)   // 获取用户信息
    userSubGroup.PUT("/update", userApi.UpdateUser)  // 更新用户信息
    userSubGroup.DELETE("/delete", userApi.DeleteUser)// 删除用户
}

六、定时任务的启动

在了解这个包之前,必须要了解github.com/robfig/cron/v3,这个包的用途。
拥有一定基础,才能更好的进行学习。

1、这里的定时任务是为了解决什么问题?

想象一下,你的系统需要定期做这样一件事:

1、从 A 数据库(如分析型数据库)获取原始数据
2、经过筛选和转换后,存储到 B 数据库(如业务型数据库)
3、定期清理 B 数据库中过期的数据

这个定时任务就像一个智能搬运工,按照设定的时间规律自动完成这些工作,无需人工干预。

咱们这里可以假设:
这个搬运工:每5分钟去 A 数据库 检查一次,把重要的用户操作"搬运"到 B 数据库 中,方便后续查询和分析。

代码结构:

main.go (启动入口)
└── cron/data_sync_manager.go (定时任务管理器)
    └── service/sync_service.go (业务逻辑实现)

1、启动入口

// 初始化并启动数据同步定时任务
dataSyncManager := cron.NewDataSyncManager()
go func() {
    if err := dataSyncManager.Start(); err != nil {
        log.WithError(err).Error("定时任务启动失败")
    } else {
        log.Info("定时任务启动成功")
    }
}()

通过go,合理运用go语言的特性,高并发

2、管理器结构

type DataSyncManager struct {
    cronEngine          *cron.Cron       // 定时器引擎(闹钟)
    syncService         *SyncService     // 同步服务(实际干活的)
    lastSyncTime        time.Time        // 上次同步时间(工作记录)
    consecutiveFailures int              // 连续失败次数(错误跟踪)
    mutex               sync.RWMutex     // 读写锁(保护共享数据)
}

具体作用:

cronEngine:就像闹钟,到点提醒该工作了
syncService:就像具体干活的工人
lastSyncTime:就像工作日记,记录上次工作时间
mutex:就像日记本的锁,防止多人同时修改

3、初始化管理器

func NewDataSyncManager() *DataSyncManager {
    // 1. 确保目标数据库表存在
    if err := ensureTargetTableExists(); err != nil {
        log.WithError(err).Error("确保目标数据表存在失败")
    }

    return &DataSyncManager{
        // 2. 创建带日志的定时器
        cronEngine: cron.New(cron.WithLogger(
            cron.VerbosePrintfLogger(log.StandardLogger()))),
        // 3. 初始化同步服务
        syncService: NewSyncService(),
        // 4. 初始同步时间设为5分钟前
        lastSyncTime: time.Now().Add(-5 * time.Minute),
    }
}

4、定时任务设置

func (m *DataSyncManager) Start() error {
    // 1. 每5分钟执行一次数据同步
    _, err := m.cronEngine.AddFunc("*/5 * * * *", func() {
        m.syncDataWithRetry()
    })
    if err != nil {
        return err
    }
    
    // 2. 每月1号凌晨2点执行数据清理
    _, err = m.cronEngine.AddFunc("0 2 1 * *", func() {
        m.cleanExpiredData()
    })
    if err != nil {
        return err
    }
    
    m.cronEngine.Start() // 启动定时器
    return nil
}

初次接触是需要掌握这个的:

时间表达式入门:

  • */5 * * * *:每 5 分钟(* 表示任意值,/ 表示间隔)
  • 0 2 1 * *:每月 1 号凌晨 2 点(分 时 日 月 周)

常见表达式示例:

  • 0 * * * *:每小时整点
  • 0 0 * * *:每天凌晨
  • 0 0 * * 0:每周日凌晨

5、核心同步方法

func (m *DataSyncManager) syncDataWithRetry() {
    // 安全保护:捕获可能的程序异常
    defer func() {
        if r := recover(); r != nil {
            log.WithField("error", r).Error("同步任务发生异常")
        }
    }()

    // 读取上次同步时间(读操作加读锁)
    m.mutex.RLock()
    lastSync := m.lastSyncTime
    m.mutex.RUnlock()

    // 执行同步
    err := m.syncService.SyncData(lastSync)

    if err != nil {
        // 同步失败:更新失败计数(写操作加写锁)
        m.mutex.Lock()
        m.consecutiveFailures++
        m.mutex.Unlock()
        
        log.WithError(err).Error("数据同步失败")
        return
    }

    // 同步成功:更新状态
    m.mutex.Lock()
    m.consecutiveFailures = 0  // 重置失败计数
    // 时间向前推1分钟,避免遗漏边缘数据
    m.lastSyncTime = time.Now().Add(-1 * time.Minute)
    m.mutex.Unlock()
}

注意,一般同步时间,都需要向前推进1分钟,这是为了防止因网络延迟,而导致的时间差。
这样,此后每一次,都会刚好向前推进1分钟。

6、获取什么样的信息?

func (s *SyncService) querySourceData(startTime, endTime time.Time) ([]SourceData, error) {
    query := `     
    SELECT id, content, create_time, status, user_id, operation_type
    FROM source_table 
    WHERE create_time >= ? AND create_time < ?
        AND status = 200                -- 只同步成功的数据
        AND user_id != 'anonymous'      -- 排除匿名用户
        AND operation_type != 'view'    -- 排除仅查看的操作
    ORDER BY create_time ASC
    LIMIT 10000                        -- 限制单次处理数量
    `
    // 执行查询并返回结果...
}

查询优化技巧:

  • 时间范围过滤:只查需要的时间段
  • 状态过滤:只同步有效数据
  • 数量限制:防止一次性处理过多数据导致内存问题

7、定期清扫

func (m *DataSyncManager) cleanExpiredData() {
    // 安全保护
    defer func() {
        if r := recover(); r != nil {
            log.WithField("error", r).Error("数据清理任务发生异常")
        }
    }()

    log.Info("开始执行数据清理任务")
    startTime := time.Now()
    
    // 清理一年前的数据
    if err := m.syncService.CleanExpiredData(1); err != nil {
        log.WithError(err).Error("数据清理任务失败")
        return
    }
    
    log.WithField("耗时", time.Since(startTime)).Info("数据清理任务完成")
}

// 服务层实现
func (s *SyncService) CleanExpiredData(expireYears int) error {
    expireTime := time.Now().AddDate(-expireYears, 0, 0)
    
    // 执行删除操作
    affectedRows, err := global.DB.Where("create_time < ?", expireTime).
        Delete(&BizDataTable{})
    
    log.Info("清理了", affectedRows, "条过期数据")
    return err
}

节省空间、提高性能、并且符合数据保留政策

8、优雅停止

func (m *DataSyncManager) Stop() {
    if m.cronEngine == nil {
        return
    }
    
    // 停止定时器并等待当前任务完成
    ctx := m.cronEngine.Stop()
    
    select {
    case <-ctx.Done():
        log.Info("定时任务已安全停止")
    case <-time.After(10 * time.Second):
        log.Warn("定时任务停止超时")
    }
}

就像下班前要把手头的工作做完再走,避免工作到一半导致数据混乱。

本篇博客,就先到这里,后续会继续出博客进行补充( •̀ ω •́ )✧


在最后我想特别的感谢,智超学长提供的学习机会,与凯龙学长的耐心指导 ^0^



网站公告

今日签到

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