629 lines
21 KiB
Go
629 lines
21 KiB
Go
package app
|
||
|
||
import (
|
||
"context"
|
||
"log/slog"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"google.golang.org/protobuf/proto"
|
||
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
||
"hyapp/pkg/appcode"
|
||
"hyapp/pkg/gamemq"
|
||
"hyapp/pkg/grpchealth"
|
||
"hyapp/pkg/healthhttp"
|
||
"hyapp/pkg/logx"
|
||
"hyapp/pkg/rocketmqx"
|
||
"hyapp/pkg/roommq"
|
||
"hyapp/pkg/usermq"
|
||
"hyapp/pkg/walletmq"
|
||
"hyapp/services/statistics-service/internal/config"
|
||
mysqlstorage "hyapp/services/statistics-service/internal/storage/mysql"
|
||
)
|
||
|
||
// App wires statistics MQ consumers to the aggregate repository.
|
||
type App struct {
|
||
cfg config.Config
|
||
repo *mysqlstorage.Repository
|
||
health *grpchealth.ServingChecker
|
||
healthHTTP *healthhttp.Server
|
||
queryHTTP *queryHTTPServer
|
||
consumers []*rocketmqx.Consumer
|
||
closeOnce sync.Once
|
||
}
|
||
|
||
func New(cfg config.Config) (*App, error) {
|
||
startupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||
defer cancel()
|
||
repo, err := mysqlstorage.Open(startupCtx, cfg.MySQLDSN, cfg.ActivityMySQLDSN)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
consumers, err := newConsumers(cfg, repo)
|
||
if err != nil {
|
||
_ = repo.Close()
|
||
return nil, err
|
||
}
|
||
health := grpchealth.NewServingChecker("statistics-service", grpchealth.Dependency{Name: "mysql", Check: repo.Ping})
|
||
healthHTTP, err := healthhttp.New(cfg.HealthHTTPAddr, cfg.NodeID, health)
|
||
if err != nil {
|
||
shutdownConsumers(consumers)
|
||
_ = repo.Close()
|
||
return nil, err
|
||
}
|
||
queryHTTP, err := newQueryHTTPServer(cfg.QueryHTTPAddr, repo)
|
||
if err != nil {
|
||
shutdownConsumers(consumers)
|
||
_ = healthHTTP.Close(context.Background())
|
||
_ = repo.Close()
|
||
return nil, err
|
||
}
|
||
return &App{cfg: cfg, repo: repo, health: health, healthHTTP: healthHTTP, queryHTTP: queryHTTP, consumers: consumers}, nil
|
||
}
|
||
|
||
func (a *App) Run() error {
|
||
a.runHealthHTTP()
|
||
a.runQueryHTTP()
|
||
if err := startConsumers(a.consumers); err != nil {
|
||
a.health.MarkStopped()
|
||
return err
|
||
}
|
||
a.health.MarkServing()
|
||
select {}
|
||
}
|
||
|
||
func (a *App) runQueryHTTP() {
|
||
if a.queryHTTP == nil {
|
||
return
|
||
}
|
||
go func() {
|
||
if err := a.queryHTTP.Run(); err != nil {
|
||
logx.Error(context.Background(), "query_http_run_failed", err)
|
||
}
|
||
}()
|
||
}
|
||
|
||
func (a *App) Close() {
|
||
a.closeOnce.Do(func() {
|
||
a.health.MarkDraining()
|
||
shutdownConsumers(a.consumers)
|
||
a.closeHealthHTTP()
|
||
if a.repo != nil {
|
||
_ = a.repo.Close()
|
||
}
|
||
})
|
||
}
|
||
|
||
func (a *App) runHealthHTTP() {
|
||
if a.healthHTTP == nil {
|
||
return
|
||
}
|
||
go func() {
|
||
if err := a.healthHTTP.Run(); err != nil {
|
||
logx.Error(context.Background(), "health_http_run_failed", err)
|
||
}
|
||
}()
|
||
}
|
||
|
||
func (a *App) closeHealthHTTP() {
|
||
if a.healthHTTP == nil {
|
||
return
|
||
}
|
||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||
defer cancel()
|
||
_ = a.healthHTTP.Close(ctx)
|
||
if a.queryHTTP != nil {
|
||
_ = a.queryHTTP.Close(ctx)
|
||
}
|
||
}
|
||
|
||
func newConsumers(cfg config.Config, repo *mysqlstorage.Repository) ([]*rocketmqx.Consumer, error) {
|
||
consumers := make([]*rocketmqx.Consumer, 0, 4)
|
||
add := func(mq config.ConsumerMQConfig, tag string, handler rocketmqx.Handler) error {
|
||
if !mq.Enabled {
|
||
return nil
|
||
}
|
||
consumer, err := rocketmqx.NewConsumer(consumerConfig(cfg.RocketMQ, mq))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err := consumer.Subscribe(mq.Topic, tag, handler); err != nil {
|
||
_ = consumer.Shutdown()
|
||
return err
|
||
}
|
||
consumers = append(consumers, consumer)
|
||
return nil
|
||
}
|
||
if err := add(cfg.RocketMQ.UserOutbox, usermq.TagUserOutboxEvent, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
|
||
event, ok, err := userRegisteredEvent(message.Body)
|
||
if err != nil || !ok {
|
||
return err
|
||
}
|
||
return repo.ConsumeUserRegistered(appcode.WithContext(ctx, event.AppCode), event)
|
||
}); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := add(cfg.RocketMQ.WalletOutbox, walletmq.TagWalletOutboxEvent, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
|
||
event, ok, err := rechargeEvent(message.Body)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if ok {
|
||
return repo.ConsumeRecharge(appcode.WithContext(ctx, event.AppCode), event)
|
||
}
|
||
stock, ok, err := coinSellerStockEvent(message.Body)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if ok {
|
||
return repo.ConsumeCoinSellerStock(appcode.WithContext(ctx, stock.AppCode), stock)
|
||
}
|
||
reward, ok, err := luckyGiftRewardEvent(message.Body)
|
||
if err != nil || !ok {
|
||
return err
|
||
}
|
||
return repo.ConsumeLuckyGiftReward(appcode.WithContext(ctx, reward.AppCode), reward)
|
||
}); err != nil {
|
||
shutdownConsumers(consumers)
|
||
return nil, err
|
||
}
|
||
if err := add(cfg.RocketMQ.RoomOutbox, roommq.TagRoomOutboxEvent, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
|
||
if gift, ok, err := roomGiftEvent(message.Body); err != nil || ok {
|
||
if err != nil || !ok {
|
||
return err
|
||
}
|
||
if err := repo.ConsumeRoomGift(appcode.WithContext(ctx, gift.AppCode), gift); err != nil {
|
||
return err
|
||
}
|
||
return repo.ConsumeUserActive(appcode.WithContext(ctx, gift.AppCode), mysqlstorage.UserActiveEvent{
|
||
AppCode: gift.AppCode,
|
||
EventID: gift.EventID + ":active",
|
||
EventType: "RoomGiftSentActive",
|
||
UserID: gift.SenderUserID,
|
||
CountryID: gift.CountryID,
|
||
RegionID: gift.VisibleRegionID,
|
||
OccurredAtMS: gift.OccurredAtMS,
|
||
})
|
||
}
|
||
active, ok, err := roomJoinActiveEvent(message.Body)
|
||
if err != nil || !ok {
|
||
return err
|
||
}
|
||
return repo.ConsumeUserActive(appcode.WithContext(ctx, active.AppCode), active)
|
||
}); err != nil {
|
||
shutdownConsumers(consumers)
|
||
return nil, err
|
||
}
|
||
if err := add(cfg.RocketMQ.GameOutbox, gamemq.TagGameOutboxEvent, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
|
||
if event, ok, err := gameOrderEvent(message.Body); err != nil || ok {
|
||
if err != nil || !ok {
|
||
return err
|
||
}
|
||
return repo.ConsumeGameOrder(appcode.WithContext(ctx, event.AppCode), event)
|
||
}
|
||
match, ok, err := selfGameMatchEvent(message.Body)
|
||
if err != nil || !ok {
|
||
pool, poolOK, poolErr := selfGamePoolAdjustmentEvent(message.Body)
|
||
if poolErr != nil || !poolOK {
|
||
return poolErr
|
||
}
|
||
return repo.ConsumeSelfGamePoolAdjustment(appcode.WithContext(ctx, pool.AppCode), pool)
|
||
}
|
||
return repo.ConsumeSelfGameMatch(appcode.WithContext(ctx, match.AppCode), match)
|
||
}); err != nil {
|
||
shutdownConsumers(consumers)
|
||
return nil, err
|
||
}
|
||
return consumers, nil
|
||
}
|
||
|
||
func userRegisteredEvent(body []byte) (mysqlstorage.UserRegisteredEvent, bool, error) {
|
||
message, err := usermq.DecodeUserOutboxMessage(body)
|
||
if err != nil {
|
||
return mysqlstorage.UserRegisteredEvent{}, false, err
|
||
}
|
||
if message.EventType != "UserRegistered" {
|
||
return mysqlstorage.UserRegisteredEvent{}, false, nil
|
||
}
|
||
payload := mysqlstorage.DecodeJSON(message.PayloadJSON)
|
||
return mysqlstorage.UserRegisteredEvent{
|
||
AppCode: message.AppCode,
|
||
EventID: message.EventID,
|
||
UserID: message.AggregateID,
|
||
CountryID: firstNonZeroInt64(mysqlstorage.Int64(payload, "country_id"), mysqlstorage.Int64(payload, "target_country_id")),
|
||
RegionID: mysqlstorage.Int64(payload, "region_id"),
|
||
OccurredAtMS: message.OccurredAtMS,
|
||
}, true, nil
|
||
}
|
||
|
||
func rechargeEvent(body []byte) (mysqlstorage.RechargeEvent, bool, error) {
|
||
message, err := walletmq.DecodeWalletOutboxMessage(body)
|
||
if err != nil {
|
||
return mysqlstorage.RechargeEvent{}, false, err
|
||
}
|
||
if message.EventType != "WalletRechargeRecorded" {
|
||
return mysqlstorage.RechargeEvent{}, false, nil
|
||
}
|
||
payload := mysqlstorage.DecodeJSON(message.PayloadJSON)
|
||
rechargeType := firstNonEmpty(mysqlstorage.String(payload, "recharge_type"), mysqlstorage.String(payload, "channel"), mysqlstorage.String(payload, "provider"), "coin_seller_transfer")
|
||
usdMinor := rechargeUSDMinorFromPayload(payload, rechargeType)
|
||
coinAmount := int64(0)
|
||
if isCoinSellerRechargeType(rechargeType) {
|
||
usdMinor = 0
|
||
coinAmount = firstNonZeroInt64(mysqlstorage.Int64(payload, "amount"), mysqlstorage.Int64(payload, "coin_amount"), mysqlstorage.Int64(payload, "available_delta"))
|
||
}
|
||
regionID := firstNonZeroInt64(mysqlstorage.Int64(payload, "target_region_id"), mysqlstorage.Int64(payload, "region_id"))
|
||
return mysqlstorage.RechargeEvent{
|
||
AppCode: message.AppCode,
|
||
EventID: message.EventID,
|
||
EventType: message.EventType,
|
||
UserID: message.UserID,
|
||
USDMinor: usdMinor,
|
||
CoinAmount: coinAmount,
|
||
RechargeSequence: mysqlstorage.Int64(payload, "recharge_sequence"),
|
||
TargetCountryID: firstNonZeroInt64(mysqlstorage.Int64(payload, "target_country_id"), mysqlstorage.Int64(payload, "country_id")),
|
||
TargetRegionID: regionID,
|
||
RechargeType: rechargeType,
|
||
OccurredAtMS: message.OccurredAtMS,
|
||
}, true, nil
|
||
}
|
||
|
||
func coinSellerStockEvent(body []byte) (mysqlstorage.CoinSellerStockEvent, bool, error) {
|
||
message, err := walletmq.DecodeWalletOutboxMessage(body)
|
||
if err != nil {
|
||
return mysqlstorage.CoinSellerStockEvent{}, false, err
|
||
}
|
||
if message.EventType != "WalletCoinSellerStockPurchased" {
|
||
return mysqlstorage.CoinSellerStockEvent{}, false, nil
|
||
}
|
||
payload := mysqlstorage.DecodeJSON(message.PayloadJSON)
|
||
if !boolValue(payload, "counts_as_seller_recharge") {
|
||
return mysqlstorage.CoinSellerStockEvent{}, false, nil
|
||
}
|
||
usdMinor := amountMicroToUSDMinor(mysqlstorage.Int64(payload, "paid_amount_micro"))
|
||
if usdMinor <= 0 {
|
||
return mysqlstorage.CoinSellerStockEvent{}, false, nil
|
||
}
|
||
return mysqlstorage.CoinSellerStockEvent{
|
||
AppCode: message.AppCode,
|
||
EventID: message.EventID,
|
||
SellerUserID: firstNonZeroInt64(mysqlstorage.Int64(payload, "seller_user_id"), message.UserID),
|
||
USDMinor: usdMinor,
|
||
CoinAmount: firstNonZeroInt64(mysqlstorage.Int64(payload, "coin_amount"), mysqlstorage.Int64(payload, "available_delta")),
|
||
CountryID: firstNonZeroInt64(mysqlstorage.Int64(payload, "country_id"), mysqlstorage.Int64(payload, "seller_country_id")),
|
||
RegionID: firstNonZeroInt64(mysqlstorage.Int64(payload, "region_id"), mysqlstorage.Int64(payload, "seller_region_id")),
|
||
OccurredAtMS: message.OccurredAtMS,
|
||
}, true, nil
|
||
}
|
||
|
||
func rechargeUSDMinorFromPayload(payload map[string]any, rechargeType string) int64 {
|
||
// 币商向用户转账是普通金币到账事实,只用于充值用户和金币金额统计;即使历史 payload 携带 recharge_usd_minor,也不能进入用户 USDT/USD 充值。
|
||
if isCoinSellerRechargeType(rechargeType) {
|
||
return 0
|
||
}
|
||
if value := mysqlstorage.Int64(payload, "recharge_usd_minor"); value > 0 {
|
||
return value
|
||
}
|
||
// 旧 Google outbox 只有 amount_micro;这里按微单位折算成美分,避免 0.99 被统计成 9900。
|
||
if isGoogleRechargeType(rechargeType) {
|
||
return amountMicroToUSDMinor(mysqlstorage.Int64(payload, "amount_micro"))
|
||
}
|
||
return 0
|
||
}
|
||
|
||
func isGoogleRechargeType(value string) bool {
|
||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||
case "google", "google_play":
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func isCoinSellerRechargeType(value string) bool {
|
||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||
case "coin_seller_transfer", "coin_seller":
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func amountMicroToUSDMinor(amountMicro int64) int64 {
|
||
if amountMicro <= 0 {
|
||
return 0
|
||
}
|
||
return amountMicro / 10_000
|
||
}
|
||
|
||
func luckyGiftRewardEvent(body []byte) (mysqlstorage.LuckyGiftRewardEvent, bool, error) {
|
||
message, err := walletmq.DecodeWalletOutboxMessage(body)
|
||
if err != nil {
|
||
return mysqlstorage.LuckyGiftRewardEvent{}, false, err
|
||
}
|
||
if message.EventType != "WalletLuckyGiftRewardCredited" {
|
||
return mysqlstorage.LuckyGiftRewardEvent{}, false, nil
|
||
}
|
||
payload := mysqlstorage.DecodeJSON(message.PayloadJSON)
|
||
return mysqlstorage.LuckyGiftRewardEvent{
|
||
AppCode: message.AppCode,
|
||
EventID: message.EventID,
|
||
EventType: message.EventType,
|
||
UserID: firstNonZeroInt64(mysqlstorage.Int64(payload, "user_id"), message.UserID),
|
||
CountryID: firstNonZeroInt64(mysqlstorage.Int64(payload, "country_id"), mysqlstorage.Int64(payload, "target_country_id")),
|
||
RegionID: firstNonZeroInt64(mysqlstorage.Int64(payload, "visible_region_id"), mysqlstorage.Int64(payload, "region_id"), mysqlstorage.Int64(payload, "target_region_id")),
|
||
PoolID: mysqlstorage.String(payload, "pool_id"),
|
||
Amount: mysqlstorage.Int64(payload, "amount"),
|
||
OccurredAtMS: message.OccurredAtMS,
|
||
}, true, nil
|
||
}
|
||
|
||
func roomGiftEvent(body []byte) (mysqlstorage.RoomGiftEvent, bool, error) {
|
||
envelope, _, err := roommq.DecodeRoomOutboxMessage(body)
|
||
if err != nil {
|
||
return mysqlstorage.RoomGiftEvent{}, false, err
|
||
}
|
||
if envelope.GetEventType() != "RoomGiftSent" {
|
||
return mysqlstorage.RoomGiftEvent{}, false, nil
|
||
}
|
||
var gift roomeventsv1.RoomGiftSent
|
||
if err := proto.Unmarshal(envelope.GetBody(), &gift); err != nil {
|
||
return mysqlstorage.RoomGiftEvent{}, false, err
|
||
}
|
||
if gift.GetIsRobotGift() {
|
||
return mysqlstorage.RoomGiftEvent{}, false, nil
|
||
}
|
||
return mysqlstorage.RoomGiftEvent{
|
||
AppCode: envelope.GetAppCode(),
|
||
EventID: envelope.GetEventId(),
|
||
SenderUserID: gift.GetSenderUserId(),
|
||
CountryID: gift.GetCountryId(),
|
||
VisibleRegionID: firstNonZeroInt64(gift.GetRegionId(), gift.GetVisibleRegionId()),
|
||
GiftValue: gift.GetGiftValue(),
|
||
CoinSpent: roomGiftCoinSpent(&gift),
|
||
PoolID: gift.GetPoolId(),
|
||
OccurredAtMS: envelope.GetOccurredAtMs(),
|
||
}, true, nil
|
||
}
|
||
|
||
func roomGiftCoinSpent(gift *roomeventsv1.RoomGiftSent) int64 {
|
||
if gift == nil {
|
||
return 0
|
||
}
|
||
// Databi 的礼物消费和幸运礼物流水按金币展示;新版 RoomGiftSent 会直接携带 coin_spent,历史事件没有该字段时才回退旧的 gift_value 热度口径。
|
||
if gift.GetCoinSpent() > 0 {
|
||
return gift.GetCoinSpent()
|
||
}
|
||
return gift.GetGiftValue()
|
||
}
|
||
|
||
func roomJoinActiveEvent(body []byte) (mysqlstorage.UserActiveEvent, bool, error) {
|
||
envelope, _, err := roommq.DecodeRoomOutboxMessage(body)
|
||
if err != nil {
|
||
return mysqlstorage.UserActiveEvent{}, false, err
|
||
}
|
||
if envelope.GetEventType() != "RoomUserJoined" {
|
||
return mysqlstorage.UserActiveEvent{}, false, nil
|
||
}
|
||
var joined roomeventsv1.RoomUserJoined
|
||
if err := proto.Unmarshal(envelope.GetBody(), &joined); err != nil {
|
||
return mysqlstorage.UserActiveEvent{}, false, err
|
||
}
|
||
return mysqlstorage.UserActiveEvent{
|
||
AppCode: envelope.GetAppCode(),
|
||
EventID: envelope.GetEventId(),
|
||
EventType: envelope.GetEventType(),
|
||
UserID: joined.GetUserId(),
|
||
CountryID: joined.GetCountryId(),
|
||
RegionID: firstNonZeroInt64(joined.GetRegionId(), joined.GetVisibleRegionId()),
|
||
OccurredAtMS: envelope.GetOccurredAtMs(),
|
||
}, true, nil
|
||
}
|
||
|
||
func gameOrderEvent(body []byte) (mysqlstorage.GameOrderEvent, bool, error) {
|
||
message, err := gamemq.DecodeGameOutboxMessage(body)
|
||
if err != nil {
|
||
return mysqlstorage.GameOrderEvent{}, false, err
|
||
}
|
||
if message.EventType != "GameOrderSettled" {
|
||
return mysqlstorage.GameOrderEvent{}, false, nil
|
||
}
|
||
payload := mysqlstorage.DecodeJSON(message.PayloadJSON)
|
||
return mysqlstorage.GameOrderEvent{
|
||
AppCode: message.AppCode,
|
||
EventID: message.EventID,
|
||
UserID: message.UserID,
|
||
CountryID: firstNonZeroInt64(mysqlstorage.Int64(payload, "country_id"), mysqlstorage.Int64(payload, "target_country_id")),
|
||
RegionID: firstNonZeroInt64(mysqlstorage.Int64(payload, "visible_region_id"), mysqlstorage.Int64(payload, "region_id")),
|
||
PlatformCode: message.PlatformCode,
|
||
GameID: message.GameID,
|
||
OpType: message.OpType,
|
||
CoinAmount: message.CoinAmount,
|
||
OccurredAtMS: message.OccurredAtMS,
|
||
}, true, nil
|
||
}
|
||
|
||
func selfGameMatchEvent(body []byte) (mysqlstorage.SelfGameMatchEvent, bool, error) {
|
||
message, err := gamemq.DecodeGameOutboxMessage(body)
|
||
if err != nil {
|
||
return mysqlstorage.SelfGameMatchEvent{}, false, err
|
||
}
|
||
switch message.EventType {
|
||
case gamemq.EventTypeSelfGameMatchCreated,
|
||
gamemq.EventTypeSelfGameMatchReady,
|
||
gamemq.EventTypeSelfGameMatchSettled,
|
||
gamemq.EventTypeSelfGameMatchCanceled,
|
||
gamemq.EventTypeSelfGameMatchFailed:
|
||
default:
|
||
return mysqlstorage.SelfGameMatchEvent{}, false, nil
|
||
}
|
||
payload := mysqlstorage.DecodeJSON(message.PayloadJSON)
|
||
event := mysqlstorage.SelfGameMatchEvent{
|
||
AppCode: message.AppCode,
|
||
EventID: message.EventID,
|
||
EventType: message.EventType,
|
||
MatchID: firstNonEmpty(mysqlstorage.String(payload, "match_id"), message.OrderID),
|
||
GameID: firstNonEmpty(mysqlstorage.String(payload, "game_id"), message.GameID),
|
||
PlatformCode: firstNonEmpty(mysqlstorage.String(payload, "platform_code"), message.PlatformCode),
|
||
RoomID: mysqlstorage.String(payload, "room_id"),
|
||
RegionID: mysqlstorage.Int64(payload, "region_id"),
|
||
StakeCoin: mysqlstorage.Int64(payload, "stake_coin"),
|
||
Status: mysqlstorage.String(payload, "status"),
|
||
Result: mysqlstorage.String(payload, "result"),
|
||
MatchMode: mysqlstorage.String(payload, "match_mode"),
|
||
ForcedResult: mysqlstorage.String(payload, "forced_result"),
|
||
UserParticipants: mysqlstorage.Int64(payload, "user_participants"),
|
||
RobotParticipants: mysqlstorage.Int64(payload, "robot_participants"),
|
||
WinnerCount: mysqlstorage.Int64(payload, "winner_count"),
|
||
LoserCount: mysqlstorage.Int64(payload, "loser_count"),
|
||
DrawCount: mysqlstorage.Int64(payload, "draw_count"),
|
||
UserStakeCoin: mysqlstorage.Int64(payload, "user_stake_coin"),
|
||
RobotStakeCoin: mysqlstorage.Int64(payload, "robot_stake_coin"),
|
||
PayoutCoin: mysqlstorage.Int64(payload, "payout_coin"),
|
||
RefundCoin: mysqlstorage.Int64(payload, "refund_coin"),
|
||
PlatformProfitCoin: mysqlstorage.Int64(payload, "platform_profit_coin"),
|
||
PoolDeltaCoin: mysqlstorage.Int64(payload, "pool_delta_coin"),
|
||
WaitMS: mysqlstorage.Int64(payload, "wait_ms"),
|
||
DurationMS: mysqlstorage.Int64(payload, "duration_ms"),
|
||
Participants: selfGameParticipants(payload["participants"]),
|
||
OccurredAtMS: message.OccurredAtMS,
|
||
}
|
||
return event, true, nil
|
||
}
|
||
|
||
func selfGamePoolAdjustmentEvent(body []byte) (mysqlstorage.SelfGamePoolAdjustmentEvent, bool, error) {
|
||
message, err := gamemq.DecodeGameOutboxMessage(body)
|
||
if err != nil {
|
||
return mysqlstorage.SelfGamePoolAdjustmentEvent{}, false, err
|
||
}
|
||
if message.EventType != gamemq.EventTypeSelfGamePoolAdjusted {
|
||
return mysqlstorage.SelfGamePoolAdjustmentEvent{}, false, nil
|
||
}
|
||
payload := mysqlstorage.DecodeJSON(message.PayloadJSON)
|
||
return mysqlstorage.SelfGamePoolAdjustmentEvent{
|
||
AppCode: message.AppCode,
|
||
EventID: message.EventID,
|
||
GameID: firstNonEmpty(mysqlstorage.String(payload, "game_id"), message.GameID),
|
||
Direction: mysqlstorage.String(payload, "direction"),
|
||
Reason: mysqlstorage.String(payload, "reason"),
|
||
AmountCoin: mysqlstorage.Int64(payload, "amount_coin"),
|
||
BalanceAfter: mysqlstorage.Int64(payload, "balance_after"),
|
||
OccurredAtMS: message.OccurredAtMS,
|
||
}, true, nil
|
||
}
|
||
|
||
func selfGameParticipants(raw any) []mysqlstorage.SelfGameMatchParticipantEvent {
|
||
values, ok := raw.([]any)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
participants := make([]mysqlstorage.SelfGameMatchParticipantEvent, 0, len(values))
|
||
for _, value := range values {
|
||
payload, ok := value.(map[string]any)
|
||
if !ok {
|
||
continue
|
||
}
|
||
participants = append(participants, mysqlstorage.SelfGameMatchParticipantEvent{
|
||
UserID: mysqlstorage.Int64(payload, "user_id"),
|
||
ParticipantType: mysqlstorage.String(payload, "participant_type"),
|
||
StakeCoin: mysqlstorage.Int64(payload, "stake_coin"),
|
||
DicePoints: int64Slice(payload["dice_points"]),
|
||
RPSGesture: mysqlstorage.String(payload, "rps_gesture"),
|
||
Result: mysqlstorage.String(payload, "result"),
|
||
PayoutCoin: mysqlstorage.Int64(payload, "payout_coin"),
|
||
NetWinCoin: mysqlstorage.Int64(payload, "net_win_coin"),
|
||
})
|
||
}
|
||
return participants
|
||
}
|
||
|
||
func int64Slice(raw any) []int64 {
|
||
values, ok := raw.([]any)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
out := make([]int64, 0, len(values))
|
||
for _, value := range values {
|
||
switch typed := value.(type) {
|
||
case float64:
|
||
out = append(out, int64(typed))
|
||
case int64:
|
||
out = append(out, typed)
|
||
case int:
|
||
out = append(out, int64(typed))
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func startConsumers(consumers []*rocketmqx.Consumer) error {
|
||
for _, consumer := range consumers {
|
||
if err := consumer.Start(); err != nil {
|
||
shutdownConsumers(consumers)
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func shutdownConsumers(consumers []*rocketmqx.Consumer) {
|
||
for _, consumer := range consumers {
|
||
if err := consumer.Shutdown(); err != nil {
|
||
logx.Warn(context.Background(), "rocketmq_consumer_shutdown_failed", slog.String("error", err.Error()))
|
||
}
|
||
}
|
||
}
|
||
|
||
func consumerConfig(cfg config.RocketMQConfig, mq config.ConsumerMQConfig) rocketmqx.ConsumerConfig {
|
||
return rocketmqx.ConsumerConfig{
|
||
EndpointConfig: rocketmqx.EndpointConfig{
|
||
NameServers: cfg.NameServers,
|
||
NameServerDomain: cfg.NameServerDomain,
|
||
AccessKey: cfg.AccessKey,
|
||
SecretKey: cfg.SecretKey,
|
||
SecurityToken: cfg.SecurityToken,
|
||
Namespace: cfg.Namespace,
|
||
VIPChannel: cfg.VIPChannel,
|
||
},
|
||
GroupName: mq.ConsumerGroup,
|
||
MaxReconsumeTimes: mq.ConsumerMaxReconsumeTimes,
|
||
ConsumeRetryDelay: time.Second,
|
||
ConsumePullBatch: 32,
|
||
ConsumeGoroutines: 1,
|
||
}
|
||
}
|
||
|
||
func firstNonEmpty(values ...string) string {
|
||
for _, value := range values {
|
||
if value = strings.TrimSpace(value); value != "" {
|
||
return value
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func firstNonZeroInt64(values ...int64) int64 {
|
||
for _, value := range values {
|
||
if value != 0 {
|
||
return value
|
||
}
|
||
}
|
||
return 0
|
||
}
|
||
|
||
func boolValue(payload map[string]any, key string) bool {
|
||
switch value := payload[key].(type) {
|
||
case bool:
|
||
return value
|
||
case string:
|
||
return strings.EqualFold(strings.TrimSpace(value), "true") || strings.TrimSpace(value) == "1"
|
||
case float64:
|
||
return value != 0
|
||
case int64:
|
||
return value != 0
|
||
default:
|
||
return false
|
||
}
|
||
}
|