Files
junhong_cmp_fiber/cmd/api/main.go
huang 984ccccc63 docs(constitution): 新增数据库设计原则(v2.4.0)
在项目宪章中新增第九条原则"数据库设计原则",明确禁止使用数据库外键约束和ORM关联标签。

主要变更:
- 新增原则IX:数据库设计原则(Database Design Principles)
- 强制要求:数据库表不得使用外键约束
- 强制要求:GORM模型不得使用ORM关联标签(foreignKey、hasMany等)
- 强制要求:表关系必须通过ID字段手动维护
- 强制要求:关联数据查询必须显式编写,避免ORM魔法
- 强制要求:时间字段由GORM处理,不使用数据库触发器

设计理念:
- 提升业务逻辑灵活性(无数据库约束限制)
- 优化高并发性能(无外键检查开销)
- 增强代码可读性(显式查询,无隐式预加载)
- 简化数据库架构和迁移流程
- 支持分布式和微服务场景

版本升级:2.3.0 → 2.4.0(MINOR)
2025-11-13 13:40:19 +08:00

246 lines
6.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"context"
"os"
"os/signal"
"strconv"
"syscall"
"github.com/bytedance/sonic"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/compress"
"github.com/gofiber/fiber/v2/middleware/requestid"
"github.com/google/uuid"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
"github.com/break/junhong_cmp_fiber/internal/handler"
"github.com/break/junhong_cmp_fiber/internal/middleware"
"github.com/break/junhong_cmp_fiber/internal/service/order"
"github.com/break/junhong_cmp_fiber/internal/service/user"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/config"
"github.com/break/junhong_cmp_fiber/pkg/database"
"github.com/break/junhong_cmp_fiber/pkg/logger"
"github.com/break/junhong_cmp_fiber/pkg/queue"
"github.com/break/junhong_cmp_fiber/pkg/validator"
)
func main() {
// 加载配置
cfg, err := config.Load()
if err != nil {
panic("加载配置失败: " + err.Error())
}
// 初始化日志
if err := logger.InitLoggers(
cfg.Logging.Level,
cfg.Logging.Development,
logger.LogRotationConfig{
Filename: cfg.Logging.AppLog.Filename,
MaxSize: cfg.Logging.AppLog.MaxSize,
MaxBackups: cfg.Logging.AppLog.MaxBackups,
MaxAge: cfg.Logging.AppLog.MaxAge,
Compress: cfg.Logging.AppLog.Compress,
},
logger.LogRotationConfig{
Filename: cfg.Logging.AccessLog.Filename,
MaxSize: cfg.Logging.AccessLog.MaxSize,
MaxBackups: cfg.Logging.AccessLog.MaxBackups,
MaxAge: cfg.Logging.AccessLog.MaxAge,
Compress: cfg.Logging.AccessLog.Compress,
},
); err != nil {
panic("初始化日志失败: " + err.Error())
}
defer func() {
_ = logger.Sync() // 忽略 sync 错误shutdown 时可能已经关闭)
}()
appLogger := logger.GetAppLogger()
appLogger.Info("应用程序启动中...",
zap.String("address", cfg.Server.Address),
)
redisAddr := cfg.Redis.Address + ":" + strconv.Itoa(cfg.Redis.Port)
// 连接 Redis
redisClient := redis.NewClient(&redis.Options{
Addr: redisAddr,
Password: cfg.Redis.Password,
DB: cfg.Redis.DB,
PoolSize: cfg.Redis.PoolSize,
MinIdleConns: cfg.Redis.MinIdleConns,
DialTimeout: cfg.Redis.DialTimeout,
ReadTimeout: cfg.Redis.ReadTimeout,
WriteTimeout: cfg.Redis.WriteTimeout,
})
defer func() {
if err := redisClient.Close(); err != nil {
appLogger.Error("关闭 Redis 客户端失败", zap.Error(err))
}
}()
// 测试 Redis 连接
ctx := context.Background()
if err := redisClient.Ping(ctx).Err(); err != nil {
appLogger.Fatal("连接 Redis 失败", zap.Error(err))
}
appLogger.Info("Redis 已连接", zap.String("address", redisAddr))
// 初始化 PostgreSQL 连接
db, err := database.InitPostgreSQL(&cfg.Database, appLogger)
if err != nil {
appLogger.Fatal("初始化 PostgreSQL 失败", zap.Error(err))
}
defer func() {
sqlDB, _ := db.DB()
if sqlDB != nil {
if err := sqlDB.Close(); err != nil {
appLogger.Error("关闭 PostgreSQL 连接失败", zap.Error(err))
}
}
}()
// 初始化 Asynq 任务提交客户端
queueClient := queue.NewClient(redisClient, appLogger)
defer func() {
if err := queueClient.Close(); err != nil {
appLogger.Error("关闭 Asynq 客户端失败", zap.Error(err))
}
}()
// 创建令牌验证器
tokenValidator := validator.NewTokenValidator(redisClient, appLogger)
// 初始化 Store 层
store := postgres.NewStore(db, appLogger)
// 初始化 Service 层
userService := user.NewService(store, appLogger)
orderService := order.NewService(store, appLogger)
// 初始化 Handler 层
userHandler := handler.NewUserHandler(userService, appLogger)
orderHandler := handler.NewOrderHandler(orderService, appLogger)
taskHandler := handler.NewTaskHandler(queueClient, appLogger)
healthHandler := handler.NewHealthHandler(db, redisClient, appLogger)
// 启动配置文件监听器(热重载)
watchCtx, cancelWatch := context.WithCancel(context.Background())
defer cancelWatch()
go config.Watch(watchCtx, appLogger)
// 创建 Fiber 应用
app := fiber.New(fiber.Config{
AppName: "君鸿卡管系统 v1.0.0",
StrictRouting: true,
CaseSensitive: true,
JSONEncoder: sonic.Marshal,
JSONDecoder: sonic.Unmarshal,
Prefork: cfg.Server.Prefork,
ReadTimeout: cfg.Server.ReadTimeout,
WriteTimeout: cfg.Server.WriteTimeout,
})
// 中间件注册(顺序很重要)
// 1. Recover - 必须第一个,捕获所有 panic
app.Use(middleware.Recover(appLogger))
// 2. RequestID - 为每个请求生成唯一 ID
app.Use(requestid.New(requestid.Config{
Generator: func() string {
return uuid.NewString()
},
}))
// 3. Logger - 记录所有请求
app.Use(logger.Middleware())
// 4. Compress - 响应压缩
app.Use(compress.New(compress.Config{
Level: compress.LevelDefault,
}))
// 路由注册
// 公共端点(无需认证)
app.Get("/health", healthHandler.Check)
// API v1 路由组
v1 := app.Group("/api/v1")
// 受保护的端点(需要认证)
if cfg.Middleware.EnableAuth {
v1.Use(middleware.KeyAuth(tokenValidator, appLogger))
}
// 可选:启用限流器
if cfg.Middleware.EnableRateLimiter {
var rateLimitStorage fiber.Storage
// 根据配置选择存储后端
if cfg.Middleware.RateLimiter.Storage == "redis" {
rateLimitStorage = middleware.NewRedisStorage(
cfg.Redis.Address,
cfg.Redis.Password,
cfg.Redis.DB,
cfg.Redis.Port,
)
appLogger.Info("限流器使用 Redis 存储", zap.String("redis_address", cfg.Redis.Address))
} else {
rateLimitStorage = nil // 使用内存存储
appLogger.Info("限流器使用内存存储")
}
v1.Use(middleware.RateLimiter(
cfg.Middleware.RateLimiter.Max,
cfg.Middleware.RateLimiter.Expiration,
rateLimitStorage,
))
}
// 用户路由
v1.Post("/users", userHandler.CreateUser)
v1.Get("/users/:id", userHandler.GetUser)
v1.Put("/users/:id", userHandler.UpdateUser)
v1.Delete("/users/:id", userHandler.DeleteUser)
v1.Get("/users", userHandler.ListUsers)
// 订单路由
v1.Post("/orders", orderHandler.CreateOrder)
v1.Get("/orders/:id", orderHandler.GetOrder)
v1.Put("/orders/:id", orderHandler.UpdateOrder)
v1.Get("/orders", orderHandler.ListOrders)
// 任务路由
v1.Post("/tasks/email", taskHandler.SubmitEmailTask)
v1.Post("/tasks/sync", taskHandler.SubmitSyncTask)
// 优雅关闭
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
go func() {
if err := app.Listen(cfg.Server.Address); err != nil {
appLogger.Fatal("服务器启动失败", zap.Error(err))
}
}()
appLogger.Info("服务器已启动", zap.String("address", cfg.Server.Address))
// 等待关闭信号
<-quit
appLogger.Info("正在关闭服务器...")
// 取消配置监听器
cancelWatch()
// 关闭 HTTP 服务器
if err := app.ShutdownWithTimeout(cfg.Server.ShutdownTimeout); err != nil {
appLogger.Error("强制关闭服务器", zap.Error(err))
}
appLogger.Info("服务器已停止")
}