package main import ( "context" "fmt" "net/http" "bindbox-game/configs" "bindbox-game/internal/pkg/env" "bindbox-game/internal/pkg/logger" "bindbox-game/internal/pkg/redis" "bindbox-game/internal/pkg/shutdown" "bindbox-game/internal/pkg/timeutil" "bindbox-game/internal/repository/mysql" "bindbox-game/internal/repository/mysql/dao" "bindbox-game/internal/router" activitysvc "bindbox-game/internal/service/activity" douyinsvc "bindbox-game/internal/service/douyin" syscfgsvc "bindbox-game/internal/service/sysconfig" usersvc "bindbox-game/internal/service/user" "flag" "go.uber.org/zap" ) // @title mini-chat 接口文档 // @version v0.0.1 // @securityDefinitions.apikey LoginVerifyToken // @in header // @name Authorization // @BasePath / func main() { flag.Parse() // 初始化 MySQL dbRepo, err := mysql.New() if err != nil { panic(err) } // 修复缺失的列以避免创建活动时报错 { db := dbRepo.GetDbW() var cnt int64 _ = db.Raw( "SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = ? AND table_name = 'activities' AND column_name = 'image'", configs.Get().MySQL.Write.Name, ).Scan(&cnt).Error if cnt == 0 { _ = db.Exec("ALTER TABLE `activities` ADD COLUMN `image` VARCHAR(512) NULL COMMENT '活动主图URL' AFTER `banner`").Error } cnt = 0 _ = db.Raw( "SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = ? AND table_name = 'activities' AND column_name = 'gameplay_intro'", configs.Get().MySQL.Write.Name, ).Scan(&cnt).Error if cnt == 0 { _ = db.Exec("ALTER TABLE `activities` ADD COLUMN `gameplay_intro` TEXT NULL COMMENT '玩法介绍' AFTER `image`").Error } cnt = 0 _ = db.Raw( "SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = ? AND table_name = 'activities' AND column_name = 'allow_item_cards'", configs.Get().MySQL.Write.Name, ).Scan(&cnt).Error if cnt == 0 { _ = db.Exec("ALTER TABLE `activities` ADD COLUMN `allow_item_cards` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许使用道具卡' AFTER `is_boss`").Error } cnt = 0 _ = db.Raw( "SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = ? AND table_name = 'activities' AND column_name = 'allow_coupons'", configs.Get().MySQL.Write.Name, ).Scan(&cnt).Error if cnt == 0 { _ = db.Exec("ALTER TABLE `activities` ADD COLUMN `allow_coupons` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许使用优惠券' AFTER `allow_item_cards`").Error } // 检查并创建 channels 表 if !db.Migrator().HasTable("channels") { _ = db.Exec(`CREATE TABLE IF NOT EXISTS channels ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', name varchar(255) NOT NULL COMMENT '渠道名称', code varchar(255) NOT NULL COMMENT '渠道唯一标识', type varchar(50) NOT NULL DEFAULT 'other' COMMENT '渠道类型', remarks varchar(512) DEFAULT NULL COMMENT '备注', created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', updated_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '更新时间', deleted_at datetime(3) DEFAULT NULL COMMENT '删除时间', PRIMARY KEY (id), UNIQUE KEY uk_code (code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推广渠道表';`).Error } // users 表新增 channel_id 字段 cnt = 0 _ = db.Raw( "SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = ? AND table_name = 'users' AND column_name = 'channel_id'", configs.Get().MySQL.Write.Name, ).Scan(&cnt).Error if cnt == 0 { _ = db.Exec("ALTER TABLE users ADD COLUMN channel_id bigint(20) DEFAULT 0 COMMENT '渠道ID' AFTER douyin_id").Error } // shipping_records 表新增 batch_no 字段(批量发货分组用) cnt = 0 _ = db.Raw( "SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = ? AND table_name = 'shipping_records' AND column_name = 'batch_no'", configs.Get().MySQL.Write.Name, ).Scan(&cnt).Error if cnt == 0 { _ = db.Exec("ALTER TABLE shipping_records ADD COLUMN batch_no VARCHAR(64) NULL COMMENT '批次号(批量发货时用于聚合)' AFTER express_no").Error _ = db.Exec("CREATE INDEX idx_shipping_records_batch_no ON shipping_records(batch_no)").Error } // 抖店订单表 if !db.Migrator().HasTable("douyin_orders") { _ = db.Exec(`CREATE TABLE IF NOT EXISTS douyin_orders ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, shop_order_id VARCHAR(64) NOT NULL COMMENT '抖店订单号', order_status INT NOT NULL DEFAULT 0 COMMENT '订单状态: 5=已完成', douyin_user_id VARCHAR(256) NOT NULL COMMENT '抖店用户ID', local_user_id BIGINT DEFAULT 0 COMMENT '匹配到的本地用户ID', actual_receive_amount BIGINT DEFAULT 0 COMMENT '实收金额(分)', pay_type_desc VARCHAR(64) DEFAULT '' COMMENT '支付方式描述', remark VARCHAR(512) DEFAULT '' COMMENT '备注', user_nickname VARCHAR(128) DEFAULT '' COMMENT '抖音昵称', raw_data JSON COMMENT '原始响应数据', created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3), updated_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), UNIQUE KEY uk_shop_order_id (shop_order_id), INDEX idx_local_user_id (local_user_id), INDEX idx_douyin_user_id (douyin_user_id), INDEX idx_order_status (order_status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抖店订单表';`).Error } else { // 检查并修改 douyin_user_id 列长度 _ = db.Exec("ALTER TABLE douyin_orders MODIFY COLUMN douyin_user_id VARCHAR(256) NOT NULL COMMENT '抖店用户ID'").Error } // 抽奖退款日志表 if !db.Migrator().HasTable("lottery_refund_logs") { _ = db.Exec(`CREATE TABLE IF NOT EXISTS lottery_refund_logs ( id bigint unsigned AUTO_INCREMENT PRIMARY KEY, issue_id bigint NOT NULL DEFAULT 0, order_id bigint NOT NULL DEFAULT 0, user_id bigint NOT NULL DEFAULT 0, amount bigint NOT NULL DEFAULT 0, coupon_type varchar(64) DEFAULT '', coupon_amount bigint DEFAULT 0, reason varchar(255) DEFAULT '', status varchar(32) DEFAULT '', created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_issue (issue_id), INDEX idx_order (order_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抽奖退款记录表';`).Error } } // 初始化 自定义 Logger customLogger, err := logger.NewCustomLogger(dao.Use(dbRepo.GetDbW()), logger.WithDebugLevel(), // 启用调试级别日志 logger.WithOutputInConsole(), // 启用控制台输出 logger.WithField("domain", fmt.Sprintf("%s[%s]", configs.ProjectName, env.Active().Value())), logger.WithTimeLayout(timeutil.CSTLayout), logger.WithFileRotationP(configs.ProjectAccessLogFile), ) if err != nil { panic(err) } defer func() { _ = customLogger.Sync() }() // 初始化 Redis if err := redis.Init(context.Background(), customLogger); err != nil { customLogger.Warn("Redis init failed, some features may be disabled", zap.Error(err)) } // 初始化 HTTP 服务 mux, cleanup, err := router.NewHTTPMux(customLogger, dbRepo) if err != nil { panic(err) } server := &http.Server{ Addr: configs.ProjectPort, Handler: mux, } go func() { if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { customLogger.Fatal("http server startup err", zap.Error(err)) } }() activitysvc.StartScheduledSettlement(customLogger, dbRepo, redis.GetClient()) usersvc.StartExpirationCheck(customLogger, dbRepo) // 启动抖店订单同步定时任务 syscfgSvc := syscfgsvc.New(customLogger, dbRepo) douyinsvc.StartDouyinOrderSync(customLogger, dbRepo, syscfgSvc) // 初始化全局动态配置服务 if err := syscfgsvc.InitGlobalDynamicConfig(customLogger, dbRepo); err != nil { customLogger.Warn("动态配置加载失败,将使用静态配置", zap.Error(err)) } // 优雅关闭 shutdown.Close( func() { // 清理资源 (Worker) if cleanup != nil { cleanup() } // 关闭 http server if err := server.Shutdown(context.TODO()); err != nil { customLogger.Error("server shutdown err", zap.Error(err)) } // 关闭 db master (支持读写) if err := dbRepo.DbWClose(); err != nil { customLogger.Error("dbw close err", zap.Error(err)) } // 关闭 db slave (仅支持读) if err := dbRepo.DbRClose(); err != nil { customLogger.Error("dbr close err", zap.Error(err)) } }, ) }