package main import ( "fmt" "log" "github.com/gin-gonic/gin" "gorm.io/driver/mysql" "gorm.io/gorm" "gorm.io/gorm/logger" "github.com/wangjia/jiu/backend/config" "github.com/wangjia/jiu/backend/internal/model" "github.com/wangjia/jiu/backend/internal/router" "github.com/wangjia/jiu/backend/internal/service" ) func main() { // 加载配置 config.Load() // 生产环境启动前置检查 if config.C.Server.Mode == "release" { if config.C.Server.CORSOrigin == "*" { log.Fatal("server.cors_origin must not be '*' in production — set it to the actual frontend origin") } } // 初始化数据库 db := initDB() // ALTER 已有数据库的 enum,使 superadmin 在已运行实例上也生效(忽略错误) db.Exec("ALTER TABLE users MODIFY COLUMN role ENUM('admin','operator','readonly','superadmin') NOT NULL DEFAULT 'operator'") // 自动迁移(GORM AutoMigrate 只增不删,生产安全) autoMigrate(db) // 启动会话/失败登录保留期清理任务(后台 goroutine) service.StartSessionCleanup(db, config.C.Session.RetentionDays) // 启动 Gin gin.SetMode(config.C.Server.Mode) r := gin.New() r.Use(gin.Logger(), gin.Recovery()) // 真实客户端 IP:只信任本机 nginx(127.0.0.1/::1)写的 X-Real-IP,丢弃客户端伪造的 // X-Forwarded-For。这样 c.ClientIP() 返回不可伪造的真实 IP,是所有按 IP 限流/审计的基础。 // 代理链:client → nginx(127.0.0.1:8445) → 后端(127.0.0.1:8080);将来若在 nginx 前再加 // 一层代理,需把其地址并入下列可信网段。 _ = r.SetTrustedProxies([]string{"127.0.0.1", "::1"}) r.RemoteIPHeaders = []string{"X-Real-IP"} // CORS corsOrigin := config.C.Server.CORSOrigin r.Use(func(c *gin.Context) { c.Header("Access-Control-Allow-Origin", corsOrigin) c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") c.Header("Access-Control-Allow-Headers", "Authorization,Content-Type") if c.Request.Method == "OPTIONS" { c.AbortWithStatus(204) return } c.Next() }) // Serve uploaded images (in production, Nginx handles /images/) r.Static("/images", config.C.Storage.UploadDir) router.Setup(r, db) addr := fmt.Sprintf(":%s", config.C.Server.Port) log.Printf("Server starting on %s (mode: %s)", addr, config.C.Server.Mode) if err := r.Run(addr); err != nil { log.Fatalf("Server failed: %v", err) } } func initDB() *gorm.DB { dsn := config.C.Database.DSN if dsn == "" { log.Fatal("database.dsn is required in config") } logLevel := logger.Silent if config.C.Server.Mode == "debug" { logLevel = logger.Info } db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ Logger: logger.Default.LogMode(logLevel), TranslateError: true, // 把 MySQL 1062 翻译成 gorm.ErrDuplicatedKey,供编码撞唯一约束时重试 }) if err != nil { log.Fatalf("failed to connect database: %v", err) } sqlDB, _ := db.DB() sqlDB.SetMaxIdleConns(config.C.Database.MaxIdleConns) sqlDB.SetMaxOpenConns(config.C.Database.MaxOpenConns) return db } func autoMigrate(db *gorm.DB) { err := db.AutoMigrate( &model.Shop{}, &model.User{}, &model.License{}, &model.LicenseDevice{}, &model.LicenseCode{}, &model.UserSession{}, &model.LoginAttempt{}, &model.ProductCategory{}, &model.Product{}, &model.Warehouse{}, &model.Partner{}, &model.StockInOrder{}, &model.StockInItem{}, &model.StockOutOrder{}, &model.StockOutItem{}, &model.Inventory{}, &model.InventoryLog{}, &model.InventoryCheck{}, &model.InventoryCheckItem{}, &model.FinanceRecord{}, &model.NumberRule{}, &model.ProductNameOption{}, &model.ProductSeriesOption{}, &model.ProductSpecOption{}, &model.ProductOriginOption{}, &model.ProductShelfLifeOption{}, &model.ProductStorageOption{}, &model.ProductDescriptionDoc{}, &model.ProductImage{}, &model.ErrorReport{}, &model.Feedback{}, ) if err != nil { log.Fatalf("auto migrate failed: %v", err) } // products(shop_id, code) 联合唯一索引:ShopID 在共用 TenantBase 上无法用 struct tag 表达, // 故在此幂等显式建(保证同店内商品编码唯一,DB 层兜底防止重复编码静默落库)。 if !db.Migrator().HasIndex(&model.Product{}, "uk_shop_code") { if err := db.Exec("CREATE UNIQUE INDEX uk_shop_code ON products (shop_id, code)").Error; err != nil { log.Fatalf("create unique index uk_shop_code failed: %v", err) } } // stock_out_items(shop_id, product_code) 复合索引:支撑出库列表「按商品编码反查单据」, // 避免在 2.6 万+ 明细行上全表扫。ShopID 在嵌入字段上无法用 struct tag 表达,故显式幂等建。 if !db.Migrator().HasIndex(&model.StockOutItem{}, "idx_so_items_shop_code") { if err := db.Exec("CREATE INDEX idx_so_items_shop_code ON stock_out_items (shop_id, product_code)").Error; err != nil { log.Fatalf("create index idx_so_items_shop_code failed: %v", err) } } log.Println("AutoMigrate completed") }