package svc import ( "context" "time" "github.com/perfect-panel/server/internal/model/client" "github.com/perfect-panel/server/internal/model/node" "github.com/perfect-panel/server/internal/model/redemption" "github.com/perfect-panel/server/pkg/device" "github.com/perfect-panel/server/pkg/storage" "github.com/perfect-panel/server/internal/config" lotterydraw "github.com/perfect-panel/server/internal/logic/lottery/draw" lotteryhandler "github.com/perfect-panel/server/internal/logic/lottery/handler" lotteryhook "github.com/perfect-panel/server/internal/logic/lottery/hook" "github.com/perfect-panel/server/internal/model/ads" "github.com/perfect-panel/server/internal/model/announcement" "github.com/perfect-panel/server/internal/model/auth" "github.com/perfect-panel/server/internal/model/coupon" "github.com/perfect-panel/server/internal/model/document" iapapple "github.com/perfect-panel/server/internal/model/iap/apple" "github.com/perfect-panel/server/internal/model/log" logmessage "github.com/perfect-panel/server/internal/model/logmessage" "github.com/perfect-panel/server/internal/model/lottery" "github.com/perfect-panel/server/internal/model/order" "github.com/perfect-panel/server/internal/model/payment" "github.com/perfect-panel/server/internal/model/promo" "github.com/perfect-panel/server/internal/model/subscribe" "github.com/perfect-panel/server/internal/model/system" "github.com/perfect-panel/server/internal/model/ticket" "github.com/perfect-panel/server/internal/model/traffic" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/pkg/limit" "github.com/perfect-panel/server/pkg/nodeMultiplier" "github.com/perfect-panel/server/pkg/orm" "github.com/perfect-panel/server/pkg/signature" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" "github.com/hibiken/asynq" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) type ServiceContext struct { DB *gorm.DB Redis *redis.Client Config config.Config Queue *asynq.Client ExchangeRate float64 GeoIP *IPLocation SignatureValidator *signature.Validator S3Store *storage.S3Store //NodeCache *cache.NodeCacheClient AuthModel auth.Model AdsModel ads.Model LogModel log.Model LogMessageModel logmessage.Model NodeModel node.Model UserModel user.Model OrderModel order.Model ClientModel client.Model TicketModel ticket.Model //ServerModel server.Model SystemModel system.Model CouponModel coupon.Model RedemptionCodeModel redemption.RedemptionCodeModel RedemptionRecordModel redemption.RedemptionRecordModel PaymentModel payment.Model PromoModel promo.Model DocumentModel document.Model SubscribeModel subscribe.Model TrafficLogModel traffic.Model AnnouncementModel announcement.Model IAPAppleTransactionModel iapapple.Model // Lottery (Stage 1) LotteryChance lottery.ChanceService LotteryLedger lottery.LedgerService LotteryInviteHook lotteryhook.InviteHook LotteryRegistry lottery.Registry LotteryDrawService *lotterydraw.Service Restart func() error TelegramBot *tgbotapi.BotAPI NodeMultiplierManager *nodeMultiplier.Manager AuthLimiter *limit.PeriodLimit DeviceManager *device.DeviceManager } func NewServiceContext(c config.Config) *ServiceContext { // gorm initialize db, err := orm.ConnectMysql(orm.Mysql{ Config: c.MySQL, }) if err != nil { panic(err.Error()) } // IP location initialize geoIP, err := NewIPLocation("./cache/GeoLite2-City.mmdb") if err != nil { panic(err.Error()) } rds := redis.NewClient(&redis.Options{ Addr: c.Redis.Host, Password: c.Redis.Pass, DB: c.Redis.DB, PoolSize: c.Redis.PoolSize, // 连接池大小:根据应用并发量调整,建议 100-500 MinIdleConns: c.Redis.MinIdleConns, // 最小空闲连接:保持一定数量的空闲连接,减少建立连接的开销 MaxRetries: c.Redis.MaxRetries, // 最大重试次数:网络抖动时自动重试 PoolTimeout: time.Second * time.Duration(c.Redis.PoolTimeout), // 从连接池获取连接的超时时间 ConnMaxIdleTime: time.Second * time.Duration(c.Redis.IdleTimeout), // 空闲连接的超时时间,自动回收长时间空闲的连接 ConnMaxLifetime: time.Second * time.Duration(c.Redis.MaxConnAge), // 连接的最大生命周期,定期重建连接避免长时间使用的问题 DialTimeout: time.Second * time.Duration(c.Redis.DialTimeout), // 建立新连接的超时时间 ReadTimeout: time.Second * time.Duration(c.Redis.ReadTimeout), // 读操作超时时间 WriteTimeout: time.Second * time.Duration(c.Redis.WriteTimeout), // 写操作超时时间 }) err = rds.Ping(context.Background()).Err() if err != nil { panic(err.Error()) } authLimiter := limit.NewPeriodLimit(86400, 15, rds, config.SendCountLimitKeyPrefix, limit.Align()) nonceStore := signature.NewRedisNonceStore(rds) srv := &ServiceContext{ DB: db, Redis: rds, Config: c, Queue: NewAsynqClient(c), ExchangeRate: 0, GeoIP: geoIP, SignatureValidator: signature.NewValidator(c.AppSignature, nonceStore), //NodeCache: cache.NewNodeCacheClient(rds), AuthLimiter: authLimiter, AdsModel: ads.NewModel(db, rds), LogModel: log.NewModel(db), LogMessageModel: logmessage.NewModel(db), NodeModel: node.NewModel(db, rds), AuthModel: auth.NewModel(db, rds), UserModel: user.NewModel(db, rds), OrderModel: order.NewModel(db, rds), ClientModel: client.NewSubscribeApplicationModel(db), TicketModel: ticket.NewModel(db, rds), //ServerModel: server.NewModel(db, rds), SystemModel: system.NewModel(db, rds), CouponModel: coupon.NewModel(db, rds), RedemptionCodeModel: redemption.NewRedemptionCodeModel(db, rds), RedemptionRecordModel: redemption.NewRedemptionRecordModel(db, rds), PaymentModel: payment.NewModel(db, rds), PromoModel: promo.NewModel(db, rds), DocumentModel: document.NewModel(db, rds), SubscribeModel: subscribe.NewModel(db, rds), TrafficLogModel: traffic.NewModel(db), AnnouncementModel: announcement.NewModel(db, rds), } srv.IAPAppleTransactionModel = iapapple.NewModel(db, rds) srv.DeviceManager = NewDeviceManager(srv) // Lottery Stage 1: wire the always-safe pieces (ChanceService, LedgerService, // InviteHook). PrizeHandler registry is created in the draw service (PR C). srv.LotteryChance = lottery.NewChanceService(db) srv.LotteryLedger = lottery.NewLedgerService() srv.LotteryInviteHook = lotteryhook.NewInviteHook(db, srv.LotteryChance) // PR C: PrizeHandler registry + Draw service wiring. Handlers are hooked in // order: noop → vpn_duration → commission. Later registrations override. registry := lottery.NewRegistry() registry.Register(lottery.NewNoopHandler()) registry.Register(lotteryhandler.NewVPNDurationHandler(lotteryhandler.VPNDurationDeps{ UserModel: srv.UserModel, Ledger: srv.LotteryLedger, DB: db, ResolveEffectiveUser: lotteryhandler.DefaultResolveEffectiveUser(db), })) registry.Register(lotteryhandler.NewCommissionHandler(lotteryhandler.CommissionDeps{ Ledger: srv.LotteryLedger, UpdateCommission: srv.UserModel.UpdateCommission, WriteCommissionLog: lotteryhandler.WriteCommissionLog, })) // Stage 2 人工奖 handler:无外部依赖,直接注册。抽奖服务在 IsAuto()=false // 时不会调用 Dispatch,而是由 draw 事务内挂 lottery_claim (pending_claim), // 交给运营在后台审核 + 线下打款/发货。 registry.Register(lotteryhandler.NewCryptoHandler()) registry.Register(lotteryhandler.NewPhysicalHandler()) registry.Register(lotteryhandler.NewManualOtherHandler()) srv.LotteryRegistry = registry drawLimiter := limit.NewPeriodLimit(1, 1, rds, "lottery:draw:rate:") srv.LotteryDrawService = lotterydraw.NewService(lotterydraw.Deps{ DB: db, Enabled: c.Lottery.Enable, RateLimiter: lotterydraw.NewRedisRateLimiter(drawLimiter), Chance: srv.LotteryChance, Evaluator: lottery.NewRuleEvaluator(), Picker: lottery.NewWeightedPicker(0), Registry: registry, // ContextBuilder 留 nil:PR C 阶段无活动配置门槛不评估用户上下文;后续 // 可接一个真实的 RuleContextBuilder(读订阅/邀请/充值/tags)。 }) if c.S3.Enable { s3Store, err := storage.NewS3Store(context.Background(), c.S3) if err != nil { panic(err.Error()) } srv.S3Store = s3Store } return srv }