幸运礼物
This commit is contained in:
parent
144cd3d781
commit
a0ee5962df
@ -10705,6 +10705,7 @@ type LuckyGiftDrawResult struct {
|
|||||||
HighMultiplier bool `protobuf:"varint,18,opt,name=high_multiplier,json=highMultiplier,proto3" json:"high_multiplier,omitempty"`
|
HighMultiplier bool `protobuf:"varint,18,opt,name=high_multiplier,json=highMultiplier,proto3" json:"high_multiplier,omitempty"`
|
||||||
CreatedAtMs int64 `protobuf:"varint,19,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"`
|
CreatedAtMs int64 `protobuf:"varint,19,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"`
|
||||||
PoolId string `protobuf:"bytes,20,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"`
|
PoolId string `protobuf:"bytes,20,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"`
|
||||||
|
MultiplierPpm int64 `protobuf:"varint,21,opt,name=multiplier_ppm,json=multiplierPpm,proto3" json:"multiplier_ppm,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -10879,6 +10880,13 @@ func (x *LuckyGiftDrawResult) GetPoolId() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *LuckyGiftDrawResult) GetMultiplierPpm() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.MultiplierPpm
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
type ExecuteLuckyGiftDrawRequest struct {
|
type ExecuteLuckyGiftDrawRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
LuckyGift *LuckyGiftMeta `protobuf:"bytes,1,opt,name=lucky_gift,json=luckyGift,proto3" json:"lucky_gift,omitempty"`
|
LuckyGift *LuckyGiftMeta `protobuf:"bytes,1,opt,name=lucky_gift,json=luckyGift,proto3" json:"lucky_gift,omitempty"`
|
||||||
@ -12673,7 +12681,7 @@ const file_proto_activity_v1_activity_proto_rawDesc = "" +
|
|||||||
"\frule_version\x18\x05 \x01(\x03R\vruleVersion\x12$\n" +
|
"\frule_version\x18\x05 \x01(\x03R\vruleVersion\x12$\n" +
|
||||||
"\x0etarget_rtp_ppm\x18\x06 \x01(\x03R\ftargetRtpPpm\x12'\n" +
|
"\x0etarget_rtp_ppm\x18\x06 \x01(\x03R\ftargetRtpPpm\x12'\n" +
|
||||||
"\x0fexperience_pool\x18\a \x01(\tR\x0eexperiencePool\x12\x17\n" +
|
"\x0fexperience_pool\x18\a \x01(\tR\x0eexperiencePool\x12\x17\n" +
|
||||||
"\apool_id\x18\b \x01(\tR\x06poolId\"\xce\x06\n" +
|
"\apool_id\x18\b \x01(\tR\x06poolId\"\xf5\x06\n" +
|
||||||
"\x13LuckyGiftDrawResult\x12\x17\n" +
|
"\x13LuckyGiftDrawResult\x12\x17\n" +
|
||||||
"\adraw_id\x18\x01 \x01(\tR\x06drawId\x12\x1d\n" +
|
"\adraw_id\x18\x01 \x01(\tR\x06drawId\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
@ -12696,7 +12704,8 @@ const file_proto_activity_v1_activity_proto_rawDesc = "" +
|
|||||||
"\x0estage_feedback\x18\x11 \x01(\bR\rstageFeedback\x12'\n" +
|
"\x0estage_feedback\x18\x11 \x01(\bR\rstageFeedback\x12'\n" +
|
||||||
"\x0fhigh_multiplier\x18\x12 \x01(\bR\x0ehighMultiplier\x12\"\n" +
|
"\x0fhigh_multiplier\x18\x12 \x01(\bR\x0ehighMultiplier\x12\"\n" +
|
||||||
"\rcreated_at_ms\x18\x13 \x01(\x03R\vcreatedAtMs\x12\x17\n" +
|
"\rcreated_at_ms\x18\x13 \x01(\x03R\vcreatedAtMs\x12\x17\n" +
|
||||||
"\apool_id\x18\x14 \x01(\tR\x06poolId\"^\n" +
|
"\apool_id\x18\x14 \x01(\tR\x06poolId\x12%\n" +
|
||||||
|
"\x0emultiplier_ppm\x18\x15 \x01(\x03R\rmultiplierPpm\"^\n" +
|
||||||
"\x1bExecuteLuckyGiftDrawRequest\x12?\n" +
|
"\x1bExecuteLuckyGiftDrawRequest\x12?\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"lucky_gift\x18\x01 \x01(\v2 .hyapp.activity.v1.LuckyGiftMetaR\tluckyGift\"^\n" +
|
"lucky_gift\x18\x01 \x01(\v2 .hyapp.activity.v1.LuckyGiftMetaR\tluckyGift\"^\n" +
|
||||||
|
|||||||
@ -1218,6 +1218,7 @@ message LuckyGiftDrawResult {
|
|||||||
bool high_multiplier = 18;
|
bool high_multiplier = 18;
|
||||||
int64 created_at_ms = 19;
|
int64 created_at_ms = 19;
|
||||||
string pool_id = 20;
|
string pool_id = 20;
|
||||||
|
int64 multiplier_ppm = 21;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ExecuteLuckyGiftDrawRequest {
|
message ExecuteLuckyGiftDrawRequest {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -63,6 +63,27 @@ message RankItem {
|
|||||||
int64 updated_at_ms = 4;
|
int64 updated_at_ms = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LuckyGiftDrawResult 是 SendGift 同步返回给当前送礼用户的幸运礼物抽奖表现。
|
||||||
|
message LuckyGiftDrawResult {
|
||||||
|
bool enabled = 1;
|
||||||
|
string draw_id = 2;
|
||||||
|
string command_id = 3;
|
||||||
|
string pool_id = 4;
|
||||||
|
string gift_id = 5;
|
||||||
|
int64 rule_version = 6;
|
||||||
|
string experience_pool = 7;
|
||||||
|
string selected_tier_id = 8;
|
||||||
|
int64 multiplier_ppm = 9;
|
||||||
|
int64 base_reward_coins = 10;
|
||||||
|
int64 room_atmosphere_reward_coins = 11;
|
||||||
|
int64 activity_subsidy_coins = 12;
|
||||||
|
int64 effective_reward_coins = 13;
|
||||||
|
string reward_status = 14;
|
||||||
|
bool stage_feedback = 15;
|
||||||
|
bool high_multiplier = 16;
|
||||||
|
int64 created_at_ms = 17;
|
||||||
|
}
|
||||||
|
|
||||||
// RoomTreasureRewardItem 是后台配置给客户端展示的宝箱奖励候选项。
|
// RoomTreasureRewardItem 是后台配置给客户端展示的宝箱奖励候选项。
|
||||||
message RoomTreasureRewardItem {
|
message RoomTreasureRewardItem {
|
||||||
string reward_item_id = 1;
|
string reward_item_id = 1;
|
||||||
@ -695,6 +716,7 @@ message SendGiftResponse {
|
|||||||
repeated RankItem gift_rank = 4;
|
repeated RankItem gift_rank = 4;
|
||||||
RoomSnapshot room = 5;
|
RoomSnapshot room = 5;
|
||||||
RoomTreasureState treasure = 6;
|
RoomTreasureState treasure = 6;
|
||||||
|
LuckyGiftDrawResult lucky_gift = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckSpeakPermissionRequest 让腾讯云 IM 发言回调或 gateway 在公屏前同步问房间业务态。
|
// CheckSpeakPermissionRequest 让腾讯云 IM 发言回调或 gateway 在公屏前同步问房间业务态。
|
||||||
|
|||||||
@ -88,6 +88,7 @@ type drawDTO struct {
|
|||||||
RuleVersion int64 `json:"rule_version"`
|
RuleVersion int64 `json:"rule_version"`
|
||||||
ExperiencePool string `json:"experience_pool"`
|
ExperiencePool string `json:"experience_pool"`
|
||||||
SelectedTierID string `json:"selected_tier_id"`
|
SelectedTierID string `json:"selected_tier_id"`
|
||||||
|
MultiplierPPM int64 `json:"multiplier_ppm"`
|
||||||
BaseRewardCoins int64 `json:"base_reward_coins"`
|
BaseRewardCoins int64 `json:"base_reward_coins"`
|
||||||
RoomAtmosphereRewardCoins int64 `json:"room_atmosphere_reward_coins"`
|
RoomAtmosphereRewardCoins int64 `json:"room_atmosphere_reward_coins"`
|
||||||
ActivitySubsidyCoins int64 `json:"activity_subsidy_coins"`
|
ActivitySubsidyCoins int64 `json:"activity_subsidy_coins"`
|
||||||
@ -346,6 +347,7 @@ func drawFromProto(draw *activityv1.LuckyGiftDrawResult) drawDTO {
|
|||||||
RuleVersion: draw.GetRuleVersion(),
|
RuleVersion: draw.GetRuleVersion(),
|
||||||
ExperiencePool: draw.GetExperiencePool(),
|
ExperiencePool: draw.GetExperiencePool(),
|
||||||
SelectedTierID: draw.GetSelectedTierId(),
|
SelectedTierID: draw.GetSelectedTierId(),
|
||||||
|
MultiplierPPM: draw.GetMultiplierPpm(),
|
||||||
BaseRewardCoins: draw.GetBaseRewardCoins(),
|
BaseRewardCoins: draw.GetBaseRewardCoins(),
|
||||||
RoomAtmosphereRewardCoins: draw.GetRoomAtmosphereRewardCoins(),
|
RoomAtmosphereRewardCoins: draw.GetRoomAtmosphereRewardCoins(),
|
||||||
ActivitySubsidyCoins: draw.GetActivitySubsidyCoins(),
|
ActivitySubsidyCoins: draw.GetActivitySubsidyCoins(),
|
||||||
|
|||||||
@ -860,3 +860,30 @@ CREATE TABLE IF NOT EXISTS message_fanout_jobs (
|
|||||||
UNIQUE KEY uk_message_fanout_command (app_code, command_id),
|
UNIQUE KEY uk_message_fanout_command (app_code, command_id),
|
||||||
KEY idx_message_fanout_status (app_code, status, next_run_at_ms, updated_at_ms)
|
KEY idx_message_fanout_status (app_code, status, next_run_at_ms, updated_at_ms)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='消息分发任务表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='消息分发任务表';
|
||||||
|
|
||||||
|
-- 本地开发必须开箱即可验证幸运礼物链路;INSERT IGNORE 只补缺省奖池,不覆盖后台已发布规则。
|
||||||
|
SET @lucky_seed_now_ms := 1779259000000;
|
||||||
|
SET @lucky_seed_tiers := JSON_ARRAY(
|
||||||
|
JSON_OBJECT('pool', 'novice', 'tier_id', 'novice_none', 'reward_coins', 0, 'multiplier_ppm', 0, 'weight', 0, 'high_water_only', false, 'enabled', true),
|
||||||
|
JSON_OBJECT('pool', 'novice', 'tier_id', 'novice_1x', 'reward_coins', 100, 'multiplier_ppm', 1000000, 'weight', 0, 'high_water_only', false, 'enabled', true),
|
||||||
|
JSON_OBJECT('pool', 'novice', 'tier_id', 'novice_2x', 'reward_coins', 200, 'multiplier_ppm', 2000000, 'weight', 0, 'high_water_only', false, 'enabled', true),
|
||||||
|
JSON_OBJECT('pool', 'intermediate', 'tier_id', 'intermediate_none', 'reward_coins', 0, 'multiplier_ppm', 0, 'weight', 0, 'high_water_only', false, 'enabled', true),
|
||||||
|
JSON_OBJECT('pool', 'intermediate', 'tier_id', 'intermediate_1x', 'reward_coins', 100, 'multiplier_ppm', 1000000, 'weight', 0, 'high_water_only', false, 'enabled', true),
|
||||||
|
JSON_OBJECT('pool', 'intermediate', 'tier_id', 'intermediate_2x', 'reward_coins', 200, 'multiplier_ppm', 2000000, 'weight', 0, 'high_water_only', false, 'enabled', true),
|
||||||
|
JSON_OBJECT('pool', 'advanced', 'tier_id', 'advanced_none', 'reward_coins', 0, 'multiplier_ppm', 0, 'weight', 0, 'high_water_only', false, 'enabled', true),
|
||||||
|
JSON_OBJECT('pool', 'advanced', 'tier_id', 'advanced_1x', 'reward_coins', 100, 'multiplier_ppm', 1000000, 'weight', 0, 'high_water_only', false, 'enabled', true),
|
||||||
|
JSON_OBJECT('pool', 'advanced', 'tier_id', 'advanced_2x', 'reward_coins', 200, 'multiplier_ppm', 2000000, 'weight', 0, 'high_water_only', false, 'enabled', true)
|
||||||
|
);
|
||||||
|
INSERT IGNORE INTO lucky_gift_rules (
|
||||||
|
app_code, gift_id, enabled, rule_version, gift_price, target_rtp_ppm, pool_rate_ppm,
|
||||||
|
global_window_draws, gift_window_draws, novice_draw_limit, intermediate_draw_limit,
|
||||||
|
high_multiplier, high_water_pool_multiple, platform_pool_weight_ppm, room_pool_weight_ppm,
|
||||||
|
gift_pool_weight_ppm, initial_platform_pool, initial_gift_pool, initial_room_pool,
|
||||||
|
platform_reserve, gift_reserve, room_reserve, max_single_payout, user_hourly_payout_cap,
|
||||||
|
user_daily_payout_cap, device_daily_payout_cap, room_hourly_payout_cap, anchor_daily_payout_cap,
|
||||||
|
room_atmosphere_rate_ppm, room_atmosphere_initial, room_atmosphere_reserve,
|
||||||
|
activity_budget, activity_daily_limit, large_tier_enabled, tiers_json,
|
||||||
|
updated_by_admin_id, created_at_ms, updated_at_ms
|
||||||
|
) VALUES
|
||||||
|
('lalu', 'lucky', true, 1, 100, 950000, 950000, 100000, 100000, 2000, 20000, 100, 2, 200000, 300000, 500000, 9500000, 23750000, 5000000, 1000000, 1000000, 100000, 50000, 3420000, 61560000, 102600000, 68400000, 1231200000, 10000, 100000, 10000, 0, 0, true, @lucky_seed_tiers, 0, @lucky_seed_now_ms, @lucky_seed_now_ms),
|
||||||
|
('lalu', 'super_lucky', true, 1, 100, 950000, 950000, 100000, 100000, 2000, 20000, 100, 2, 200000, 300000, 500000, 9500000, 23750000, 5000000, 1000000, 1000000, 100000, 50000, 3420000, 61560000, 102600000, 68400000, 1231200000, 10000, 100000, 10000, 0, 0, true, @lucky_seed_tiers, 0, @lucky_seed_now_ms, @lucky_seed_now_ms);
|
||||||
|
|||||||
@ -112,6 +112,7 @@ type DrawResult struct {
|
|||||||
RuleVersion int64
|
RuleVersion int64
|
||||||
ExperiencePool string
|
ExperiencePool string
|
||||||
SelectedTierID string
|
SelectedTierID string
|
||||||
|
MultiplierPPM int64
|
||||||
BaseRewardCoins int64
|
BaseRewardCoins int64
|
||||||
RoomAtmosphereRewardCoins int64
|
RoomAtmosphereRewardCoins int64
|
||||||
ActivitySubsidyCoins int64
|
ActivitySubsidyCoins int64
|
||||||
|
|||||||
@ -415,6 +415,7 @@ func (r *Repository) ExecuteLuckyGiftDraw(ctx context.Context, cmd domain.DrawCo
|
|||||||
RuleVersion: config.RuleVersion,
|
RuleVersion: config.RuleVersion,
|
||||||
ExperiencePool: experiencePool,
|
ExperiencePool: experiencePool,
|
||||||
SelectedTierID: candidate.TierID,
|
SelectedTierID: candidate.TierID,
|
||||||
|
MultiplierPPM: candidate.MultiplierPPM,
|
||||||
BaseRewardCoins: candidate.BaseReward,
|
BaseRewardCoins: candidate.BaseReward,
|
||||||
RoomAtmosphereRewardCoins: candidate.RoomReward,
|
RoomAtmosphereRewardCoins: candidate.RoomReward,
|
||||||
ActivitySubsidyCoins: candidate.ActivityReward,
|
ActivitySubsidyCoins: candidate.ActivityReward,
|
||||||
@ -604,6 +605,7 @@ func (r *Repository) ListLuckyGiftDraws(ctx context.Context, query domain.DrawQu
|
|||||||
args = append(args, limit, offset)
|
args = append(args, limit, offset)
|
||||||
rows, err := r.db.QueryContext(ctx, `
|
rows, err := r.db.QueryContext(ctx, `
|
||||||
SELECT draw_id, command_id, pool_id, gift_id, rule_version, experience_pool, selected_tier_id,
|
SELECT draw_id, command_id, pool_id, gift_id, rule_version, experience_pool, selected_tier_id,
|
||||||
|
COALESCE(JSON_UNQUOTE(JSON_EXTRACT(candidate_tiers_json, '$.multiplier_ppm')), '0'),
|
||||||
base_reward_coins, room_atmosphere_reward_coins, activity_subsidy_coins,
|
base_reward_coins, room_atmosphere_reward_coins, activity_subsidy_coins,
|
||||||
effective_reward_coins, COALESCE(CAST(budget_sources_json AS CHAR), ''), reward_status,
|
effective_reward_coins, COALESCE(CAST(budget_sources_json AS CHAR), ''), reward_status,
|
||||||
rtp_window_index, gift_rtp_window_index, stage_feedback, high_multiplier, created_at_ms
|
rtp_window_index, gift_rtp_window_index, stage_feedback, high_multiplier, created_at_ms
|
||||||
@ -619,6 +621,7 @@ func (r *Repository) ListLuckyGiftDraws(ctx context.Context, query domain.DrawQu
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var item domain.DrawResult
|
var item domain.DrawResult
|
||||||
if err := rows.Scan(&item.DrawID, &item.CommandID, &item.PoolID, &item.GiftID, &item.RuleVersion, &item.ExperiencePool, &item.SelectedTierID,
|
if err := rows.Scan(&item.DrawID, &item.CommandID, &item.PoolID, &item.GiftID, &item.RuleVersion, &item.ExperiencePool, &item.SelectedTierID,
|
||||||
|
&item.MultiplierPPM,
|
||||||
&item.BaseRewardCoins, &item.RoomAtmosphereRewardCoins, &item.ActivitySubsidyCoins, &item.EffectiveRewardCoins,
|
&item.BaseRewardCoins, &item.RoomAtmosphereRewardCoins, &item.ActivitySubsidyCoins, &item.EffectiveRewardCoins,
|
||||||
&item.BudgetSourcesJSON, &item.RewardStatus, &item.RTPWindowIndex, &item.GiftRTPWindowIndex,
|
&item.BudgetSourcesJSON, &item.RewardStatus, &item.RTPWindowIndex, &item.GiftRTPWindowIndex,
|
||||||
&item.StageFeedback, &item.HighMultiplier, &item.CreatedAtMS); err != nil {
|
&item.StageFeedback, &item.HighMultiplier, &item.CreatedAtMS); err != nil {
|
||||||
@ -1256,6 +1259,7 @@ func (r *Repository) getLuckyDrawByCommand(ctx context.Context, tx *sql.Tx, appC
|
|||||||
}
|
}
|
||||||
row := tx.QueryRowContext(ctx, `
|
row := tx.QueryRowContext(ctx, `
|
||||||
SELECT draw_id, command_id, pool_id, gift_id, rule_version, experience_pool, selected_tier_id,
|
SELECT draw_id, command_id, pool_id, gift_id, rule_version, experience_pool, selected_tier_id,
|
||||||
|
COALESCE(JSON_UNQUOTE(JSON_EXTRACT(candidate_tiers_json, '$.multiplier_ppm')), '0'),
|
||||||
base_reward_coins, room_atmosphere_reward_coins, activity_subsidy_coins,
|
base_reward_coins, room_atmosphere_reward_coins, activity_subsidy_coins,
|
||||||
effective_reward_coins, COALESCE(CAST(budget_sources_json AS CHAR), ''), reward_status,
|
effective_reward_coins, COALESCE(CAST(budget_sources_json AS CHAR), ''), reward_status,
|
||||||
rtp_window_index, gift_rtp_window_index, stage_feedback, high_multiplier, created_at_ms
|
rtp_window_index, gift_rtp_window_index, stage_feedback, high_multiplier, created_at_ms
|
||||||
@ -1265,6 +1269,7 @@ func (r *Repository) getLuckyDrawByCommand(ctx context.Context, tx *sql.Tx, appC
|
|||||||
)
|
)
|
||||||
var item domain.DrawResult
|
var item domain.DrawResult
|
||||||
if err := row.Scan(&item.DrawID, &item.CommandID, &item.PoolID, &item.GiftID, &item.RuleVersion, &item.ExperiencePool, &item.SelectedTierID,
|
if err := row.Scan(&item.DrawID, &item.CommandID, &item.PoolID, &item.GiftID, &item.RuleVersion, &item.ExperiencePool, &item.SelectedTierID,
|
||||||
|
&item.MultiplierPPM,
|
||||||
&item.BaseRewardCoins, &item.RoomAtmosphereRewardCoins, &item.ActivitySubsidyCoins, &item.EffectiveRewardCoins,
|
&item.BaseRewardCoins, &item.RoomAtmosphereRewardCoins, &item.ActivitySubsidyCoins, &item.EffectiveRewardCoins,
|
||||||
&item.BudgetSourcesJSON, &item.RewardStatus, &item.RTPWindowIndex, &item.GiftRTPWindowIndex,
|
&item.BudgetSourcesJSON, &item.RewardStatus, &item.RTPWindowIndex, &item.GiftRTPWindowIndex,
|
||||||
&item.StageFeedback, &item.HighMultiplier, &item.CreatedAtMS); err != nil {
|
&item.StageFeedback, &item.HighMultiplier, &item.CreatedAtMS); err != nil {
|
||||||
|
|||||||
@ -270,6 +270,7 @@ func luckyDrawResultToProto(result domain.DrawResult) *activityv1.LuckyGiftDrawR
|
|||||||
RuleVersion: result.RuleVersion,
|
RuleVersion: result.RuleVersion,
|
||||||
ExperiencePool: result.ExperiencePool,
|
ExperiencePool: result.ExperiencePool,
|
||||||
SelectedTierId: result.SelectedTierID,
|
SelectedTierId: result.SelectedTierID,
|
||||||
|
MultiplierPpm: result.MultiplierPPM,
|
||||||
BaseRewardCoins: result.BaseRewardCoins,
|
BaseRewardCoins: result.BaseRewardCoins,
|
||||||
RoomAtmosphereRewardCoins: result.RoomAtmosphereRewardCoins,
|
RoomAtmosphereRewardCoins: result.RoomAtmosphereRewardCoins,
|
||||||
ActivitySubsidyCoins: result.ActivitySubsidyCoins,
|
ActivitySubsidyCoins: result.ActivitySubsidyCoins,
|
||||||
|
|||||||
@ -66,6 +66,8 @@ type UserCountryQueryClient interface {
|
|||||||
|
|
||||||
// UserHostClient 抽象 gateway 对 user-service host domain 身份查询能力的依赖。
|
// UserHostClient 抽象 gateway 对 user-service host domain 身份查询能力的依赖。
|
||||||
type UserHostClient interface {
|
type UserHostClient interface {
|
||||||
|
SearchAgencies(ctx context.Context, req *userv1.SearchAgenciesRequest) (*userv1.SearchAgenciesResponse, error)
|
||||||
|
ApplyToAgency(ctx context.Context, req *userv1.ApplyToAgencyRequest) (*userv1.ApplyToAgencyResponse, error)
|
||||||
GetHostProfile(ctx context.Context, req *userv1.GetHostProfileRequest) (*userv1.GetHostProfileResponse, error)
|
GetHostProfile(ctx context.Context, req *userv1.GetHostProfileRequest) (*userv1.GetHostProfileResponse, error)
|
||||||
GetBDProfile(ctx context.Context, req *userv1.GetBDProfileRequest) (*userv1.GetBDProfileResponse, error)
|
GetBDProfile(ctx context.Context, req *userv1.GetBDProfileRequest) (*userv1.GetBDProfileResponse, error)
|
||||||
GetCoinSellerProfile(ctx context.Context, req *userv1.GetCoinSellerProfileRequest) (*userv1.GetCoinSellerProfileResponse, error)
|
GetCoinSellerProfile(ctx context.Context, req *userv1.GetCoinSellerProfileRequest) (*userv1.GetCoinSellerProfileResponse, error)
|
||||||
@ -294,6 +296,14 @@ func (c *grpcUserCountryQueryClient) ListLoginRiskBlockedCountries(ctx context.C
|
|||||||
return c.client.ListLoginRiskBlockedCountries(ctx, req)
|
return c.client.ListLoginRiskBlockedCountries(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *grpcUserHostClient) SearchAgencies(ctx context.Context, req *userv1.SearchAgenciesRequest) (*userv1.SearchAgenciesResponse, error) {
|
||||||
|
return c.client.SearchAgencies(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *grpcUserHostClient) ApplyToAgency(ctx context.Context, req *userv1.ApplyToAgencyRequest) (*userv1.ApplyToAgencyResponse, error) {
|
||||||
|
return c.client.ApplyToAgency(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *grpcUserHostClient) GetHostProfile(ctx context.Context, req *userv1.GetHostProfileRequest) (*userv1.GetHostProfileResponse, error) {
|
func (c *grpcUserHostClient) GetHostProfile(ctx context.Context, req *userv1.GetHostProfileRequest) (*userv1.GetHostProfileResponse, error) {
|
||||||
return c.client.GetHostProfile(ctx, req)
|
return c.client.GetHostProfile(ctx, req)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -221,6 +221,10 @@ func RequireCompletedProfile(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
// RoomMeta 把外部 HTTP 请求上下文转换成 room-service 命令元信息。
|
// RoomMeta 把外部 HTTP 请求上下文转换成 room-service 命令元信息。
|
||||||
// request_id 只做链路追踪;command_id 必须来自客户端用户动作,gateway 只裁剪空白。
|
// request_id 只做链路追踪;command_id 必须来自客户端用户动作,gateway 只裁剪空白。
|
||||||
func RoomMeta(request *http.Request, roomID string, commandID string) *roomv1.RequestMeta {
|
func RoomMeta(request *http.Request, roomID string, commandID string) *roomv1.RequestMeta {
|
||||||
|
sessionID := auth.SessionIDFromContext(request.Context())
|
||||||
|
if sessionID == "" {
|
||||||
|
sessionID = idgen.New("sess")
|
||||||
|
}
|
||||||
return &roomv1.RequestMeta{
|
return &roomv1.RequestMeta{
|
||||||
RequestId: RequestIDFromContext(request.Context()),
|
RequestId: RequestIDFromContext(request.Context()),
|
||||||
CommandId: strings.TrimSpace(commandID),
|
CommandId: strings.TrimSpace(commandID),
|
||||||
@ -228,7 +232,7 @@ func RoomMeta(request *http.Request, roomID string, commandID string) *roomv1.Re
|
|||||||
RoomId: roomID,
|
RoomId: roomID,
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
GatewayNodeId: "gateway-local",
|
GatewayNodeId: "gateway-local",
|
||||||
SessionId: idgen.New("sess"),
|
SessionId: sessionID,
|
||||||
SentAtMs: time.Now().UnixMilli(),
|
SentAtMs: time.Now().UnixMilli(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,6 +67,8 @@ type UserHandlers struct {
|
|||||||
GetMyIdentity http.HandlerFunc
|
GetMyIdentity http.HandlerFunc
|
||||||
GetMyHostIdentity http.HandlerFunc
|
GetMyHostIdentity http.HandlerFunc
|
||||||
GetMyRoleSummary http.HandlerFunc
|
GetMyRoleSummary http.HandlerFunc
|
||||||
|
SearchHostAgencies http.HandlerFunc
|
||||||
|
ApplyToHostAgency http.HandlerFunc
|
||||||
CompleteMyOnboarding http.HandlerFunc
|
CompleteMyOnboarding http.HandlerFunc
|
||||||
UpdateMyProfile http.HandlerFunc
|
UpdateMyProfile http.HandlerFunc
|
||||||
ChangeMyCountry http.HandlerFunc
|
ChangeMyCountry http.HandlerFunc
|
||||||
@ -268,6 +270,8 @@ func (r routes) registerUserRoutes() {
|
|||||||
r.auth("/users/me/identity", "", h.GetMyIdentity)
|
r.auth("/users/me/identity", "", h.GetMyIdentity)
|
||||||
r.profile("/users/me/host-identity", "", h.GetMyHostIdentity)
|
r.profile("/users/me/host-identity", "", h.GetMyHostIdentity)
|
||||||
r.profile("/users/me/role-summary", "", h.GetMyRoleSummary)
|
r.profile("/users/me/role-summary", "", h.GetMyRoleSummary)
|
||||||
|
r.profile("/host/agencies/search", http.MethodGet, h.SearchHostAgencies)
|
||||||
|
r.profile("/host/agency-applications", http.MethodPost, h.ApplyToHostAgency)
|
||||||
r.auth("/users/me/onboarding/complete", "", h.CompleteMyOnboarding)
|
r.auth("/users/me/onboarding/complete", "", h.CompleteMyOnboarding)
|
||||||
r.profile("/users/me/profile/update", "", h.UpdateMyProfile)
|
r.profile("/users/me/profile/update", "", h.UpdateMyProfile)
|
||||||
r.profile("/users/me/country/change", "", h.ChangeMyCountry)
|
r.profile("/users/me/country/change", "", h.ChangeMyCountry)
|
||||||
|
|||||||
@ -52,6 +52,7 @@ type fakeRoomClient struct {
|
|||||||
lastKick *roomv1.KickUserRequest
|
lastKick *roomv1.KickUserRequest
|
||||||
lastUnban *roomv1.UnbanUserRequest
|
lastUnban *roomv1.UnbanUserRequest
|
||||||
lastGift *roomv1.SendGiftRequest
|
lastGift *roomv1.SendGiftRequest
|
||||||
|
sendGiftResp *roomv1.SendGiftResponse
|
||||||
lastFollowRoom *roomv1.FollowRoomRequest
|
lastFollowRoom *roomv1.FollowRoomRequest
|
||||||
lastUnfollowRoom *roomv1.UnfollowRoomRequest
|
lastUnfollowRoom *roomv1.UnfollowRoomRequest
|
||||||
createErr error
|
createErr error
|
||||||
@ -219,6 +220,9 @@ func (f *fakeRoomClient) UnbanUser(_ context.Context, req *roomv1.UnbanUserReque
|
|||||||
|
|
||||||
func (f *fakeRoomClient) SendGift(_ context.Context, req *roomv1.SendGiftRequest) (*roomv1.SendGiftResponse, error) {
|
func (f *fakeRoomClient) SendGift(_ context.Context, req *roomv1.SendGiftRequest) (*roomv1.SendGiftResponse, error) {
|
||||||
f.lastGift = req
|
f.lastGift = req
|
||||||
|
if f.sendGiftResp != nil {
|
||||||
|
return f.sendGiftResp, nil
|
||||||
|
}
|
||||||
return &roomv1.SendGiftResponse{Result: &roomv1.CommandResult{Applied: true, RoomVersion: 14}}, nil
|
return &roomv1.SendGiftResponse{Result: &roomv1.CommandResult{Applied: true, RoomVersion: 14}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -315,21 +319,27 @@ type fakeUserCountryQueryClient struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type fakeUserHostClient struct {
|
type fakeUserHostClient struct {
|
||||||
last *userv1.GetCoinSellerProfileRequest
|
lastSearchAgencies *userv1.SearchAgenciesRequest
|
||||||
profile *userv1.CoinSellerProfile
|
searchAgenciesResp *userv1.SearchAgenciesResponse
|
||||||
err error
|
searchAgenciesErr error
|
||||||
lastHost *userv1.GetHostProfileRequest
|
lastApplyAgency *userv1.ApplyToAgencyRequest
|
||||||
hostProfile *userv1.HostProfile
|
applyAgencyResp *userv1.ApplyToAgencyResponse
|
||||||
hostErr error
|
applyAgencyErr error
|
||||||
lastBD *userv1.GetBDProfileRequest
|
last *userv1.GetCoinSellerProfileRequest
|
||||||
bdProfile *userv1.BDProfile
|
profile *userv1.CoinSellerProfile
|
||||||
bdErr error
|
err error
|
||||||
lastRoleSummary *userv1.GetUserRoleSummaryRequest
|
lastHost *userv1.GetHostProfileRequest
|
||||||
roleSummary *userv1.UserRoleSummary
|
hostProfile *userv1.HostProfile
|
||||||
roleSummaryErr error
|
hostErr error
|
||||||
lastCapability *userv1.CheckBusinessCapabilityRequest
|
lastBD *userv1.GetBDProfileRequest
|
||||||
capabilityResp *userv1.CheckBusinessCapabilityResponse
|
bdProfile *userv1.BDProfile
|
||||||
capabilityErr error
|
bdErr error
|
||||||
|
lastRoleSummary *userv1.GetUserRoleSummaryRequest
|
||||||
|
roleSummary *userv1.UserRoleSummary
|
||||||
|
roleSummaryErr error
|
||||||
|
lastCapability *userv1.CheckBusinessCapabilityRequest
|
||||||
|
capabilityResp *userv1.CheckBusinessCapabilityResponse
|
||||||
|
capabilityErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
type fakeWalletClient struct {
|
type fakeWalletClient struct {
|
||||||
@ -903,6 +913,28 @@ func (f *fakeBroadcastClient) RemoveRegionBroadcastMember(_ context.Context, req
|
|||||||
return &activityv1.RemoveRegionBroadcastMemberResponse{GroupId: "hy_lalu_bc_r_1001", Removed: true}, nil
|
return &activityv1.RemoveRegionBroadcastMemberResponse{GroupId: "hy_lalu_bc_r_1001", Removed: true}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *fakeUserHostClient) SearchAgencies(_ context.Context, req *userv1.SearchAgenciesRequest) (*userv1.SearchAgenciesResponse, error) {
|
||||||
|
f.lastSearchAgencies = req
|
||||||
|
if f.searchAgenciesErr != nil {
|
||||||
|
return nil, f.searchAgenciesErr
|
||||||
|
}
|
||||||
|
if f.searchAgenciesResp != nil {
|
||||||
|
return f.searchAgenciesResp, nil
|
||||||
|
}
|
||||||
|
return &userv1.SearchAgenciesResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeUserHostClient) ApplyToAgency(_ context.Context, req *userv1.ApplyToAgencyRequest) (*userv1.ApplyToAgencyResponse, error) {
|
||||||
|
f.lastApplyAgency = req
|
||||||
|
if f.applyAgencyErr != nil {
|
||||||
|
return nil, f.applyAgencyErr
|
||||||
|
}
|
||||||
|
if f.applyAgencyResp != nil {
|
||||||
|
return f.applyAgencyResp, nil
|
||||||
|
}
|
||||||
|
return &userv1.ApplyToAgencyResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (f *fakeUserHostClient) GetHostProfile(_ context.Context, req *userv1.GetHostProfileRequest) (*userv1.GetHostProfileResponse, error) {
|
func (f *fakeUserHostClient) GetHostProfile(_ context.Context, req *userv1.GetHostProfileRequest) (*userv1.GetHostProfileResponse, error) {
|
||||||
f.lastHost = req
|
f.lastHost = req
|
||||||
if f.hostErr != nil {
|
if f.hostErr != nil {
|
||||||
@ -1528,6 +1560,55 @@ func TestRoomCommandsPropagateClientCommandID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSendGiftResponseIncludesLuckyGiftDraw(t *testing.T) {
|
||||||
|
roomClient := &fakeRoomClient{sendGiftResp: &roomv1.SendGiftResponse{
|
||||||
|
Result: &roomv1.CommandResult{Applied: true, RoomVersion: 14, ServerTimeMs: 1_779_258_000_000},
|
||||||
|
BillingReceiptId: "receipt-lucky",
|
||||||
|
RoomHeat: 100,
|
||||||
|
LuckyGift: &roomv1.LuckyGiftDrawResult{
|
||||||
|
Enabled: true,
|
||||||
|
DrawId: "lucky_draw_test",
|
||||||
|
CommandId: "cmd-gift-lucky",
|
||||||
|
PoolId: "super_lucky",
|
||||||
|
GiftId: "rose",
|
||||||
|
RuleVersion: 12,
|
||||||
|
ExperiencePool: "novice",
|
||||||
|
SelectedTierId: "novice_2x",
|
||||||
|
MultiplierPpm: 2_000_000,
|
||||||
|
EffectiveRewardCoins: 200,
|
||||||
|
RewardStatus: "pending",
|
||||||
|
CreatedAtMs: 1_779_258_000_000,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
router := NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}).Routes(auth.NewVerifier("secret"))
|
||||||
|
request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/send", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"cmd-gift-lucky","target_user_id":43,"gift_id":"rose","gift_count":1,"pool_id":"super_lucky"}`)))
|
||||||
|
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
var envelope struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Data struct {
|
||||||
|
LuckyGift struct {
|
||||||
|
DrawID string `json:"draw_id"`
|
||||||
|
PoolID string `json:"pool_id"`
|
||||||
|
MultiplierPPM int64 `json:"multiplier_ppm"`
|
||||||
|
EffectiveRewardCoins int64 `json:"effective_reward_coins"`
|
||||||
|
} `json:"lucky_gift"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
|
||||||
|
t.Fatalf("decode response failed: %v body=%s", err, recorder.Body.String())
|
||||||
|
}
|
||||||
|
if envelope.Code != "OK" || envelope.Data.LuckyGift.DrawID != "lucky_draw_test" || envelope.Data.LuckyGift.PoolID != "super_lucky" || envelope.Data.LuckyGift.MultiplierPPM != 2_000_000 || envelope.Data.LuckyGift.EffectiveRewardCoins != 200 {
|
||||||
|
t.Fatalf("lucky_gift response mismatch: %+v", envelope.Data.LuckyGift)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestJoinRoomReturnsInitialDataWithIMGroupRTCTokenAndProfiles(t *testing.T) {
|
func TestJoinRoomReturnsInitialDataWithIMGroupRTCTokenAndProfiles(t *testing.T) {
|
||||||
previousNow := roomapi.TimeNow
|
previousNow := roomapi.TimeNow
|
||||||
roomapi.TimeNow = func() time.Time { return time.Unix(1_700_000_000, 0) }
|
roomapi.TimeNow = func() time.Time { return time.Unix(1_700_000_000, 0) }
|
||||||
@ -2513,6 +2594,77 @@ func TestGetMyHostIdentityTreatsMissingProfilesAsFalse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSearchHostAgenciesUsesAuthenticatedUserAndShortID(t *testing.T) {
|
||||||
|
hostClient := &fakeUserHostClient{searchAgenciesResp: &userv1.SearchAgenciesResponse{Agencies: []*userv1.Agency{
|
||||||
|
{
|
||||||
|
AgencyId: 7001,
|
||||||
|
OwnerUserId: 901,
|
||||||
|
RegionId: 30,
|
||||||
|
Name: "Admin Seed Agency",
|
||||||
|
Status: "active",
|
||||||
|
JoinEnabled: true,
|
||||||
|
MaxHosts: 100,
|
||||||
|
CreatedAtMs: 1700000000000,
|
||||||
|
UpdatedAtMs: 1700000001000,
|
||||||
|
},
|
||||||
|
}}}
|
||||||
|
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
||||||
|
handler.SetUserHostClient(hostClient)
|
||||||
|
router := handler.Routes(auth.NewVerifier("secret"))
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "/api/v1/host/agencies/search?short_id=900901&page_size=99", nil)
|
||||||
|
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||||
|
request.Header.Set("X-Request-ID", "req-host-agency-search")
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
if hostClient.lastSearchAgencies == nil || hostClient.lastSearchAgencies.GetUserId() != 42 || hostClient.lastSearchAgencies.GetKeyword() != "900901" || hostClient.lastSearchAgencies.GetPageSize() != 20 {
|
||||||
|
t.Fatalf("host agency search request mismatch: %+v", hostClient.lastSearchAgencies)
|
||||||
|
}
|
||||||
|
var response httpkit.ResponseEnvelope
|
||||||
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
|
t.Fatalf("decode response failed: %v", err)
|
||||||
|
}
|
||||||
|
data := response.Data.(map[string]any)
|
||||||
|
items := data["items"].([]any)
|
||||||
|
first := items[0].(map[string]any)
|
||||||
|
if first["agency_id"] != "7001" || first["owner_user_id"] != "901" || first["name"] != "Admin Seed Agency" || data["page_size"] != float64(20) {
|
||||||
|
t.Fatalf("host agency search response mismatch: %+v", response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyToHostAgencyPropagatesClientCommandID(t *testing.T) {
|
||||||
|
hostClient := &fakeUserHostClient{applyAgencyResp: &userv1.ApplyToAgencyResponse{Application: &userv1.AgencyApplication{
|
||||||
|
ApplicationId: 8001,
|
||||||
|
CommandId: "cmd-apply-agency",
|
||||||
|
ApplicantUserId: 42,
|
||||||
|
AgencyId: 7001,
|
||||||
|
RegionId: 30,
|
||||||
|
Status: "pending",
|
||||||
|
CreatedAtMs: 1700000000000,
|
||||||
|
UpdatedAtMs: 1700000001000,
|
||||||
|
}}}
|
||||||
|
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
||||||
|
handler.SetUserHostClient(hostClient)
|
||||||
|
router := handler.Routes(auth.NewVerifier("secret"))
|
||||||
|
request := httptest.NewRequest(http.MethodPost, "/api/v1/host/agency-applications", bytes.NewReader([]byte(`{"command_id":"cmd-apply-agency","agency_id":"7001"}`)))
|
||||||
|
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||||
|
request.Header.Set("X-Request-ID", "req-host-agency-apply")
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
if hostClient.lastApplyAgency == nil || hostClient.lastApplyAgency.GetUserId() != 42 || hostClient.lastApplyAgency.GetCommandId() != "cmd-apply-agency" || hostClient.lastApplyAgency.GetAgencyId() != 7001 {
|
||||||
|
t.Fatalf("host agency apply request mismatch: %+v", hostClient.lastApplyAgency)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestConfirmMicPublishingForwardsSessionVersionAndEventTime(t *testing.T) {
|
func TestConfirmMicPublishingForwardsSessionVersionAndEventTime(t *testing.T) {
|
||||||
client := &fakeRoomClient{}
|
client := &fakeRoomClient{}
|
||||||
router := NewHandler(client).Routes(auth.NewVerifier("secret"))
|
router := NewHandler(client).Routes(auth.NewVerifier("secret"))
|
||||||
|
|||||||
@ -48,6 +48,8 @@ func (h *Handler) UserHandlers() httproutes.UserHandlers {
|
|||||||
GetMyIdentity: h.getMyIdentity,
|
GetMyIdentity: h.getMyIdentity,
|
||||||
GetMyHostIdentity: h.getMyHostIdentity,
|
GetMyHostIdentity: h.getMyHostIdentity,
|
||||||
GetMyRoleSummary: h.getMyRoleSummary,
|
GetMyRoleSummary: h.getMyRoleSummary,
|
||||||
|
SearchHostAgencies: h.searchHostAgencies,
|
||||||
|
ApplyToHostAgency: h.applyToHostAgency,
|
||||||
CompleteMyOnboarding: h.completeMyOnboarding,
|
CompleteMyOnboarding: h.completeMyOnboarding,
|
||||||
UpdateMyProfile: h.updateMyProfile,
|
UpdateMyProfile: h.updateMyProfile,
|
||||||
ChangeMyCountry: h.changeMyCountry,
|
ChangeMyCountry: h.changeMyCountry,
|
||||||
|
|||||||
@ -0,0 +1,185 @@
|
|||||||
|
package userapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
userv1 "hyapp.local/api/proto/user/v1"
|
||||||
|
"hyapp/services/gateway-service/internal/auth"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
type hostAgencyData struct {
|
||||||
|
AgencyID string `json:"agency_id"`
|
||||||
|
OwnerUserID string `json:"owner_user_id"`
|
||||||
|
RegionID int64 `json:"region_id"`
|
||||||
|
ParentBDUserID string `json:"parent_bd_user_id,omitempty"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
JoinEnabled bool `json:"join_enabled"`
|
||||||
|
MaxHosts int32 `json:"max_hosts"`
|
||||||
|
CreatedByUserID string `json:"created_by_user_id,omitempty"`
|
||||||
|
CreatedAtMS int64 `json:"created_at_ms"`
|
||||||
|
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type hostAgencyApplicationData struct {
|
||||||
|
ApplicationID string `json:"application_id"`
|
||||||
|
CommandID string `json:"command_id"`
|
||||||
|
ApplicantUserID string `json:"applicant_user_id"`
|
||||||
|
AgencyID string `json:"agency_id"`
|
||||||
|
RegionID int64 `json:"region_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
ReviewedByUserID string `json:"reviewed_by_user_id,omitempty"`
|
||||||
|
ReviewReason string `json:"review_reason,omitempty"`
|
||||||
|
ReviewedAtMS int64 `json:"reviewed_at_ms,omitempty"`
|
||||||
|
CreatedAtMS int64 `json:"created_at_ms"`
|
||||||
|
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) searchHostAgencies(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if h.userHostClient == nil {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keyword := strings.TrimSpace(request.URL.Query().Get("short_id"))
|
||||||
|
if keyword == "" {
|
||||||
|
keyword = strings.TrimSpace(request.URL.Query().Get("keyword"))
|
||||||
|
}
|
||||||
|
if keyword == "" {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 20)
|
||||||
|
if !ok {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if pageSize > 20 {
|
||||||
|
pageSize = 20
|
||||||
|
}
|
||||||
|
resp, err := h.userHostClient.SearchAgencies(request.Context(), &userv1.SearchAgenciesRequest{
|
||||||
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
|
Keyword: keyword,
|
||||||
|
PageSize: pageSize,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items := make([]hostAgencyData, 0, len(resp.GetAgencies()))
|
||||||
|
for _, agency := range resp.GetAgencies() {
|
||||||
|
items = append(items, hostAgencyFromProto(agency))
|
||||||
|
}
|
||||||
|
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items), "page_size": pageSize})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) applyToHostAgency(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if h.userHostClient == nil {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body struct {
|
||||||
|
CommandID string `json:"command_id"`
|
||||||
|
CommandIDAlt string `json:"commandId"`
|
||||||
|
AgencyID json.RawMessage `json:"agency_id"`
|
||||||
|
AgencyIDAlt json.RawMessage `json:"agencyId"`
|
||||||
|
}
|
||||||
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
commandID := strings.TrimSpace(body.CommandID)
|
||||||
|
if commandID == "" {
|
||||||
|
commandID = strings.TrimSpace(body.CommandIDAlt)
|
||||||
|
}
|
||||||
|
agencyID, ok := firstRawInt64(body.AgencyID, body.AgencyIDAlt)
|
||||||
|
if commandID == "" || !ok || agencyID <= 0 {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.userHostClient.ApplyToAgency(request.Context(), &userv1.ApplyToAgencyRequest{
|
||||||
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
|
CommandId: commandID,
|
||||||
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
|
AgencyId: agencyID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httpkit.WriteOK(writer, request, map[string]any{"application": hostAgencyApplicationFromProto(resp.GetApplication())})
|
||||||
|
}
|
||||||
|
|
||||||
|
func hostAgencyFromProto(agency *userv1.Agency) hostAgencyData {
|
||||||
|
if agency == nil {
|
||||||
|
return hostAgencyData{}
|
||||||
|
}
|
||||||
|
return hostAgencyData{
|
||||||
|
AgencyID: int64String(agency.GetAgencyId()),
|
||||||
|
OwnerUserID: int64String(agency.GetOwnerUserId()),
|
||||||
|
RegionID: agency.GetRegionId(),
|
||||||
|
ParentBDUserID: int64String(agency.GetParentBdUserId()),
|
||||||
|
Name: agency.GetName(),
|
||||||
|
Status: agency.GetStatus(),
|
||||||
|
JoinEnabled: agency.GetJoinEnabled(),
|
||||||
|
MaxHosts: agency.GetMaxHosts(),
|
||||||
|
CreatedByUserID: int64String(agency.GetCreatedByUserId()),
|
||||||
|
CreatedAtMS: agency.GetCreatedAtMs(),
|
||||||
|
UpdatedAtMS: agency.GetUpdatedAtMs(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hostAgencyApplicationFromProto(application *userv1.AgencyApplication) hostAgencyApplicationData {
|
||||||
|
if application == nil {
|
||||||
|
return hostAgencyApplicationData{}
|
||||||
|
}
|
||||||
|
return hostAgencyApplicationData{
|
||||||
|
ApplicationID: int64String(application.GetApplicationId()),
|
||||||
|
CommandID: application.GetCommandId(),
|
||||||
|
ApplicantUserID: int64String(application.GetApplicantUserId()),
|
||||||
|
AgencyID: int64String(application.GetAgencyId()),
|
||||||
|
RegionID: application.GetRegionId(),
|
||||||
|
Status: application.GetStatus(),
|
||||||
|
ReviewedByUserID: int64String(application.GetReviewedByUserId()),
|
||||||
|
ReviewReason: application.GetReviewReason(),
|
||||||
|
ReviewedAtMS: application.GetReviewedAtMs(),
|
||||||
|
CreatedAtMS: application.GetCreatedAtMs(),
|
||||||
|
UpdatedAtMS: application.GetUpdatedAtMs(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func int64String(value int64) string {
|
||||||
|
if value <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strconv.FormatInt(value, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstRawInt64(values ...json.RawMessage) (int64, bool) {
|
||||||
|
for _, raw := range values {
|
||||||
|
value, ok := parseRawInt64(raw)
|
||||||
|
if ok {
|
||||||
|
return value, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRawInt64(raw json.RawMessage) (int64, bool) {
|
||||||
|
text := strings.TrimSpace(string(raw))
|
||||||
|
if text == "" || text == "null" {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(text, `"`) {
|
||||||
|
var value string
|
||||||
|
if err := json.Unmarshal(raw, &value); err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
text = strings.TrimSpace(value)
|
||||||
|
}
|
||||||
|
value, err := strconv.ParseInt(text, 10, 64)
|
||||||
|
return value, err == nil
|
||||||
|
}
|
||||||
@ -84,7 +84,7 @@ func New(cfg config.Config) (*App, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var activityConn *grpc.ClientConn
|
var activityConn *grpc.ClientConn
|
||||||
if cfg.OutboxWorker.PublishMode == config.OutboxPublishModeDirect || cfg.OutboxWorker.PublishMode == config.OutboxPublishModeDual {
|
if cfg.ActivityServiceAddr != "" {
|
||||||
activityConn, err = grpc.Dial(cfg.ActivityServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
activityConn, err = grpc.Dial(cfg.ActivityServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = walletConn.Close()
|
_ = walletConn.Close()
|
||||||
@ -189,6 +189,10 @@ func New(cfg config.Config) (*App, error) {
|
|||||||
treasureOpenScheduler = integration.NewRocketMQTreasureOpenScheduler(treasureProducer, cfg.RocketMQ.TreasureOpen.Topic)
|
treasureOpenScheduler = integration.NewRocketMQTreasureOpenScheduler(treasureProducer, cfg.RocketMQ.TreasureOpen.Topic)
|
||||||
}
|
}
|
||||||
outboxPublisher := integration.NewCompositeOutboxPublisher(outboxPublishers...)
|
outboxPublisher := integration.NewCompositeOutboxPublisher(outboxPublishers...)
|
||||||
|
var luckyGiftClient integration.LuckyGiftClient
|
||||||
|
if activityConn != nil {
|
||||||
|
luckyGiftClient = integration.NewGRPCLuckyGiftClient(activityConn)
|
||||||
|
}
|
||||||
|
|
||||||
// 领域服务只依赖接口,App 层负责选择 MySQL/Redis/gRPC 具体实现。
|
// 领域服务只依赖接口,App 层负责选择 MySQL/Redis/gRPC 具体实现。
|
||||||
svc := roomservice.New(roomservice.Config{
|
svc := roomservice.New(roomservice.Config{
|
||||||
@ -200,7 +204,7 @@ func New(cfg config.Config) (*App, error) {
|
|||||||
MicPublishTimeout: cfg.MicPublishTimeout,
|
MicPublishTimeout: cfg.MicPublishTimeout,
|
||||||
RTCUserRemover: rtcUserRemover,
|
RTCUserRemover: rtcUserRemover,
|
||||||
RoomTreasureOpenScheduler: treasureOpenScheduler,
|
RoomTreasureOpenScheduler: treasureOpenScheduler,
|
||||||
}, directory, repository, integration.NewGRPCWalletClient(walletConn), roomPublisher, outboxPublisher)
|
}, directory, repository, integration.NewGRPCWalletClient(walletConn), roomPublisher, outboxPublisher, luckyGiftClient)
|
||||||
if cfg.RocketMQ.TreasureOpen.Enabled {
|
if cfg.RocketMQ.TreasureOpen.Enabled {
|
||||||
treasureConsumer, err := rocketmqx.NewConsumer(rocketMQConsumerConfig(cfg.RocketMQ, cfg.RocketMQ.TreasureOpen.ConsumerGroup, cfg.RocketMQ.TreasureOpen.ConsumerMaxReconsumeTimes))
|
treasureConsumer, err := rocketmqx.NewConsumer(rocketMQConsumerConfig(cfg.RocketMQ, cfg.RocketMQ.TreasureOpen.ConsumerGroup, cfg.RocketMQ.TreasureOpen.ConsumerMaxReconsumeTimes))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -4,6 +4,7 @@ package integration
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||||
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
||||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||||
"hyapp/pkg/tencentim"
|
"hyapp/pkg/tencentim"
|
||||||
@ -17,6 +18,14 @@ type WalletClient interface {
|
|||||||
GrantResourceGroup(ctx context.Context, req *walletv1.GrantResourceGroupRequest) (*walletv1.ResourceGrantResponse, error)
|
GrantResourceGroup(ctx context.Context, req *walletv1.GrantResourceGroupRequest) (*walletv1.ResourceGrantResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LuckyGiftClient 抽象 room-service 对 activity-service 幸运礼物抽奖边界的同步依赖。
|
||||||
|
type LuckyGiftClient interface {
|
||||||
|
// CheckLuckyGift 在扣费前确认奖池规则可用;失败时不能扣费。
|
||||||
|
CheckLuckyGift(ctx context.Context, req *activityv1.CheckLuckyGiftRequest) (*activityv1.CheckLuckyGiftResponse, error)
|
||||||
|
// ExecuteLuckyGiftDraw 在钱包扣费成功后按 command_id 幂等落抽奖事实。
|
||||||
|
ExecuteLuckyGiftDraw(ctx context.Context, req *activityv1.ExecuteLuckyGiftDrawRequest) (*activityv1.ExecuteLuckyGiftDrawResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
// RoomEventPublisher 保留腾讯云 IM 直接桥接能力;房间命令主链路不再调用它。
|
// RoomEventPublisher 保留腾讯云 IM 直接桥接能力;房间命令主链路不再调用它。
|
||||||
type RoomEventPublisher interface {
|
type RoomEventPublisher interface {
|
||||||
// EnsureRoomGroup 确保房间在腾讯云 IM 中有对应群组。
|
// EnsureRoomGroup 确保房间在腾讯云 IM 中有对应群组。
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -32,3 +33,20 @@ func (c *grpcWalletClient) GrantResourceGroup(ctx context.Context, req *walletv1
|
|||||||
// 宝箱奖励以 resource group 为后台配置单位,wallet-service 负责展开资产和权益。
|
// 宝箱奖励以 resource group 为后台配置单位,wallet-service 负责展开资产和权益。
|
||||||
return c.client.GrantResourceGroup(ctx, req)
|
return c.client.GrantResourceGroup(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type grpcLuckyGiftClient struct {
|
||||||
|
client activityv1.LuckyGiftServiceClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGRPCLuckyGiftClient 用 gRPC 连接创建 activity-service 幸运礼物客户端。
|
||||||
|
func NewGRPCLuckyGiftClient(conn *grpc.ClientConn) LuckyGiftClient {
|
||||||
|
return &grpcLuckyGiftClient{client: activityv1.NewLuckyGiftServiceClient(conn)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *grpcLuckyGiftClient) CheckLuckyGift(ctx context.Context, req *activityv1.CheckLuckyGiftRequest) (*activityv1.CheckLuckyGiftResponse, error) {
|
||||||
|
return c.client.CheckLuckyGift(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *grpcLuckyGiftClient) ExecuteLuckyGiftDraw(ctx context.Context, req *activityv1.ExecuteLuckyGiftDrawRequest) (*activityv1.ExecuteLuckyGiftDrawResponse, error) {
|
||||||
|
return c.client.ExecuteLuckyGiftDraw(ctx, req)
|
||||||
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||||
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
||||||
roomv1 "hyapp.local/api/proto/room/v1"
|
roomv1 "hyapp.local/api/proto/room/v1"
|
||||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||||
@ -66,6 +67,26 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
|
|||||||
if !exists {
|
if !exists {
|
||||||
return mutationResult{}, nil, xerr.New(xerr.NotFound, "room not found")
|
return mutationResult{}, nil, xerr.New(xerr.NotFound, "room not found")
|
||||||
}
|
}
|
||||||
|
luckyEnabled := false
|
||||||
|
if cmd.PoolID != "" {
|
||||||
|
if s.luckyGift == nil {
|
||||||
|
return mutationResult{}, nil, xerr.New(xerr.Unavailable, "lucky gift service is not configured")
|
||||||
|
}
|
||||||
|
checkResp, err := s.luckyGift.CheckLuckyGift(ctx, &activityv1.CheckLuckyGiftRequest{
|
||||||
|
Meta: activityMetaFromRoom(ctx, req.GetMeta()),
|
||||||
|
UserId: cmd.ActorUserID(),
|
||||||
|
RoomId: cmd.RoomID(),
|
||||||
|
GiftId: cmd.GiftID,
|
||||||
|
PoolId: cmd.PoolID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return mutationResult{}, nil, err
|
||||||
|
}
|
||||||
|
if checkResp == nil || !checkResp.GetEnabled() {
|
||||||
|
return mutationResult{}, nil, xerr.New(xerr.RuleNotActive, "lucky gift rule is not active")
|
||||||
|
}
|
||||||
|
luckyEnabled = true
|
||||||
|
}
|
||||||
treasureConfig, err := s.roomTreasureConfig(ctx)
|
treasureConfig, err := s.roomTreasureConfig(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mutationResult{}, nil, err
|
return mutationResult{}, nil, err
|
||||||
@ -98,6 +119,33 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
|
|||||||
settledCommand.HeatValue = heatValue
|
settledCommand.HeatValue = heatValue
|
||||||
settledCommand.PriceVersion = billing.GetPriceVersion()
|
settledCommand.PriceVersion = billing.GetPriceVersion()
|
||||||
settledCommand.GiftTypeCode = billing.GetGiftTypeCode()
|
settledCommand.GiftTypeCode = billing.GetGiftTypeCode()
|
||||||
|
if !luckyEnabled {
|
||||||
|
luckyEnabled = s.shouldDrawLuckyGift(cmd.PoolID, billing.GetGiftTypeCode())
|
||||||
|
}
|
||||||
|
var luckyGift *roomv1.LuckyGiftDrawResult
|
||||||
|
if luckyEnabled {
|
||||||
|
drawResp, err := s.luckyGift.ExecuteLuckyGiftDraw(ctx, &activityv1.ExecuteLuckyGiftDrawRequest{
|
||||||
|
LuckyGift: &activityv1.LuckyGiftMeta{
|
||||||
|
Meta: activityMetaFromRoom(ctx, req.GetMeta()),
|
||||||
|
CommandId: cmd.ID(),
|
||||||
|
UserId: cmd.ActorUserID(),
|
||||||
|
DeviceId: luckyGiftDeviceID(cmd),
|
||||||
|
RoomId: cmd.RoomID(),
|
||||||
|
AnchorId: luckyGiftAnchorID(roomMeta),
|
||||||
|
GiftId: cmd.GiftID,
|
||||||
|
CoinSpent: billing.GetCoinSpent(),
|
||||||
|
PaidAtMs: now.UTC().UnixMilli(),
|
||||||
|
PoolId: cmd.PoolID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return mutationResult{}, nil, err
|
||||||
|
}
|
||||||
|
if drawResp == nil {
|
||||||
|
return mutationResult{}, nil, xerr.New(xerr.Unavailable, "lucky gift draw response is empty")
|
||||||
|
}
|
||||||
|
luckyGift = luckyGiftResultFromProto(drawResp.GetResult())
|
||||||
|
}
|
||||||
|
|
||||||
// 扣费成功后,Room Cell 同步更新热度和本地礼物榜。
|
// 扣费成功后,Room Cell 同步更新热度和本地礼物榜。
|
||||||
current.Heat += heatValue
|
current.Heat += heatValue
|
||||||
@ -167,6 +215,7 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
|
|||||||
billingReceiptID: billing.GetBillingReceiptId(),
|
billingReceiptID: billing.GetBillingReceiptId(),
|
||||||
roomHeat: current.Heat,
|
roomHeat: current.Heat,
|
||||||
giftRank: cloneProtoRank(current.GiftRank),
|
giftRank: cloneProtoRank(current.GiftRank),
|
||||||
|
luckyGift: luckyGift,
|
||||||
commandPayload: commandPayload,
|
commandPayload: commandPayload,
|
||||||
walletDebitMS: walletDebitMS,
|
walletDebitMS: walletDebitMS,
|
||||||
syncEvent: &tencentim.RoomEvent{
|
syncEvent: &tencentim.RoomEvent{
|
||||||
@ -202,9 +251,71 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
|
|||||||
GiftRank: result.giftRank,
|
GiftRank: result.giftRank,
|
||||||
Room: result.snapshot,
|
Room: result.snapshot,
|
||||||
Treasure: result.snapshot.GetTreasure(),
|
Treasure: result.snapshot.GetTreasure(),
|
||||||
|
LuckyGift: result.luckyGift,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) shouldDrawLuckyGift(poolID string, giftTypeCode string) bool {
|
||||||
|
if s.luckyGift == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch strings.TrimSpace(giftTypeCode) {
|
||||||
|
case "lucky", "super_lucky":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return strings.TrimSpace(poolID) != ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func activityMetaFromRoom(ctx context.Context, meta *roomv1.RequestMeta) *activityv1.RequestMeta {
|
||||||
|
return &activityv1.RequestMeta{
|
||||||
|
RequestId: meta.GetRequestId(),
|
||||||
|
Caller: "room-service",
|
||||||
|
GatewayNodeId: meta.GetGatewayNodeId(),
|
||||||
|
SentAtMs: time.Now().UTC().UnixMilli(),
|
||||||
|
AppCode: appcode.FromContext(ctx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func luckyGiftDeviceID(cmd command.SendGift) string {
|
||||||
|
if value := strings.TrimSpace(cmd.SessionID); value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return cmd.ID()
|
||||||
|
}
|
||||||
|
|
||||||
|
func luckyGiftAnchorID(roomMeta RoomMeta) string {
|
||||||
|
if roomMeta.OwnerUserID <= 0 {
|
||||||
|
return roomMeta.RoomID
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d", roomMeta.OwnerUserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func luckyGiftResultFromProto(result *activityv1.LuckyGiftDrawResult) *roomv1.LuckyGiftDrawResult {
|
||||||
|
if result == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &roomv1.LuckyGiftDrawResult{
|
||||||
|
Enabled: true,
|
||||||
|
DrawId: result.GetDrawId(),
|
||||||
|
CommandId: result.GetCommandId(),
|
||||||
|
PoolId: result.GetPoolId(),
|
||||||
|
GiftId: result.GetGiftId(),
|
||||||
|
RuleVersion: result.GetRuleVersion(),
|
||||||
|
ExperiencePool: result.GetExperiencePool(),
|
||||||
|
SelectedTierId: result.GetSelectedTierId(),
|
||||||
|
MultiplierPpm: result.GetMultiplierPpm(),
|
||||||
|
BaseRewardCoins: result.GetBaseRewardCoins(),
|
||||||
|
RoomAtmosphereRewardCoins: result.GetRoomAtmosphereRewardCoins(),
|
||||||
|
ActivitySubsidyCoins: result.GetActivitySubsidyCoins(),
|
||||||
|
EffectiveRewardCoins: result.GetEffectiveRewardCoins(),
|
||||||
|
RewardStatus: result.GetRewardStatus(),
|
||||||
|
StageFeedback: result.GetStageFeedback(),
|
||||||
|
HighMultiplier: result.GetHighMultiplier(),
|
||||||
|
CreatedAtMs: result.GetCreatedAtMs(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeGiftTargetType(raw string) string {
|
func normalizeGiftTargetType(raw string) string {
|
||||||
raw = strings.TrimSpace(raw)
|
raw = strings.TrimSpace(raw)
|
||||||
switch raw {
|
switch raw {
|
||||||
|
|||||||
110
services/room-service/internal/room/service/lucky_gift_test.go
Normal file
110
services/room-service/internal/room/service/lucky_gift_test.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
package service_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||||
|
roomv1 "hyapp.local/api/proto/room/v1"
|
||||||
|
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/services/room-service/internal/integration"
|
||||||
|
roomservice "hyapp/services/room-service/internal/room/service"
|
||||||
|
"hyapp/services/room-service/internal/router"
|
||||||
|
"hyapp/services/room-service/internal/testutil/mysqltest"
|
||||||
|
)
|
||||||
|
|
||||||
|
type luckyGiftTestClient struct {
|
||||||
|
checks []*activityv1.CheckLuckyGiftRequest
|
||||||
|
draws []*activityv1.ExecuteLuckyGiftDrawRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *luckyGiftTestClient) CheckLuckyGift(_ context.Context, req *activityv1.CheckLuckyGiftRequest) (*activityv1.CheckLuckyGiftResponse, error) {
|
||||||
|
c.checks = append(c.checks, req)
|
||||||
|
return &activityv1.CheckLuckyGiftResponse{
|
||||||
|
Enabled: true,
|
||||||
|
Reason: "enabled",
|
||||||
|
GiftId: req.GetGiftId(),
|
||||||
|
PoolId: req.GetPoolId(),
|
||||||
|
RuleVersion: 12,
|
||||||
|
ExperiencePool: "novice",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *luckyGiftTestClient) ExecuteLuckyGiftDraw(_ context.Context, req *activityv1.ExecuteLuckyGiftDrawRequest) (*activityv1.ExecuteLuckyGiftDrawResponse, error) {
|
||||||
|
c.draws = append(c.draws, req)
|
||||||
|
meta := req.GetLuckyGift()
|
||||||
|
return &activityv1.ExecuteLuckyGiftDrawResponse{Result: &activityv1.LuckyGiftDrawResult{
|
||||||
|
DrawId: "lucky_draw_test",
|
||||||
|
CommandId: meta.GetCommandId(),
|
||||||
|
PoolId: meta.GetPoolId(),
|
||||||
|
GiftId: meta.GetGiftId(),
|
||||||
|
RuleVersion: 12,
|
||||||
|
ExperiencePool: "novice",
|
||||||
|
SelectedTierId: "novice_2x",
|
||||||
|
MultiplierPpm: 2_000_000,
|
||||||
|
BaseRewardCoins: 200,
|
||||||
|
EffectiveRewardCoins: 200,
|
||||||
|
RewardStatus: "pending",
|
||||||
|
CreatedAtMs: 1_779_258_000_000,
|
||||||
|
}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendGiftReturnsLuckyGiftDrawResult(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
repository := mysqltest.NewRepository(t)
|
||||||
|
wallet := &treasureTestWallet{debits: []*walletv1.DebitGiftResponse{{
|
||||||
|
BillingReceiptId: "receipt-lucky",
|
||||||
|
CoinSpent: 100,
|
||||||
|
GiftPointAdded: 100,
|
||||||
|
HeatValue: 100,
|
||||||
|
GiftTypeCode: "super_lucky",
|
||||||
|
}}}
|
||||||
|
luckyGift := &luckyGiftTestClient{}
|
||||||
|
svc := roomservice.New(roomservice.Config{
|
||||||
|
NodeID: "node-lucky-test",
|
||||||
|
LeaseTTL: 10 * time.Second,
|
||||||
|
RankLimit: 20,
|
||||||
|
SnapshotEveryN: 1,
|
||||||
|
}, router.NewMemoryDirectory(), repository, wallet, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher(), luckyGift)
|
||||||
|
|
||||||
|
roomID := "room-lucky-gift"
|
||||||
|
ownerID := int64(101)
|
||||||
|
viewerID := int64(202)
|
||||||
|
createTreasureRoom(t, ctx, svc, roomID, ownerID, 9001)
|
||||||
|
joinTreasureRoom(t, ctx, svc, roomID, viewerID)
|
||||||
|
|
||||||
|
resp, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{
|
||||||
|
Meta: &roomv1.RequestMeta{
|
||||||
|
RequestId: "req-lucky",
|
||||||
|
CommandId: "cmd-lucky",
|
||||||
|
ActorUserId: ownerID,
|
||||||
|
RoomId: roomID,
|
||||||
|
SessionId: "device-session-1",
|
||||||
|
AppCode: appcode.Default,
|
||||||
|
SentAtMs: time.Date(2026, 5, 20, 12, 0, 0, 0, time.UTC).UnixMilli(),
|
||||||
|
},
|
||||||
|
TargetType: "user",
|
||||||
|
TargetUserId: viewerID,
|
||||||
|
GiftId: "rose",
|
||||||
|
GiftCount: 1,
|
||||||
|
PoolId: "super_lucky",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("send lucky gift failed: %v", err)
|
||||||
|
}
|
||||||
|
if resp.GetLuckyGift().GetDrawId() != "lucky_draw_test" || resp.GetLuckyGift().GetMultiplierPpm() != 2_000_000 || resp.GetLuckyGift().GetEffectiveRewardCoins() != 200 {
|
||||||
|
t.Fatalf("lucky gift result mismatch: %+v", resp.GetLuckyGift())
|
||||||
|
}
|
||||||
|
if len(luckyGift.checks) != 1 || luckyGift.checks[0].GetPoolId() != "super_lucky" || luckyGift.checks[0].GetGiftId() != "rose" {
|
||||||
|
t.Fatalf("lucky check request mismatch: %+v", luckyGift.checks)
|
||||||
|
}
|
||||||
|
if len(luckyGift.draws) != 1 {
|
||||||
|
t.Fatalf("expected one lucky draw, got %d", len(luckyGift.draws))
|
||||||
|
}
|
||||||
|
drawMeta := luckyGift.draws[0].GetLuckyGift()
|
||||||
|
if drawMeta.GetCommandId() != "cmd-lucky" || drawMeta.GetCoinSpent() != 100 || drawMeta.GetDeviceId() != "device-session-1" || drawMeta.GetAnchorId() != "101" {
|
||||||
|
t.Fatalf("lucky draw meta mismatch: %+v", drawMeta)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -60,6 +60,8 @@ type Service struct {
|
|||||||
repository Repository
|
repository Repository
|
||||||
// wallet 是 SendGift 的同步扣费依赖,扣费失败不能进入 Room Cell 状态变更。
|
// wallet 是 SendGift 的同步扣费依赖,扣费失败不能进入 Room Cell 状态变更。
|
||||||
wallet integration.WalletClient
|
wallet integration.WalletClient
|
||||||
|
// luckyGift 是幸运礼物同步检查和抽奖依赖;未配置时幸运礼物按未启用处理。
|
||||||
|
luckyGift integration.LuckyGiftClient
|
||||||
// syncPublisher 是 room-service 到腾讯云 IM 的低时延房间系统事件桥。
|
// syncPublisher 是 room-service 到腾讯云 IM 的低时延房间系统事件桥。
|
||||||
syncPublisher integration.RoomEventPublisher
|
syncPublisher integration.RoomEventPublisher
|
||||||
// outboxPublisher 是补偿 worker 对外部消费者的异步投递抽象。
|
// outboxPublisher 是补偿 worker 对外部消费者的异步投递抽象。
|
||||||
@ -97,6 +99,8 @@ type mutationResult struct {
|
|||||||
roomHeat int64
|
roomHeat int64
|
||||||
// giftRank 是 SendGift 后的本地礼物榜投影。
|
// giftRank 是 SendGift 后的本地礼物榜投影。
|
||||||
giftRank []*roomv1.RankItem
|
giftRank []*roomv1.RankItem
|
||||||
|
// luckyGift 是扣费成功后同步执行的幸运礼物抽奖结果;普通礼物保持 nil。
|
||||||
|
luckyGift *roomv1.LuckyGiftDrawResult
|
||||||
// commandPayload 允许少数命令把外部依赖的结算快照写入 command log,用于恢复。
|
// commandPayload 允许少数命令把外部依赖的结算快照写入 command log,用于恢复。
|
||||||
commandPayload []byte
|
commandPayload []byte
|
||||||
// syncEvent 标记该命令存在腾讯云 IM 房间消息形态;实际投递只由 outbox worker 异步执行。
|
// syncEvent 标记该命令存在腾讯云 IM 房间消息形态;实际投递只由 outbox worker 异步执行。
|
||||||
@ -117,7 +121,7 @@ type mutationResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New 初始化 room-service 领域服务。
|
// New 初始化 room-service 领域服务。
|
||||||
func New(cfg Config, directory router.Directory, repository Repository, wallet integration.WalletClient, syncPublisher integration.RoomEventPublisher, outboxPublisher integration.OutboxPublisher) *Service {
|
func New(cfg Config, directory router.Directory, repository Repository, wallet integration.WalletClient, syncPublisher integration.RoomEventPublisher, outboxPublisher integration.OutboxPublisher, luckyGift ...integration.LuckyGiftClient) *Service {
|
||||||
usedClock := cfg.Clock
|
usedClock := cfg.Clock
|
||||||
if usedClock == nil {
|
if usedClock == nil {
|
||||||
// 生产默认系统时钟,测试可注入固定时钟保证断言稳定。
|
// 生产默认系统时钟,测试可注入固定时钟保证断言稳定。
|
||||||
@ -148,6 +152,11 @@ func New(cfg Config, directory router.Directory, repository Repository, wallet i
|
|||||||
micPublishTimeout = 15 * time.Second
|
micPublishTimeout = 15 * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var luckyGiftClient integration.LuckyGiftClient
|
||||||
|
if len(luckyGift) > 0 {
|
||||||
|
luckyGiftClient = luckyGift[0]
|
||||||
|
}
|
||||||
|
|
||||||
return &Service{
|
return &Service{
|
||||||
nodeID: cfg.NodeID,
|
nodeID: cfg.NodeID,
|
||||||
leaseTTL: cfg.LeaseTTL,
|
leaseTTL: cfg.LeaseTTL,
|
||||||
@ -159,6 +168,7 @@ func New(cfg Config, directory router.Directory, repository Repository, wallet i
|
|||||||
directory: directory,
|
directory: directory,
|
||||||
repository: repository,
|
repository: repository,
|
||||||
wallet: wallet,
|
wallet: wallet,
|
||||||
|
luckyGift: luckyGiftClient,
|
||||||
syncPublisher: syncPublisher,
|
syncPublisher: syncPublisher,
|
||||||
outboxPublisher: outboxPublisher,
|
outboxPublisher: outboxPublisher,
|
||||||
roomTreasureOpenScheduler: cfg.RoomTreasureOpenScheduler,
|
roomTreasureOpenScheduler: cfg.RoomTreasureOpenScheduler,
|
||||||
|
|||||||
@ -400,6 +400,13 @@ func TestAdminCreateAgencyControlsAppSearch(t *testing.T) {
|
|||||||
if len(agencies) != 1 || agencies[0].AgencyID != created.Agency.AgencyID {
|
if len(agencies) != 1 || agencies[0].AgencyID != created.Agency.AgencyID {
|
||||||
t.Fatalf("created agency should be searchable: %+v", agencies)
|
t.Fatalf("created agency should be searchable: %+v", agencies)
|
||||||
}
|
}
|
||||||
|
agencies, err = svc.SearchAgencies(ctx, 902, displayID(901), 20)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SearchAgencies by owner display id failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(agencies) != 1 || agencies[0].AgencyID != created.Agency.AgencyID {
|
||||||
|
t.Fatalf("created agency should be searchable by owner display id: %+v", agencies)
|
||||||
|
}
|
||||||
|
|
||||||
disabledJoin, err := svc.SetAgencyJoinEnabled(ctx, hostservice.SetAgencyJoinEnabledInput{
|
disabledJoin, err := svc.SetAgencyJoinEnabled(ctx, hostservice.SetAgencyJoinEnabledInput{
|
||||||
CommandID: "admin-disable-agency-join-901",
|
CommandID: "admin-disable-agency-join-901",
|
||||||
|
|||||||
@ -30,8 +30,13 @@ func (r *Repository) SearchAgencies(ctx context.Context, command hostservice.Sea
|
|||||||
`, agencyColumns)
|
`, agencyColumns)
|
||||||
args := []any{appcode.FromContext(ctx), regionID, hostdomain.AgencyStatusActive}
|
args := []any{appcode.FromContext(ctx), regionID, hostdomain.AgencyStatusActive}
|
||||||
if strings.TrimSpace(command.Keyword) != "" {
|
if strings.TrimSpace(command.Keyword) != "" {
|
||||||
query += " AND name LIKE ?"
|
keyword := strings.TrimSpace(command.Keyword)
|
||||||
args = append(args, "%"+strings.TrimSpace(command.Keyword)+"%")
|
query += ` AND (name LIKE ? OR owner_user_id IN (
|
||||||
|
SELECT user_id
|
||||||
|
FROM users
|
||||||
|
WHERE app_code = ? AND current_display_user_id = ?
|
||||||
|
))`
|
||||||
|
args = append(args, "%"+keyword+"%", appcode.FromContext(ctx), keyword)
|
||||||
}
|
}
|
||||||
query += " ORDER BY created_at_ms DESC, agency_id DESC LIMIT ?"
|
query += " ORDER BY created_at_ms DESC, agency_id DESC LIMIT ?"
|
||||||
args = append(args, command.PageSize)
|
args = append(args, command.PageSize)
|
||||||
|
|||||||
@ -1417,6 +1417,31 @@ func TestManagerCenterResourceGrantFiltersAndRechecksSwitch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateResourceRejectsDuplicateResourceCodeAsConflict(t *testing.T) {
|
||||||
|
repository := mysqltest.NewRepository(t)
|
||||||
|
svc := walletservice.New(repository)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
command := resourcedomain.ResourceCommand{
|
||||||
|
ResourceCode: "duplicate_badge",
|
||||||
|
ResourceType: resourcedomain.TypeBadge,
|
||||||
|
Name: "Duplicate Badge",
|
||||||
|
Status: resourcedomain.StatusActive,
|
||||||
|
Grantable: true,
|
||||||
|
ManagerGrantEnabled: true,
|
||||||
|
GrantStrategy: resourcedomain.GrantStrategySetActiveFlag,
|
||||||
|
UsageScopes: []string{"profile"},
|
||||||
|
OperatorUserID: 90001,
|
||||||
|
}
|
||||||
|
if _, err := svc.CreateResource(ctx, command); err != nil {
|
||||||
|
t.Fatalf("first CreateResource failed: %v", err)
|
||||||
|
}
|
||||||
|
command.Name = "Duplicate Badge Again"
|
||||||
|
if _, err := svc.CreateResource(ctx, command); !xerr.IsCode(err, xerr.Conflict) {
|
||||||
|
t.Fatalf("duplicate resource_code should be conflict, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestManagerCenterCannotGrantResourceGroup 固定首版经理中心只允许单资源赠送。
|
// TestManagerCenterCannotGrantResourceGroup 固定首版经理中心只允许单资源赠送。
|
||||||
func TestManagerCenterCannotGrantResourceGroup(t *testing.T) {
|
func TestManagerCenterCannotGrantResourceGroup(t *testing.T) {
|
||||||
repository := mysqltest.NewRepository(t)
|
repository := mysqltest.NewRepository(t)
|
||||||
|
|||||||
@ -146,7 +146,7 @@ func (r *Repository) CreateResource(ctx context.Context, command resourcedomain.
|
|||||||
nowMs,
|
nowMs,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resourcedomain.Resource{}, err
|
return resourcedomain.Resource{}, mapResourceWriteError(err)
|
||||||
}
|
}
|
||||||
resourceID, err := result.LastInsertId()
|
resourceID, err := result.LastInsertId()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -225,7 +225,7 @@ func (r *Repository) UpdateResource(ctx context.Context, command resourcedomain.
|
|||||||
command.ResourceID,
|
command.ResourceID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resourcedomain.Resource{}, err
|
return resourcedomain.Resource{}, mapResourceWriteError(err)
|
||||||
}
|
}
|
||||||
if affected, err := result.RowsAffected(); err != nil {
|
if affected, err := result.RowsAffected(); err != nil {
|
||||||
return resourcedomain.Resource{}, err
|
return resourcedomain.Resource{}, err
|
||||||
@ -2215,6 +2215,13 @@ func resourceOffset(page int32, pageSize int32) int32 {
|
|||||||
return (page - 1) * pageSize
|
return (page - 1) * pageSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mapResourceWriteError(err error) error {
|
||||||
|
if isMySQLDuplicateError(err) {
|
||||||
|
return xerr.New(xerr.Conflict, "resource_code already exists")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeResourceCommand(command resourcedomain.ResourceCommand) resourcedomain.ResourceCommand {
|
func normalizeResourceCommand(command resourcedomain.ResourceCommand) resourcedomain.ResourceCommand {
|
||||||
command.ResourceCode = strings.TrimSpace(command.ResourceCode)
|
command.ResourceCode = strings.TrimSpace(command.ResourceCode)
|
||||||
command.ResourceType = resourcedomain.NormalizeResourceType(command.ResourceType)
|
command.ResourceType = resourcedomain.NormalizeResourceType(command.ResourceType)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user