统计修改
This commit is contained in:
parent
af1f6da657
commit
338fc140d8
File diff suppressed because it is too large
Load Diff
@ -1665,6 +1665,8 @@ message LuckyGiftMeta {
|
||||
int32 gift_count = 12;
|
||||
// visible_region_id 来自 room-service 的房间可见区域,用于中奖后的区域 IM 飘屏。
|
||||
int64 visible_region_id = 13;
|
||||
// country_id 来自送礼用户当前国家,用于统计服务按真实国家聚合返奖,不用区域桶反推。
|
||||
int64 country_id = 14;
|
||||
}
|
||||
|
||||
message LuckyGiftTier {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -140,6 +140,8 @@ message LaunchGameRequest {
|
||||
// access_token is the same bearer token used by the App request.
|
||||
// Provider adapters pass it through as the game token instead of minting a separate game token.
|
||||
string access_token = 7;
|
||||
// country_id 由 gateway 从当前用户资料注入,供 provider 回调订单按国家统计。
|
||||
int64 country_id = 8;
|
||||
}
|
||||
|
||||
message LaunchGameResponse {
|
||||
@ -194,6 +196,8 @@ message DiceMatch {
|
||||
int64 ready_at_ms = 24;
|
||||
int64 canceled_at_ms = 25;
|
||||
int64 pool_delta_coin = 26;
|
||||
// country_id 是创建局用户的国家快照,用于局级订单和统计事件按国家归因。
|
||||
int64 country_id = 27;
|
||||
}
|
||||
|
||||
message DiceStakeOption {
|
||||
@ -364,6 +368,8 @@ message CreateDiceMatchRequest {
|
||||
int32 min_players = 7;
|
||||
int32 max_players = 8;
|
||||
string rps_gesture = 9;
|
||||
// country_id 由 gateway 从当前用户资料注入,客户端不能直接决定统计国家。
|
||||
int64 country_id = 10;
|
||||
}
|
||||
|
||||
message JoinDiceMatchRequest {
|
||||
@ -397,6 +403,8 @@ message MatchDiceRequest {
|
||||
int64 region_id = 5;
|
||||
int64 stake_coin = 6;
|
||||
string rps_gesture = 7;
|
||||
// country_id 由 gateway 从当前用户资料注入,客户端不能直接决定统计国家。
|
||||
int64 country_id = 8;
|
||||
}
|
||||
|
||||
message CancelDiceMatchRequest {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1577,6 +1577,8 @@ message CreditLuckyGiftRewardRequest {
|
||||
string pool_id = 8;
|
||||
string reason = 9;
|
||||
int64 visible_region_id = 10;
|
||||
// country_id 是中奖用户所属国家,钱包 outbox 原样发布给统计服务,避免返奖只落到区域未知国家。
|
||||
int64 country_id = 11;
|
||||
}
|
||||
|
||||
// CreditLuckyGiftRewardResponse 返回幸运礼物返奖入账流水和用户 COIN 账后余额。
|
||||
|
||||
@ -152,6 +152,7 @@ type DrawCommand struct {
|
||||
CoinSpent int64
|
||||
PaidAtMS int64
|
||||
VisibleRegionID int64
|
||||
CountryID int64
|
||||
}
|
||||
|
||||
type CheckResult struct {
|
||||
|
||||
@ -326,6 +326,7 @@ func (s *Service) creditLuckyGiftRewardFastPath(ctx context.Context, cmd domain.
|
||||
SenderUserID: cmd.UserID,
|
||||
TargetUserID: cmd.TargetUserID,
|
||||
VisibleRegionID: cmd.VisibleRegionID,
|
||||
CountryID: cmd.CountryID,
|
||||
CoinSpent: cmd.CoinSpent,
|
||||
RuleVersion: result.RuleVersion,
|
||||
ExperiencePool: result.ExperiencePool,
|
||||
@ -395,6 +396,7 @@ func (s *Service) creditLuckyGiftReward(ctx context.Context, appCode string, pay
|
||||
PoolId: payload.PoolID,
|
||||
Reason: "lucky_gift_reward",
|
||||
VisibleRegionId: payload.VisibleRegionID,
|
||||
CountryId: payload.CountryID,
|
||||
})
|
||||
if err != nil {
|
||||
return luckyGiftRewardReceipt{}, err
|
||||
@ -545,6 +547,7 @@ type luckyGiftDrawnPayload struct {
|
||||
SenderUserID int64 `json:"sender_user_id"`
|
||||
TargetUserID int64 `json:"target_user_id"`
|
||||
VisibleRegionID int64 `json:"visible_region_id"`
|
||||
CountryID int64 `json:"country_id"`
|
||||
CoinSpent int64 `json:"coin_spent"`
|
||||
RuleVersion int64 `json:"rule_version"`
|
||||
ExperiencePool string `json:"experience_pool"`
|
||||
|
||||
@ -72,6 +72,7 @@ func TestDrawCreditsLuckyGiftRewardFastPath(t *testing.T) {
|
||||
PoolID: "super_lucky",
|
||||
UserID: 42,
|
||||
TargetUserID: 99,
|
||||
CountryID: 15,
|
||||
DeviceID: "device-1",
|
||||
RoomID: "room-1",
|
||||
AnchorID: "99",
|
||||
@ -83,7 +84,7 @@ func TestDrawCreditsLuckyGiftRewardFastPath(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Draw failed: %v", err)
|
||||
}
|
||||
if wallet.last == nil || wallet.last.GetCommandId() != "lucky_reward:lucky_draw_fast_1" || wallet.last.GetAmount() != 300 || wallet.last.GetTargetUserId() != 42 {
|
||||
if wallet.last == nil || wallet.last.GetCommandId() != "lucky_reward:lucky_draw_fast_1" || wallet.last.GetAmount() != 300 || wallet.last.GetTargetUserId() != 42 || wallet.last.GetCountryId() != 15 {
|
||||
t.Fatalf("wallet fast path request mismatch: %+v", wallet.last)
|
||||
}
|
||||
if result.RewardStatus != domain.StatusGranted || result.WalletTransactionID != "wallet_tx_lucky" || result.CoinBalanceAfter != 1300 {
|
||||
@ -138,6 +139,7 @@ func TestProcessPendingDrawOutboxCreditsWalletOnly(t *testing.T) {
|
||||
"user_id": 42,
|
||||
"sender_user_id": 42,
|
||||
"target_user_id": 99,
|
||||
"country_id": 15,
|
||||
"visible_region_id": 210,
|
||||
"room_id": "room-1",
|
||||
"gift_id": "rose",
|
||||
@ -169,7 +171,7 @@ func TestProcessPendingDrawOutboxCreditsWalletOnly(t *testing.T) {
|
||||
if processed != 1 {
|
||||
t.Fatalf("processed count mismatch: %d", processed)
|
||||
}
|
||||
if wallet.last == nil || wallet.last.GetDrawId() != "lucky_draw_test" || wallet.last.GetTargetUserId() != 42 || wallet.last.GetAmount() != 300 {
|
||||
if wallet.last == nil || wallet.last.GetDrawId() != "lucky_draw_test" || wallet.last.GetTargetUserId() != 42 || wallet.last.GetAmount() != 300 || wallet.last.GetCountryId() != 15 {
|
||||
t.Fatalf("wallet reward request mismatch: %+v", wallet.last)
|
||||
}
|
||||
if publisher.last.EventID != "" || broadcaster.last.EventID != "" {
|
||||
@ -192,6 +194,7 @@ func TestProcessPendingBatchDrawOutboxCreditsWalletOnceAndGrantsAllDraws(t *test
|
||||
"user_id": 42,
|
||||
"sender_user_id": 42,
|
||||
"target_user_id": 99,
|
||||
"country_id": 15,
|
||||
"room_id": "room-1",
|
||||
"gift_id": "rose",
|
||||
"gift_count": 3,
|
||||
@ -222,7 +225,7 @@ func TestProcessPendingBatchDrawOutboxCreditsWalletOnceAndGrantsAllDraws(t *test
|
||||
if processed != 1 {
|
||||
t.Fatalf("processed count mismatch: %d", processed)
|
||||
}
|
||||
if wallet.last == nil || wallet.last.GetAmount() != 800 || wallet.last.GetDrawId() != "lucky_draw_batch_1" || wallet.last.GetCommandId() != "lucky_reward:lucky_draw_batch_1" || wallet.last.GetVisibleRegionId() != 210 {
|
||||
if wallet.last == nil || wallet.last.GetAmount() != 800 || wallet.last.GetDrawId() != "lucky_draw_batch_1" || wallet.last.GetCommandId() != "lucky_reward:lucky_draw_batch_1" || wallet.last.GetCountryId() != 15 || wallet.last.GetVisibleRegionId() != 210 {
|
||||
t.Fatalf("wallet should receive one aggregate reward request: %+v", wallet.last)
|
||||
}
|
||||
if got := strings.Join(repository.grantedDrawIDs, ","); got != "lucky_draw_batch_1,lucky_draw_batch_2,lucky_draw_batch_3" {
|
||||
|
||||
@ -1130,6 +1130,7 @@ func (r *Repository) insertLuckyAggregateRewardSettlementOutbox(ctx context.Cont
|
||||
"sender_user_id": cmd.UserID,
|
||||
"target_user_id": cmd.TargetUserID,
|
||||
"visible_region_id": cmd.VisibleRegionID,
|
||||
"country_id": cmd.CountryID,
|
||||
"room_id": cmd.RoomID,
|
||||
"gift_id": cmd.GiftID,
|
||||
"gift_count": cmd.GiftCount,
|
||||
@ -1568,6 +1569,7 @@ func (r *Repository) applyLuckyDraw(ctx context.Context, tx *sql.Tx, input lucky
|
||||
"sender_user_id": input.Command.UserID,
|
||||
"target_user_id": input.Command.TargetUserID,
|
||||
"visible_region_id": input.Command.VisibleRegionID,
|
||||
"country_id": input.Command.CountryID,
|
||||
"room_id": input.Command.RoomID,
|
||||
"gift_id": input.Command.GiftID,
|
||||
"gift_count": input.Command.GiftCount,
|
||||
|
||||
@ -60,6 +60,7 @@ func (s *LuckyGiftServer) ExecuteLuckyGiftDraw(ctx context.Context, req *activit
|
||||
CoinSpent: meta.GetCoinSpent(),
|
||||
PaidAtMS: meta.GetPaidAtMs(),
|
||||
VisibleRegionID: meta.GetVisibleRegionId(),
|
||||
CountryID: meta.GetCountryId(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
|
||||
@ -69,6 +69,7 @@ CREATE TABLE IF NOT EXISTS game_launch_sessions (
|
||||
display_user_id VARCHAR(32) NOT NULL COMMENT '展示用户 ID',
|
||||
room_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '房间 ID',
|
||||
region_id BIGINT NOT NULL DEFAULT 0 COMMENT '用户启动游戏时的区域 ID',
|
||||
country_id BIGINT NOT NULL DEFAULT 0 COMMENT '用户启动游戏时的国家 ID',
|
||||
scene VARCHAR(64) NOT NULL COMMENT '场景',
|
||||
platform_code VARCHAR(64) NOT NULL COMMENT '平台编码',
|
||||
game_id VARCHAR(96) NOT NULL COMMENT '游戏 ID',
|
||||
@ -95,6 +96,7 @@ CREATE TABLE IF NOT EXISTS game_orders (
|
||||
user_id BIGINT NOT NULL COMMENT '用户 ID',
|
||||
room_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '房间 ID',
|
||||
region_id BIGINT NOT NULL DEFAULT 0 COMMENT '用户启动游戏时的区域 ID',
|
||||
country_id BIGINT NOT NULL DEFAULT 0 COMMENT '用户启动游戏时的国家 ID',
|
||||
op_type VARCHAR(32) NOT NULL COMMENT 'op类型',
|
||||
coin_amount BIGINT NOT NULL COMMENT '金币数量',
|
||||
status VARCHAR(32) NOT NULL COMMENT '业务状态',
|
||||
@ -120,6 +122,7 @@ CREATE TABLE IF NOT EXISTS game_dice_matches (
|
||||
provider_game_id VARCHAR(128) NOT NULL COMMENT '提供方游戏 ID 快照',
|
||||
room_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '房间 ID',
|
||||
region_id BIGINT NOT NULL DEFAULT 0 COMMENT '用户区域 ID',
|
||||
country_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建局用户国家 ID',
|
||||
min_players INT NOT NULL COMMENT '最少开局人数',
|
||||
max_players INT NOT NULL COMMENT '最多参与人数',
|
||||
current_players INT NOT NULL DEFAULT 0 COMMENT '当前参与人数',
|
||||
|
||||
@ -165,6 +165,7 @@ type Match struct {
|
||||
ProviderGameID string
|
||||
RoomID string
|
||||
RegionID int64
|
||||
CountryID int64
|
||||
MinPlayers int32
|
||||
MaxPlayers int32
|
||||
CurrentPlayers int32
|
||||
|
||||
@ -117,6 +117,7 @@ type LaunchSession struct {
|
||||
DisplayUserID string
|
||||
RoomID string
|
||||
RegionID int64
|
||||
CountryID int64
|
||||
Scene string
|
||||
PlatformCode string
|
||||
GameID string
|
||||
@ -143,6 +144,7 @@ type GameOrder struct {
|
||||
UserID int64
|
||||
RoomID string
|
||||
RegionID int64
|
||||
CountryID int64
|
||||
// OpType 只允许钱包语义 debit/credit,厂商枚举在适配器层转换。
|
||||
OpType string
|
||||
CoinAmount int64
|
||||
|
||||
@ -145,6 +145,7 @@ type CreateMatchCommand struct {
|
||||
GameID string
|
||||
RoomID string
|
||||
RegionID int64
|
||||
CountryID int64
|
||||
StakeCoin int64
|
||||
MinPlayers int32
|
||||
MaxPlayers int32
|
||||
@ -183,6 +184,7 @@ type MatchCommand struct {
|
||||
GameID string
|
||||
RoomID string
|
||||
RegionID int64
|
||||
CountryID int64
|
||||
StakeCoin int64
|
||||
RPSGesture string
|
||||
}
|
||||
@ -291,6 +293,7 @@ func (s *Service) CreateMatch(ctx context.Context, command CreateMatchCommand) (
|
||||
ProviderGameID: strings.TrimSpace(game.ProviderGameID),
|
||||
RoomID: strings.TrimSpace(command.RoomID),
|
||||
RegionID: command.RegionID,
|
||||
CountryID: command.CountryID,
|
||||
MinPlayers: minPlayers,
|
||||
MaxPlayers: maxPlayers,
|
||||
CurrentPlayers: 1,
|
||||
@ -650,6 +653,7 @@ func (s *Service) Match(ctx context.Context, command MatchCommand) (dicedomain.M
|
||||
GameID: config.GameID,
|
||||
RoomID: "",
|
||||
RegionID: command.RegionID,
|
||||
CountryID: command.CountryID,
|
||||
StakeCoin: command.StakeCoin,
|
||||
MinPlayers: config.MinPlayers,
|
||||
MaxPlayers: config.MaxPlayers,
|
||||
@ -1569,6 +1573,7 @@ func (s *Service) applyGameCoinChange(ctx context.Context, requestID string, mat
|
||||
UserID: participant.UserID,
|
||||
RoomID: match.RoomID,
|
||||
RegionID: match.RegionID,
|
||||
CountryID: match.CountryID,
|
||||
OpType: opType,
|
||||
CoinAmount: amount,
|
||||
Status: gamedomain.OrderStatusWalletApplying,
|
||||
|
||||
@ -130,6 +130,7 @@ type LaunchCommand struct {
|
||||
Scene string
|
||||
RoomID string
|
||||
RegionID int64
|
||||
CountryID int64
|
||||
AccessToken string
|
||||
}
|
||||
|
||||
@ -275,13 +276,20 @@ func (s *Service) LaunchGame(ctx context.Context, command LaunchCommand) (Launch
|
||||
if game.PlatformStatus != gamedomain.StatusActive || game.Status != gamedomain.StatusActive {
|
||||
return LaunchResult{}, xerr.New(xerr.Conflict, "game is not launchable")
|
||||
}
|
||||
regionID := command.RegionID
|
||||
if regionID <= 0 {
|
||||
regionID, countryID := command.RegionID, command.CountryID
|
||||
if regionID <= 0 || countryID <= 0 {
|
||||
var err error
|
||||
regionID, err = s.resolveUserRegion(ctx, command.AppCode, command.RequestID, command.UserID)
|
||||
// 游戏订单的统计事件只能用启动时的用户维度快照,不能在回调时靠区域桶或当前资料反推国家。
|
||||
resolvedRegionID, resolvedCountryID, err := s.resolveUserDimensions(ctx, command.AppCode, command.RequestID, command.UserID)
|
||||
if err != nil {
|
||||
return LaunchResult{}, err
|
||||
}
|
||||
if regionID <= 0 {
|
||||
regionID = resolvedRegionID
|
||||
}
|
||||
if countryID <= 0 {
|
||||
countryID = resolvedCountryID
|
||||
}
|
||||
}
|
||||
|
||||
now := s.now()
|
||||
@ -316,6 +324,7 @@ func (s *Service) LaunchGame(ctx context.Context, command LaunchCommand) (Launch
|
||||
DisplayUserID: command.DisplayUserID,
|
||||
RoomID: strings.TrimSpace(command.RoomID),
|
||||
RegionID: regionID,
|
||||
CountryID: countryID,
|
||||
Scene: command.Scene,
|
||||
PlatformCode: game.PlatformCode,
|
||||
GameID: game.GameID,
|
||||
@ -515,8 +524,15 @@ func (s *Service) getCallbackUserInfo(ctx context.Context, app string, req *game
|
||||
}
|
||||
|
||||
func (s *Service) resolveUserRegion(ctx context.Context, app string, requestID string, userID int64) (int64, error) {
|
||||
regionID, _, err := s.resolveUserDimensions(ctx, app, requestID, userID)
|
||||
return regionID, err
|
||||
}
|
||||
|
||||
// resolveUserDimensions 只在入口没有携带完整维度时兜底读取 user-service;
|
||||
// 统计事实随后持久化快照,避免后续用户换国家导致历史订单归因漂移。
|
||||
func (s *Service) resolveUserDimensions(ctx context.Context, app string, requestID string, userID int64) (int64, int64, error) {
|
||||
if userID <= 0 || s.user == nil {
|
||||
return 0, nil
|
||||
return 0, 0, nil
|
||||
}
|
||||
userResp, err := s.user.GetUser(ctx, &userv1.GetUserRequest{
|
||||
Meta: &userv1.RequestMeta{
|
||||
@ -528,9 +544,10 @@ func (s *Service) resolveUserRegion(ctx context.Context, app string, requestID s
|
||||
UserId: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, 0, err
|
||||
}
|
||||
return userResp.GetUser().GetRegionId(), nil
|
||||
user := userResp.GetUser()
|
||||
return user.GetRegionId(), user.GetCountryId(), nil
|
||||
}
|
||||
|
||||
func (s *Service) applyCallbackCoinChange(ctx context.Context, app string, req *gamev1.CallbackRequest, requestHash string) (map[string]any, error) {
|
||||
@ -563,7 +580,7 @@ func (s *Service) applyCallbackCoinChange(ctx context.Context, app string, req *
|
||||
}
|
||||
body.ProviderGameID = game.ProviderGameID
|
||||
}
|
||||
regionID, err := s.resolveUserRegion(ctx, app, req.GetMeta().GetRequestId(), body.UserID)
|
||||
regionID, countryID, err := s.resolveUserDimensions(ctx, app, req.GetMeta().GetRequestId(), body.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -578,6 +595,7 @@ func (s *Service) applyCallbackCoinChange(ctx context.Context, app string, req *
|
||||
UserID: body.UserID,
|
||||
RoomID: strings.TrimSpace(body.RoomID),
|
||||
RegionID: regionID,
|
||||
CountryID: countryID,
|
||||
OpType: body.OpType,
|
||||
CoinAmount: body.CoinAmount,
|
||||
Status: gamedomain.OrderStatusWalletApplying,
|
||||
|
||||
@ -309,6 +309,7 @@ func (s *Service) applyYomiCoinChange(ctx context.Context, app string, req *game
|
||||
UserID: session.UserID,
|
||||
RoomID: strings.TrimSpace(firstNonEmpty(roomID, session.RoomID)),
|
||||
RegionID: session.RegionID,
|
||||
CountryID: session.CountryID,
|
||||
OpType: opType,
|
||||
CoinAmount: amount,
|
||||
Status: gamedomain.OrderStatusWalletApplying,
|
||||
|
||||
@ -30,12 +30,12 @@ func (r *Repository) CreateDiceMatch(ctx context.Context, match dicedomain.Match
|
||||
// match 和首个 participant 必须同事务提交;否则创建成功但参与者缺失会导致后续 roll 永远人数不足。
|
||||
if _, err := tx.ExecContext(ctx,
|
||||
`INSERT INTO game_dice_matches (
|
||||
app_code, match_id, game_id, platform_code, provider_game_id, room_id, region_id,
|
||||
app_code, match_id, game_id, platform_code, provider_game_id, room_id, region_id, country_id,
|
||||
min_players, max_players, current_players, stake_coin, round_no, status, result,
|
||||
join_deadline_ms, ready_at_ms, created_at_ms, updated_at_ms, settled_at_ms, canceled_at_ms,
|
||||
fee_bps, pool_bps, match_mode, forced_result, pool_delta_coin
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
match.AppCode, match.MatchID, match.GameID, match.PlatformCode, match.ProviderGameID, match.RoomID, match.RegionID,
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
match.AppCode, match.MatchID, match.GameID, match.PlatformCode, match.ProviderGameID, match.RoomID, match.RegionID, match.CountryID,
|
||||
match.MinPlayers, match.MaxPlayers, match.CurrentPlayers, match.StakeCoin, match.RoundNo, match.Status, match.Result,
|
||||
match.JoinDeadlineMS, match.ReadyAtMS, match.CreatedAtMS, match.UpdatedAtMS, match.SettledAtMS, match.CanceledAtMS,
|
||||
match.FeeBPS, match.PoolBPS, match.MatchMode, match.ForcedResult, match.PoolDeltaCoin,
|
||||
@ -548,6 +548,7 @@ func selfGameMatchOutboxPayload(match dicedomain.Match, nowMs int64) map[string]
|
||||
"provider_game_id": strings.TrimSpace(match.ProviderGameID),
|
||||
"room_id": strings.TrimSpace(match.RoomID),
|
||||
"region_id": match.RegionID,
|
||||
"country_id": match.CountryID,
|
||||
"stake_coin": match.StakeCoin,
|
||||
"min_players": match.MinPlayers,
|
||||
"max_players": match.MaxPlayers,
|
||||
@ -584,7 +585,7 @@ func selfGameMatchOutboxPayload(match dicedomain.Match, nowMs int64) map[string]
|
||||
|
||||
func queryDiceMatch(ctx context.Context, q diceQuerier, appCode string, matchID string, forUpdate bool) (dicedomain.Match, error) {
|
||||
// forUpdate 只给状态推进路径使用;读路径不加锁,避免普通刷新阻塞 join/roll。
|
||||
query := `SELECT app_code, match_id, game_id, platform_code, provider_game_id, room_id, region_id,
|
||||
query := `SELECT app_code, match_id, game_id, platform_code, provider_game_id, room_id, region_id, country_id,
|
||||
min_players, max_players, current_players, stake_coin, round_no, status, result,
|
||||
join_deadline_ms, ready_at_ms, created_at_ms, updated_at_ms, settled_at_ms, canceled_at_ms,
|
||||
fee_bps, pool_bps, match_mode, forced_result, pool_delta_coin
|
||||
@ -595,7 +596,7 @@ func queryDiceMatch(ctx context.Context, q diceQuerier, appCode string, matchID
|
||||
}
|
||||
row := q.QueryRowContext(ctx, query, appcode.Normalize(appCode), strings.TrimSpace(matchID))
|
||||
var match dicedomain.Match
|
||||
if err := row.Scan(&match.AppCode, &match.MatchID, &match.GameID, &match.PlatformCode, &match.ProviderGameID, &match.RoomID, &match.RegionID, &match.MinPlayers, &match.MaxPlayers, &match.CurrentPlayers, &match.StakeCoin, &match.RoundNo, &match.Status, &match.Result, &match.JoinDeadlineMS, &match.ReadyAtMS, &match.CreatedAtMS, &match.UpdatedAtMS, &match.SettledAtMS, &match.CanceledAtMS, &match.FeeBPS, &match.PoolBPS, &match.MatchMode, &match.ForcedResult, &match.PoolDeltaCoin); err != nil {
|
||||
if err := row.Scan(&match.AppCode, &match.MatchID, &match.GameID, &match.PlatformCode, &match.ProviderGameID, &match.RoomID, &match.RegionID, &match.CountryID, &match.MinPlayers, &match.MaxPlayers, &match.CurrentPlayers, &match.StakeCoin, &match.RoundNo, &match.Status, &match.Result, &match.JoinDeadlineMS, &match.ReadyAtMS, &match.CreatedAtMS, &match.UpdatedAtMS, &match.SettledAtMS, &match.CanceledAtMS, &match.FeeBPS, &match.PoolBPS, &match.MatchMode, &match.ForcedResult, &match.PoolDeltaCoin); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return dicedomain.Match{}, xerr.New(xerr.NotFound, "dice match not found")
|
||||
}
|
||||
|
||||
@ -120,6 +120,7 @@ func (r *Repository) Migrate(ctx context.Context) error {
|
||||
display_user_id VARCHAR(32) NOT NULL,
|
||||
room_id VARCHAR(96) NOT NULL DEFAULT '',
|
||||
region_id BIGINT NOT NULL DEFAULT 0,
|
||||
country_id BIGINT NOT NULL DEFAULT 0,
|
||||
scene VARCHAR(64) NOT NULL,
|
||||
platform_code VARCHAR(64) NOT NULL,
|
||||
game_id VARCHAR(96) NOT NULL,
|
||||
@ -144,6 +145,7 @@ func (r *Repository) Migrate(ctx context.Context) error {
|
||||
user_id BIGINT NOT NULL,
|
||||
room_id VARCHAR(96) NOT NULL DEFAULT '',
|
||||
region_id BIGINT NOT NULL DEFAULT 0,
|
||||
country_id BIGINT NOT NULL DEFAULT 0,
|
||||
op_type VARCHAR(32) NOT NULL,
|
||||
coin_amount BIGINT NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
@ -168,6 +170,7 @@ func (r *Repository) Migrate(ctx context.Context) error {
|
||||
provider_game_id VARCHAR(128) NOT NULL,
|
||||
room_id VARCHAR(96) NOT NULL DEFAULT '',
|
||||
region_id BIGINT NOT NULL DEFAULT 0,
|
||||
country_id BIGINT NOT NULL DEFAULT 0,
|
||||
min_players INT NOT NULL,
|
||||
max_players INT NOT NULL,
|
||||
current_players INT NOT NULL DEFAULT 0,
|
||||
@ -520,6 +523,15 @@ func (r *Repository) Migrate(ctx context.Context) error {
|
||||
if err := r.ensureColumn(ctx, "game_self_game_new_user_policies", "black_pool_robot_force_win_enabled", "black_pool_robot_force_win_enabled TINYINT(1) NOT NULL DEFAULT 0 AFTER normal_phase_target_win_rate_percent"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureColumn(ctx, "game_orders", "country_id", "country_id BIGINT NOT NULL DEFAULT 0 AFTER region_id"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureColumn(ctx, "game_launch_sessions", "country_id", "country_id BIGINT NOT NULL DEFAULT 0 AFTER region_id"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureColumn(ctx, "game_dice_matches", "country_id", "country_id BIGINT NOT NULL DEFAULT 0 AFTER region_id"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureColumn(ctx, "game_outbox", "worker_id", "worker_id VARCHAR(128) NOT NULL DEFAULT '' AFTER status"); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -823,10 +835,10 @@ func (r *Repository) GetPlatform(ctx context.Context, appCode string, platformCo
|
||||
func (r *Repository) CreateLaunchSession(ctx context.Context, session gamedomain.LaunchSession) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
`INSERT INTO game_launch_sessions (
|
||||
app_code, session_id, user_id, display_user_id, room_id, region_id, scene, platform_code, game_id,
|
||||
app_code, session_id, user_id, display_user_id, room_id, region_id, country_id, scene, platform_code, game_id,
|
||||
provider_game_id, launch_token_hash, status, expires_at_ms, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
appcode.Normalize(session.AppCode), session.SessionID, session.UserID, session.DisplayUserID, session.RoomID, session.RegionID,
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
appcode.Normalize(session.AppCode), session.SessionID, session.UserID, session.DisplayUserID, session.RoomID, session.RegionID, session.CountryID,
|
||||
session.Scene, session.PlatformCode, session.GameID, session.ProviderGameID, session.LaunchTokenHash, session.Status,
|
||||
session.ExpiresAtMS, session.CreatedAtMS, session.UpdatedAtMS,
|
||||
)
|
||||
@ -835,14 +847,14 @@ func (r *Repository) CreateLaunchSession(ctx context.Context, session gamedomain
|
||||
|
||||
func (r *Repository) GetLaunchSessionByToken(ctx context.Context, appCode string, token string) (gamedomain.LaunchSession, error) {
|
||||
row := r.db.QueryRowContext(ctx,
|
||||
`SELECT app_code, session_id, user_id, display_user_id, room_id, region_id, scene, platform_code, game_id,
|
||||
`SELECT app_code, session_id, user_id, display_user_id, room_id, region_id, country_id, scene, platform_code, game_id,
|
||||
provider_game_id, launch_token_hash, status, expires_at_ms, created_at_ms, updated_at_ms
|
||||
FROM game_launch_sessions
|
||||
WHERE app_code = ? AND launch_token_hash = ?`,
|
||||
appcode.Normalize(appCode), stableHash(strings.TrimSpace(token)),
|
||||
)
|
||||
var item gamedomain.LaunchSession
|
||||
if err := row.Scan(&item.AppCode, &item.SessionID, &item.UserID, &item.DisplayUserID, &item.RoomID, &item.RegionID, &item.Scene, &item.PlatformCode, &item.GameID, &item.ProviderGameID, &item.LaunchTokenHash, &item.Status, &item.ExpiresAtMS, &item.CreatedAtMS, &item.UpdatedAtMS); err != nil {
|
||||
if err := row.Scan(&item.AppCode, &item.SessionID, &item.UserID, &item.DisplayUserID, &item.RoomID, &item.RegionID, &item.CountryID, &item.Scene, &item.PlatformCode, &item.GameID, &item.ProviderGameID, &item.LaunchTokenHash, &item.Status, &item.ExpiresAtMS, &item.CreatedAtMS, &item.UpdatedAtMS); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return gamedomain.LaunchSession{}, xerr.New(xerr.Unauthorized, "game session token invalid")
|
||||
}
|
||||
@ -855,7 +867,7 @@ func (r *Repository) GetLaunchSessionByTokenScope(ctx context.Context, appCode s
|
||||
// 同一个 App access token 会被多个厂商和多个游戏复用,回调校验必须把 token、平台和可选游戏 ID 一起收窄。
|
||||
// 这里按 created_at_ms 倒序取最新一条,避免同用户重复打开同一游戏时拿到旧房间或旧 session。
|
||||
row := r.db.QueryRowContext(ctx,
|
||||
`SELECT app_code, session_id, user_id, display_user_id, room_id, region_id, scene, platform_code, game_id,
|
||||
`SELECT app_code, session_id, user_id, display_user_id, room_id, region_id, country_id, scene, platform_code, game_id,
|
||||
provider_game_id, launch_token_hash, status, expires_at_ms, created_at_ms, updated_at_ms
|
||||
FROM game_launch_sessions
|
||||
WHERE app_code = ?
|
||||
@ -867,7 +879,7 @@ func (r *Repository) GetLaunchSessionByTokenScope(ctx context.Context, appCode s
|
||||
appcode.Normalize(appCode), stableHash(strings.TrimSpace(token)), strings.TrimSpace(platformCode), strings.TrimSpace(providerGameID), strings.TrimSpace(providerGameID),
|
||||
)
|
||||
var item gamedomain.LaunchSession
|
||||
if err := row.Scan(&item.AppCode, &item.SessionID, &item.UserID, &item.DisplayUserID, &item.RoomID, &item.RegionID, &item.Scene, &item.PlatformCode, &item.GameID, &item.ProviderGameID, &item.LaunchTokenHash, &item.Status, &item.ExpiresAtMS, &item.CreatedAtMS, &item.UpdatedAtMS); err != nil {
|
||||
if err := row.Scan(&item.AppCode, &item.SessionID, &item.UserID, &item.DisplayUserID, &item.RoomID, &item.RegionID, &item.CountryID, &item.Scene, &item.PlatformCode, &item.GameID, &item.ProviderGameID, &item.LaunchTokenHash, &item.Status, &item.ExpiresAtMS, &item.CreatedAtMS, &item.UpdatedAtMS); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return gamedomain.LaunchSession{}, xerr.New(xerr.Unauthorized, "game session token invalid")
|
||||
}
|
||||
@ -922,10 +934,10 @@ func (r *Repository) CreateGameOrder(ctx context.Context, order gamedomain.GameO
|
||||
if _, err := tx.ExecContext(ctx,
|
||||
`INSERT INTO game_orders (
|
||||
app_code, order_id, platform_code, provider_order_id, provider_round_id, game_id, provider_game_id,
|
||||
user_id, room_id, region_id, op_type, coin_amount, status, request_hash, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
user_id, room_id, region_id, country_id, op_type, coin_amount, status, request_hash, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
order.AppCode, order.OrderID, order.PlatformCode, order.ProviderOrderID, order.ProviderRoundID, order.GameID, order.ProviderGameID,
|
||||
order.UserID, order.RoomID, order.RegionID, order.OpType, order.CoinAmount, order.Status, order.RequestHash, order.CreatedAtMS, order.UpdatedAtMS,
|
||||
order.UserID, order.RoomID, order.RegionID, order.CountryID, order.OpType, order.CoinAmount, order.Status, order.RequestHash, order.CreatedAtMS, order.UpdatedAtMS,
|
||||
); err != nil {
|
||||
return gamedomain.GameOrder{}, false, err
|
||||
}
|
||||
@ -938,7 +950,7 @@ func (r *Repository) CreateGameOrder(ctx context.Context, order gamedomain.GameO
|
||||
func queryOrderForUpdate(ctx context.Context, tx *sql.Tx, appCode string, platformCode string, providerOrderID string) (gamedomain.GameOrder, bool, error) {
|
||||
row := tx.QueryRowContext(ctx,
|
||||
`SELECT app_code, order_id, platform_code, provider_order_id, provider_round_id, game_id, provider_game_id,
|
||||
user_id, room_id, region_id, op_type, coin_amount, status, wallet_transaction_id, wallet_balance_after,
|
||||
user_id, room_id, region_id, country_id, op_type, coin_amount, status, wallet_transaction_id, wallet_balance_after,
|
||||
request_hash, failure_code, failure_message, created_at_ms, updated_at_ms
|
||||
FROM game_orders
|
||||
WHERE app_code = ? AND platform_code = ? AND provider_order_id = ?
|
||||
@ -946,7 +958,7 @@ func queryOrderForUpdate(ctx context.Context, tx *sql.Tx, appCode string, platfo
|
||||
appcode.Normalize(appCode), strings.TrimSpace(platformCode), strings.TrimSpace(providerOrderID),
|
||||
)
|
||||
var order gamedomain.GameOrder
|
||||
if err := row.Scan(&order.AppCode, &order.OrderID, &order.PlatformCode, &order.ProviderOrderID, &order.ProviderRoundID, &order.GameID, &order.ProviderGameID, &order.UserID, &order.RoomID, &order.RegionID, &order.OpType, &order.CoinAmount, &order.Status, &order.WalletTransactionID, &order.WalletBalanceAfter, &order.RequestHash, &order.FailureCode, &order.FailureMessage, &order.CreatedAtMS, &order.UpdatedAtMS); err != nil {
|
||||
if err := row.Scan(&order.AppCode, &order.OrderID, &order.PlatformCode, &order.ProviderOrderID, &order.ProviderRoundID, &order.GameID, &order.ProviderGameID, &order.UserID, &order.RoomID, &order.RegionID, &order.CountryID, &order.OpType, &order.CoinAmount, &order.Status, &order.WalletTransactionID, &order.WalletBalanceAfter, &order.RequestHash, &order.FailureCode, &order.FailureMessage, &order.CreatedAtMS, &order.UpdatedAtMS); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return gamedomain.GameOrder{}, false, nil
|
||||
}
|
||||
@ -1044,7 +1056,7 @@ func (r *Repository) getRepairOrderByProvider(ctx context.Context, appCode strin
|
||||
func queryOrderByIDForUpdate(ctx context.Context, tx *sql.Tx, appCode string, orderID string) (gamedomain.GameOrder, bool, error) {
|
||||
row := tx.QueryRowContext(ctx,
|
||||
`SELECT app_code, order_id, platform_code, provider_order_id, provider_round_id, game_id, provider_game_id,
|
||||
user_id, room_id, region_id, op_type, coin_amount, status, wallet_transaction_id, wallet_balance_after,
|
||||
user_id, room_id, region_id, country_id, op_type, coin_amount, status, wallet_transaction_id, wallet_balance_after,
|
||||
request_hash, failure_code, failure_message, created_at_ms, updated_at_ms
|
||||
FROM game_orders
|
||||
WHERE app_code = ? AND order_id = ?
|
||||
@ -1052,7 +1064,7 @@ func queryOrderByIDForUpdate(ctx context.Context, tx *sql.Tx, appCode string, or
|
||||
appcode.Normalize(appCode), strings.TrimSpace(orderID),
|
||||
)
|
||||
var order gamedomain.GameOrder
|
||||
if err := row.Scan(&order.AppCode, &order.OrderID, &order.PlatformCode, &order.ProviderOrderID, &order.ProviderRoundID, &order.GameID, &order.ProviderGameID, &order.UserID, &order.RoomID, &order.RegionID, &order.OpType, &order.CoinAmount, &order.Status, &order.WalletTransactionID, &order.WalletBalanceAfter, &order.RequestHash, &order.FailureCode, &order.FailureMessage, &order.CreatedAtMS, &order.UpdatedAtMS); err != nil {
|
||||
if err := row.Scan(&order.AppCode, &order.OrderID, &order.PlatformCode, &order.ProviderOrderID, &order.ProviderRoundID, &order.GameID, &order.ProviderGameID, &order.UserID, &order.RoomID, &order.RegionID, &order.CountryID, &order.OpType, &order.CoinAmount, &order.Status, &order.WalletTransactionID, &order.WalletBalanceAfter, &order.RequestHash, &order.FailureCode, &order.FailureMessage, &order.CreatedAtMS, &order.UpdatedAtMS); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return gamedomain.GameOrder{}, false, nil
|
||||
}
|
||||
@ -1087,6 +1099,7 @@ func insertGameLevelEventOutbox(ctx context.Context, tx *sql.Tx, order gamedomai
|
||||
|
||||
func insertGameStatisticsOutbox(ctx context.Context, tx *sql.Tx, order gamedomain.GameOrder, walletTransactionID string, balanceAfter int64, nowMs int64) error {
|
||||
eventID := "GameOrderSettled:" + order.OrderID
|
||||
// 国家必须来自订单快照;region_id 只能表达区域分桶,不能替代国家维度。
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
INSERT IGNORE INTO game_outbox (
|
||||
app_code, event_id, event_type, order_id, user_id, platform_code, game_id, op_type,
|
||||
@ -1102,6 +1115,7 @@ func insertGameStatisticsOutbox(ctx context.Context, tx *sql.Tx, order gamedomai
|
||||
'room_id', ?,
|
||||
'visible_region_id', ?,
|
||||
'region_id', ?,
|
||||
'country_id', ?,
|
||||
'op_type', ?,
|
||||
'coin_amount', ?,
|
||||
'wallet_transaction_id', ?,
|
||||
@ -1109,7 +1123,7 @@ func insertGameStatisticsOutbox(ctx context.Context, tx *sql.Tx, order gamedomai
|
||||
), 'pending', ?, ?)`,
|
||||
order.AppCode, eventID, order.OrderID, order.UserID, order.PlatformCode, order.GameID, order.OpType, order.CoinAmount,
|
||||
order.OrderID, order.PlatformCode, order.GameID, order.ProviderGameID, order.ProviderOrderID, order.ProviderRoundID,
|
||||
order.UserID, order.RoomID, order.RegionID, order.RegionID, order.OpType, order.CoinAmount, walletTransactionID, balanceAfter,
|
||||
order.UserID, order.RoomID, order.RegionID, order.RegionID, order.CountryID, order.OpType, order.CoinAmount, walletTransactionID, balanceAfter,
|
||||
nowMs, nowMs,
|
||||
)
|
||||
return err
|
||||
|
||||
@ -130,6 +130,7 @@ func (s *Server) LaunchGame(ctx context.Context, req *gamev1.LaunchGameRequest)
|
||||
GameID: req.GetGameId(),
|
||||
Scene: req.GetScene(),
|
||||
RoomID: req.GetRoomId(),
|
||||
CountryID: req.GetCountryId(),
|
||||
AccessToken: req.GetAccessToken(),
|
||||
})
|
||||
if err != nil {
|
||||
@ -169,6 +170,7 @@ func (s *Server) MatchDice(ctx context.Context, req *gamev1.MatchDiceRequest) (*
|
||||
GameID: req.GetGameId(),
|
||||
RoomID: req.GetRoomId(),
|
||||
RegionID: req.GetRegionId(),
|
||||
CountryID: req.GetCountryId(),
|
||||
StakeCoin: req.GetStakeCoin(),
|
||||
RPSGesture: req.GetRpsGesture(),
|
||||
})
|
||||
@ -191,6 +193,7 @@ func (s *Server) CreateDiceMatch(ctx context.Context, req *gamev1.CreateDiceMatc
|
||||
GameID: req.GetGameId(),
|
||||
RoomID: req.GetRoomId(),
|
||||
RegionID: req.GetRegionId(),
|
||||
CountryID: req.GetCountryId(),
|
||||
StakeCoin: req.GetStakeCoin(),
|
||||
MinPlayers: req.GetMinPlayers(),
|
||||
MaxPlayers: req.GetMaxPlayers(),
|
||||
@ -765,6 +768,7 @@ func diceMatchToProtoAt(match dicedomain.Match, nowMS int64) *gamev1.DiceMatch {
|
||||
GameId: match.GameID,
|
||||
RoomId: match.RoomID,
|
||||
RegionId: match.RegionID,
|
||||
CountryId: match.CountryID,
|
||||
MinPlayers: match.MinPlayers,
|
||||
MaxPlayers: match.MaxPlayers,
|
||||
CurrentPlayers: match.CurrentPlayers,
|
||||
|
||||
@ -155,6 +155,7 @@ func (h *Handler) launchGame(writer http.ResponseWriter, request *http.Request)
|
||||
Scene: body.Scene,
|
||||
RoomId: body.RoomID,
|
||||
AccessToken: bearerAccessToken(request),
|
||||
CountryId: userResp.GetUser().GetCountryId(),
|
||||
})
|
||||
httpkit.Write(writer, request, gameLaunchDataFromProto(resp), err)
|
||||
}
|
||||
@ -237,6 +238,7 @@ func (h *Handler) matchSelfGame(writer http.ResponseWriter, request *http.Reques
|
||||
GameId: gameID,
|
||||
RoomId: strings.TrimSpace(body.RoomID),
|
||||
RegionId: userResp.GetUser().GetRegionId(),
|
||||
CountryId: userResp.GetUser().GetCountryId(),
|
||||
StakeCoin: body.StakeCoin,
|
||||
RpsGesture: rpsGesture,
|
||||
})
|
||||
@ -297,6 +299,7 @@ func (h *Handler) createSelfGameMatch(writer http.ResponseWriter, request *http.
|
||||
GameId: gameID,
|
||||
RoomId: strings.TrimSpace(body.RoomID),
|
||||
RegionId: userResp.GetUser().GetRegionId(),
|
||||
CountryId: userResp.GetUser().GetCountryId(),
|
||||
StakeCoin: body.StakeCoin,
|
||||
MinPlayers: body.MinPlayers,
|
||||
MaxPlayers: body.MaxPlayers,
|
||||
|
||||
@ -569,6 +569,7 @@ func (s *Service) executeLuckyGiftDraws(ctx context.Context, meta *roomv1.Reques
|
||||
PaidAtMs: now.UTC().UnixMilli(),
|
||||
PoolId: cmd.PoolID,
|
||||
VisibleRegionId: roomMeta.VisibleRegionID,
|
||||
CountryId: cmd.SenderCountryID,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@ -470,6 +470,7 @@ func selfGameMatchEvent(body []byte) (mysqlstorage.SelfGameMatchEvent, bool, err
|
||||
GameID: firstNonEmpty(mysqlstorage.String(payload, "game_id"), message.GameID),
|
||||
PlatformCode: firstNonEmpty(mysqlstorage.String(payload, "platform_code"), message.PlatformCode),
|
||||
RoomID: mysqlstorage.String(payload, "room_id"),
|
||||
CountryID: mysqlstorage.Int64(payload, "country_id"),
|
||||
RegionID: mysqlstorage.Int64(payload, "region_id"),
|
||||
StakeCoin: mysqlstorage.Int64(payload, "stake_coin"),
|
||||
Status: mysqlstorage.String(payload, "status"),
|
||||
|
||||
@ -65,6 +65,49 @@ func TestQueryOverviewUsesActivityLuckyGiftDayStats(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsumeUserRegisteredCountsOnlyNewRegistration(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
_, file, _, _ := runtime.Caller(0)
|
||||
statsSchema := mysqlschema.New(t, mysqlschema.Config{
|
||||
InitDBPath: mysqlschema.InitDBPath(t, file, "..", "..", "..", "deploy", "mysql", "initdb", "001_statistics_service.sql"),
|
||||
DatabasePrefix: "hy_stats_registration_idempotent_test",
|
||||
})
|
||||
repository, err := Open(ctx, statsSchema.DSN)
|
||||
if err != nil {
|
||||
t.Fatalf("open repository: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = repository.Close() })
|
||||
|
||||
event := UserRegisteredEvent{
|
||||
AppCode: "lalu",
|
||||
EventID: "user_registered:327300172394536960",
|
||||
UserID: 327300172394536960,
|
||||
CountryID: 15,
|
||||
RegionID: 10,
|
||||
OccurredAtMS: time.Date(2026, 6, 22, 1, 2, 3, 0, time.UTC).UnixMilli(),
|
||||
}
|
||||
if err := repository.ConsumeUserRegistered(ctx, event); err != nil {
|
||||
t.Fatalf("consume registration first time: %v", err)
|
||||
}
|
||||
// 生产环境会遇到 MQ 重投或历史补偿;注册 cohort 表去重后,国家日新增只能按真实新行累加。
|
||||
event.EventID = "user_registered:327300172394536960:replay"
|
||||
if err := repository.ConsumeUserRegistered(ctx, event); err != nil {
|
||||
t.Fatalf("consume registration replay: %v", err)
|
||||
}
|
||||
|
||||
var newUsers int64
|
||||
if err := repository.db.QueryRowContext(ctx, `
|
||||
SELECT new_users
|
||||
FROM stat_app_day_country
|
||||
WHERE app_code = 'lalu' AND stat_day = '2026-06-22' AND country_id = 15 AND region_id = 10
|
||||
`).Scan(&newUsers); err != nil {
|
||||
t.Fatalf("query app day country: %v", err)
|
||||
}
|
||||
if newUsers != 1 {
|
||||
t.Fatalf("registration replay must not duplicate new users, got %d", newUsers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryOverviewReturnsPreviousPeriodDeltaRates(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
_, file, _, _ := runtime.Caller(0)
|
||||
|
||||
@ -463,6 +463,7 @@ type SelfGameMatchEvent struct {
|
||||
GameID string
|
||||
PlatformCode string
|
||||
RoomID string
|
||||
CountryID int64
|
||||
RegionID int64
|
||||
StakeCoin int64
|
||||
Status string
|
||||
@ -504,17 +505,20 @@ func (r *Repository) ConsumeUserRegistered(ctx context.Context, event UserRegist
|
||||
if err := ensureAppDay(ctx, tx, event.AppCode, day, countryID, regionID, nowMS); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
inserted, err := insertUnique(ctx, tx, `
|
||||
INSERT IGNORE INTO stat_user_registration (app_code, user_id, registered_day, country_id, region_id, registered_at_ms)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`, appcode.Normalize(event.AppCode), event.UserID, day, countryID, regionID, event.OccurredAtMS); err != nil {
|
||||
`, appcode.Normalize(event.AppCode), event.UserID, day, countryID, regionID, event.OccurredAtMS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
// MQ 重投和补偿任务可能带来新的 event_id,但同一用户注册 cohort 只能入库一次;
|
||||
// new_users 必须按 INSERT IGNORE 的真实插入行数累加,避免回放把国家新增人数重复放大。
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
UPDATE stat_app_day_country
|
||||
SET new_users = new_users + 1, updated_at_ms = ?
|
||||
SET new_users = new_users + ?, updated_at_ms = ?
|
||||
WHERE app_code = ? AND stat_day = ? AND country_id = ? AND region_id = ?
|
||||
`, nowMS, appcode.Normalize(event.AppCode), day, countryID, regionID)
|
||||
`, inserted, nowMS, appcode.Normalize(event.AppCode), day, countryID, regionID)
|
||||
return err
|
||||
})
|
||||
}
|
||||
@ -816,7 +820,7 @@ func (r *Repository) ConsumeSelfGameH5Event(ctx context.Context, event SelfGameH
|
||||
func (r *Repository) ConsumeSelfGameMatch(ctx context.Context, event SelfGameMatchEvent) error {
|
||||
return r.withEvent(ctx, SourceGame, event.EventID, event.EventType, func(tx *sql.Tx, nowMS int64) error {
|
||||
day := statDay(event.OccurredAtMS)
|
||||
countryID, regionID := normalizeDimension(0, event.RegionID)
|
||||
countryID, regionID := normalizeDimension(event.CountryID, event.RegionID)
|
||||
gameID := normalizeShortText(event.GameID)
|
||||
if gameID == "" {
|
||||
gameID = "unknown"
|
||||
|
||||
@ -965,6 +965,7 @@ type LuckyGiftRewardCommand struct {
|
||||
DrawID string
|
||||
RoomID string
|
||||
VisibleRegionID int64
|
||||
CountryID int64
|
||||
GiftID string
|
||||
PoolID string
|
||||
Reason string
|
||||
|
||||
@ -1667,14 +1667,16 @@ func TestCreditLuckyGiftRewardIsIdempotent(t *testing.T) {
|
||||
repository := mysqltest.NewRepository(t)
|
||||
svc := walletservice.New(repository)
|
||||
command := ledger.LuckyGiftRewardCommand{
|
||||
CommandID: "lucky_reward:lucky_draw_1",
|
||||
TargetUserID: 21002,
|
||||
Amount: 188,
|
||||
DrawID: "lucky_draw_1",
|
||||
RoomID: "room-1",
|
||||
GiftID: "rose",
|
||||
PoolID: "super_lucky",
|
||||
Reason: "lucky_gift_reward",
|
||||
CommandID: "lucky_reward:lucky_draw_1",
|
||||
TargetUserID: 21002,
|
||||
Amount: 188,
|
||||
DrawID: "lucky_draw_1",
|
||||
RoomID: "room-1",
|
||||
GiftID: "rose",
|
||||
PoolID: "super_lucky",
|
||||
CountryID: 15,
|
||||
VisibleRegionID: 10,
|
||||
Reason: "lucky_gift_reward",
|
||||
}
|
||||
|
||||
first, err := svc.CreditLuckyGiftReward(context.Background(), command)
|
||||
@ -1697,6 +1699,9 @@ func TestCreditLuckyGiftRewardIsIdempotent(t *testing.T) {
|
||||
if got := repository.CountRows("wallet_outbox", "transaction_id = ? AND event_type = ?", first.TransactionID, "WalletLuckyGiftRewardCredited"); got != 1 {
|
||||
t.Fatalf("lucky reward should write one reward fact event, got %d", got)
|
||||
}
|
||||
if got := repository.CountRows("wallet_outbox", "transaction_id = ? AND event_type = ? AND JSON_EXTRACT(payload_json, '$.country_id') = 15 AND JSON_EXTRACT(payload_json, '$.region_id') = 10", first.TransactionID, "WalletLuckyGiftRewardCredited"); got != 1 {
|
||||
t.Fatalf("lucky reward fact should carry country and region dimensions, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTransferCoinFromSellerMovesDedicatedAssetToPlayerCoin 验证币商专用金币和玩家普通金币不会混账。
|
||||
|
||||
@ -1391,6 +1391,7 @@ func (r *Repository) CreditLuckyGiftReward(ctx context.Context, command ledger.L
|
||||
DrawID: command.DrawID,
|
||||
RoomID: command.RoomID,
|
||||
VisibleRegionID: command.VisibleRegionID,
|
||||
CountryID: command.CountryID,
|
||||
GiftID: command.GiftID,
|
||||
PoolID: command.PoolID,
|
||||
Reason: command.Reason,
|
||||
@ -3388,6 +3389,7 @@ type luckyGiftRewardMetadata struct {
|
||||
DrawID string `json:"draw_id"`
|
||||
RoomID string `json:"room_id"`
|
||||
VisibleRegionID int64 `json:"visible_region_id"`
|
||||
CountryID int64 `json:"country_id"`
|
||||
GiftID string `json:"gift_id"`
|
||||
PoolID string `json:"pool_id"`
|
||||
Reason string `json:"reason"`
|
||||
@ -4102,6 +4104,7 @@ func luckyGiftRewardCreditedEvent(transactionID string, commandID string, metada
|
||||
"room_id": metadata.RoomID,
|
||||
"visible_region_id": metadata.VisibleRegionID,
|
||||
"region_id": metadata.VisibleRegionID,
|
||||
"country_id": metadata.CountryID,
|
||||
"gift_id": metadata.GiftID,
|
||||
"pool_id": metadata.PoolID,
|
||||
"amount": metadata.Amount,
|
||||
@ -4613,12 +4616,14 @@ func taskRewardRequestHash(command ledger.TaskRewardCommand) string {
|
||||
}
|
||||
|
||||
func luckyGiftRewardRequestHash(command ledger.LuckyGiftRewardCommand) string {
|
||||
return stableHash(fmt.Sprintf("lucky_gift_reward|%s|%d|%d|%s|%s|%s|%s|%s",
|
||||
return stableHash(fmt.Sprintf("lucky_gift_reward|%s|%d|%d|%s|%s|%d|%d|%s|%s|%s",
|
||||
appcode.Normalize(command.AppCode),
|
||||
command.TargetUserID,
|
||||
command.Amount,
|
||||
strings.TrimSpace(command.DrawID),
|
||||
strings.TrimSpace(command.RoomID),
|
||||
command.VisibleRegionID,
|
||||
command.CountryID,
|
||||
strings.TrimSpace(command.GiftID),
|
||||
strings.TrimSpace(command.PoolID),
|
||||
strings.TrimSpace(command.Reason),
|
||||
|
||||
@ -1046,6 +1046,7 @@ func (s *Server) CreditLuckyGiftReward(ctx context.Context, req *walletv1.Credit
|
||||
DrawID: req.GetDrawId(),
|
||||
RoomID: req.GetRoomId(),
|
||||
VisibleRegionID: req.GetVisibleRegionId(),
|
||||
CountryID: req.GetCountryId(),
|
||||
GiftID: req.GetGiftId(),
|
||||
PoolID: req.GetPoolId(),
|
||||
Reason: req.GetReason(),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user