a46fb83054
Closes HIF-3 (阶段 PR B) PR B:handler 真实业务对接 + 邀请钩子(迭代含 R1 修复) - 迁移 02157_lottery_grant_ledger:external_ref UNIQUE 作为发奖幂等键 - log.CommissionTypeLottery=339(架构师批准的新常量) - DispatchRequest.IdempotencyKey(架构师 review 建议第 2 条) - GrantLedger + LedgerService.Reserve:INSERT ON CONFLICT DO NOTHING 幂等 upsert - VPNDurationHandler:ResolveEffectiveUser 归位家庭 owner + UpdateSubscribe,ExpireTime 三分支对齐 grantGiftDays - CommissionHandler:UpdateCommission + WriteCommissionLog(339),发给中奖者本人,不做家庭组归位 - Handler 从 model 层迁到 logic 层(避免 model → logic 反向依赖);noop 保留在 model 层 - InviteHook:fire-and-forget 独立 goroutine + 10s timeout,扫 running 活动的 invite_success 源 - ServiceContext 新增 LotteryChance / LotteryLedger / LotteryInviteHook - activateOrderLogic.handleCommission 两条分支通过 invokeInviteHookIfEligible 助手触发,助手内统一 gate IsNew(架构师 R1 打回后的修复) 架构师 R1 打回:branch B 未按"首次付款激活"gate,续费也会给 referer 发抽奖机会 → 已通过助手函数集中收拢,避免 branch A/B 判断漂移。 测试覆盖率:model 76.5% / handler 73.2% / hook 91.7%;补 4 个回归测试覆盖 IsNew 门槛(含关键的 DoesNotFireOnRenewal)。 线上仍零可见变更:无对外路由,钩子仅在活动 status=running 时生效,Stage 1 全流程无活动记录时 loadRunningActivities 返回空。
173 lines
6.8 KiB
Go
173 lines
6.8 KiB
Go
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"
|
|
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
|
|
|
|
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)
|
|
if c.S3.Enable {
|
|
s3Store, err := storage.NewS3Store(context.Background(), c.S3)
|
|
if err != nil {
|
|
panic(err.Error())
|
|
}
|
|
srv.S3Store = s3Store
|
|
}
|
|
return srv
|
|
|
|
}
|