ce3babcc33
Closes HIF-3 Stage 1 完整闭环 PR C:用户 API + 后台 CRUD + 抽奖事务服务 + 审计 + QA curl。合并后 Stage 1 可交测试。 架构师 review R1(rulecaps depth≤8/nodes≤64/bytes≤8KB)+ R2(InviteHook source_ref 加 order: 前缀)已全部落地。 - 迁移 02158_admin_action_log:后台写操作审计 - xerr 100xxx 段:抽奖错误码(NotEligible/NoChances/ActivityEnded/RateLimited/NotClaimable/InternalError/RuleTooDeep/RuleTooMany/RuleTooLarge) - feature flag config.Lottery.Enable 默认 false,合并后线上零副作用 - draw service:feature flag → rate limit → pre-tx reads → nonce dedupe → Consume → Pick → 乐观扣库存 → 双快照 → Dispatch → finalize - 用户 API 4 个:GET /config、POST /draw、GET /records、POST /claim(Stage 1 返回 100010) - 后台 CRUD:活动 / 奖品 / rules PUT(rulecaps gate)/ chances/grant - audit.WriteAdminAction:与调用方 tx 同生共死,SHA1 body 摘要 - QA 脚本:qa/lottery/stage1_curl.sh 全链路 curl 测试覆盖:model 76.5% / draw 69% / handler 68.3% / hook 91.9% / rulecaps 87.8% / audit 100% 100 并发抢库存 + 10000 次概率分布 e2e 推 QA 环境(sqlmock 无法忠实模拟 InnoDB 行锁) CI 全绿:构建/Vet/测试 + golangci-lint
207 lines
8.4 KiB
Go
207 lines
8.4 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"
|
||
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,
|
||
}))
|
||
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
|
||
|
||
}
|