活动moban
This commit is contained in:
parent
d164dd5fd7
commit
892e237be5
File diff suppressed because it is too large
Load Diff
@ -2377,6 +2377,533 @@ message ApplyAgencyOpeningResponse {
|
||||
bool created = 2;
|
||||
}
|
||||
|
||||
// ActivityTemplateLocale 是活动模版的单语言文案。locale 使用 BCP-47 基础语言码,
|
||||
// 当前运营后台至少配置 en,客户端按 locale -> en 的顺序回退。
|
||||
message ActivityTemplateLocale {
|
||||
string locale = 1;
|
||||
string title = 2;
|
||||
string rules = 3;
|
||||
}
|
||||
|
||||
// ActivityTemplateGift 是礼物挑战赛允许计分的礼物快照引用。
|
||||
message ActivityTemplateGift {
|
||||
string gift_id = 1;
|
||||
int32 sort_order = 2;
|
||||
string name = 3;
|
||||
string icon_url = 4;
|
||||
int64 coin_price = 5;
|
||||
string price_version = 6;
|
||||
}
|
||||
|
||||
// ActivityTemplateRewardItem 是 wallet pinned snapshot 的安全展示投影;不暴露发放策略和管理字段。
|
||||
message ActivityTemplateRewardItem {
|
||||
string item_type = 1;
|
||||
int64 resource_id = 2;
|
||||
string resource_type = 3;
|
||||
string name = 4;
|
||||
string icon_url = 5;
|
||||
int64 quantity = 6;
|
||||
int64 duration_ms = 7;
|
||||
string wallet_asset_type = 8;
|
||||
int64 wallet_asset_amount = 9;
|
||||
int32 sort_order = 10;
|
||||
}
|
||||
|
||||
// ActivityTemplateTask 表达模版内的一档每日任务。奖励使用 wallet-service 资源组,
|
||||
// 由资源组负责展开头像框、座驾、金币等实际资产。
|
||||
message ActivityTemplateTask {
|
||||
string task_key = 1;
|
||||
string task_type = 2;
|
||||
int64 target_value = 3;
|
||||
int64 reward_resource_group_id = 4;
|
||||
int32 sort_order = 5;
|
||||
string reward_snapshot_id = 6;
|
||||
string reward_snapshot_hash = 7;
|
||||
string reward_name = 8;
|
||||
repeated ActivityTemplateRewardItem reward_items = 9;
|
||||
}
|
||||
|
||||
// ActivityTemplateRankReward 是日榜或总榜的一个不重叠名次区间奖励。
|
||||
message ActivityTemplateRankReward {
|
||||
string board_type = 1;
|
||||
int32 rank_from = 2;
|
||||
int32 rank_to = 3;
|
||||
int64 resource_group_id = 4;
|
||||
int32 sort_order = 5;
|
||||
string reward_snapshot_id = 6;
|
||||
string reward_snapshot_hash = 7;
|
||||
string reward_name = 8;
|
||||
repeated ActivityTemplateRewardItem reward_items = 9;
|
||||
}
|
||||
|
||||
// ActivityTemplateAsset 是客户端活动页素材引用。locale="*" 表示所有语言共享,
|
||||
// 独立语言素材覆盖共享素材;media_type 支持 image/webp/svga/pag。
|
||||
message ActivityTemplateAsset {
|
||||
string asset_key = 1;
|
||||
string locale = 2;
|
||||
string url = 3;
|
||||
string media_type = 4;
|
||||
int32 sort_order = 5;
|
||||
}
|
||||
|
||||
// ActivityTemplate 是可排期的运营活动实例。运营侧沿用“活动模版”名称,
|
||||
// 发布时 activity-service 会固化完整版本快照,避免运行中的配置被后台编辑覆盖。
|
||||
message ActivityTemplate {
|
||||
string app_code = 1;
|
||||
string template_id = 2;
|
||||
string template_code = 3;
|
||||
string name = 4;
|
||||
string activity_type = 5;
|
||||
string status = 6;
|
||||
string lifecycle_status = 7;
|
||||
int64 start_ms = 8;
|
||||
int64 end_ms = 9;
|
||||
bool all_regions = 10;
|
||||
repeated int64 region_ids = 11;
|
||||
repeated ActivityTemplateLocale locales = 12;
|
||||
repeated ActivityTemplateGift gifts = 13;
|
||||
repeated ActivityTemplateTask tasks = 14;
|
||||
int32 daily_leaderboard_size = 15;
|
||||
int32 total_leaderboard_size = 16;
|
||||
repeated ActivityTemplateRankReward rank_rewards = 17;
|
||||
repeated ActivityTemplateAsset assets = 18;
|
||||
string display_config_json = 19;
|
||||
int64 revision = 20;
|
||||
int64 published_version = 21;
|
||||
int64 created_by_admin_id = 22;
|
||||
int64 updated_by_admin_id = 23;
|
||||
int64 published_by_admin_id = 24;
|
||||
int64 created_at_ms = 25;
|
||||
int64 updated_at_ms = 26;
|
||||
int64 published_at_ms = 27;
|
||||
int64 archived_at_ms = 28;
|
||||
}
|
||||
|
||||
// ActivityTemplateSummary 是高密度列表专用投影,不携带规则、任务、素材和展示 JSON,
|
||||
// 避免后台翻页时把每个模版的完整聚合重复传输。
|
||||
message ActivityTemplateSummary {
|
||||
string app_code = 1;
|
||||
string template_id = 2;
|
||||
string template_code = 3;
|
||||
string name = 4;
|
||||
string activity_type = 5;
|
||||
string status = 6;
|
||||
string lifecycle_status = 7;
|
||||
int64 start_ms = 8;
|
||||
int64 end_ms = 9;
|
||||
bool all_regions = 10;
|
||||
repeated int64 region_ids = 11;
|
||||
int64 revision = 12;
|
||||
int64 published_version = 13;
|
||||
int64 created_by_admin_id = 14;
|
||||
int64 updated_by_admin_id = 15;
|
||||
int64 created_at_ms = 16;
|
||||
int64 updated_at_ms = 17;
|
||||
}
|
||||
|
||||
message ListActivityTemplatesRequest {
|
||||
RequestMeta meta = 1;
|
||||
string keyword = 2;
|
||||
string status = 3;
|
||||
optional int64 region_id = 4;
|
||||
int64 start_ms = 5;
|
||||
int64 end_ms = 6;
|
||||
int32 page = 7;
|
||||
int32 page_size = 8;
|
||||
optional bool all_regions = 9;
|
||||
}
|
||||
|
||||
message ListActivityTemplatesResponse {
|
||||
repeated ActivityTemplateSummary templates = 1;
|
||||
int64 total = 2;
|
||||
int64 server_time_ms = 3;
|
||||
}
|
||||
|
||||
message CreateActivityTemplateRequest {
|
||||
RequestMeta meta = 1;
|
||||
ActivityTemplate template = 2;
|
||||
int64 operator_admin_id = 3;
|
||||
}
|
||||
|
||||
message CreateActivityTemplateResponse {
|
||||
ActivityTemplate template = 1;
|
||||
}
|
||||
|
||||
message GetActivityTemplateRequest {
|
||||
RequestMeta meta = 1;
|
||||
string template_id = 2;
|
||||
}
|
||||
|
||||
message GetActivityTemplateResponse {
|
||||
ActivityTemplate template = 1;
|
||||
}
|
||||
|
||||
message UpdateActivityTemplateRequest {
|
||||
RequestMeta meta = 1;
|
||||
ActivityTemplate template = 2;
|
||||
int64 expected_revision = 3;
|
||||
int64 operator_admin_id = 4;
|
||||
}
|
||||
|
||||
message UpdateActivityTemplateResponse {
|
||||
ActivityTemplate template = 1;
|
||||
}
|
||||
|
||||
message SetActivityTemplateStatusRequest {
|
||||
RequestMeta meta = 1;
|
||||
string template_id = 2;
|
||||
string status = 3;
|
||||
int64 expected_revision = 4;
|
||||
int64 operator_admin_id = 5;
|
||||
}
|
||||
|
||||
message SetActivityTemplateStatusResponse {
|
||||
ActivityTemplate template = 1;
|
||||
}
|
||||
|
||||
message DeleteActivityTemplateRequest {
|
||||
RequestMeta meta = 1;
|
||||
string template_id = 2;
|
||||
int64 expected_revision = 3;
|
||||
int64 operator_admin_id = 4;
|
||||
}
|
||||
|
||||
message DeleteActivityTemplateResponse {
|
||||
ActivityTemplate template = 1;
|
||||
bool archived = 2;
|
||||
}
|
||||
|
||||
// ActivityTemplateVersion 是每次发布形成的不可变完整配置快照。
|
||||
message ActivityTemplateVersion {
|
||||
string template_id = 1;
|
||||
int64 version_no = 2;
|
||||
ActivityTemplate snapshot = 3;
|
||||
int64 published_by_admin_id = 4;
|
||||
int64 published_at_ms = 5;
|
||||
// 实际运行窗口来自 activity_template_runtime_versions;停用后 runtime_to_ms 固化,重发会创建新版本窗口。
|
||||
int64 runtime_from_ms = 6;
|
||||
int64 runtime_to_ms = 7;
|
||||
}
|
||||
|
||||
message ListActivityTemplateVersionsRequest {
|
||||
RequestMeta meta = 1;
|
||||
string template_id = 2;
|
||||
int32 page = 3;
|
||||
int32 page_size = 4;
|
||||
}
|
||||
|
||||
message ListActivityTemplateVersionsResponse {
|
||||
repeated ActivityTemplateVersion versions = 1;
|
||||
int64 total = 2;
|
||||
}
|
||||
|
||||
// CloneActivityTemplateRequest 从当前配置或指定发布版本复制为新草稿;
|
||||
// source_version=0 表示复制当前配置,新的 template_code 必须在 App 内唯一。
|
||||
message CloneActivityTemplateRequest {
|
||||
RequestMeta meta = 1;
|
||||
string source_template_id = 2;
|
||||
int64 source_version = 3;
|
||||
string template_code = 4;
|
||||
string name = 5;
|
||||
int64 operator_admin_id = 6;
|
||||
}
|
||||
|
||||
message CloneActivityTemplateResponse {
|
||||
ActivityTemplate template = 1;
|
||||
}
|
||||
|
||||
message ActivityTemplateValidationIssue {
|
||||
string field = 1;
|
||||
string code = 2;
|
||||
string message = 3;
|
||||
}
|
||||
|
||||
// ValidateActivityTemplateRequest 让编辑页在正式保存或发布前复用 owner service 的校验规则。
|
||||
message ValidateActivityTemplateRequest {
|
||||
RequestMeta meta = 1;
|
||||
ActivityTemplate template = 2;
|
||||
bool for_publish = 3;
|
||||
}
|
||||
|
||||
message ValidateActivityTemplateResponse {
|
||||
bool valid = 1;
|
||||
repeated ActivityTemplateValidationIssue issues = 2;
|
||||
}
|
||||
|
||||
// ActivityTemplateRuntime 描述一个用户当前命中的已发布不可变版本。
|
||||
// runtime_from_ms/runtime_to_ms 是该版本的实际启停窗口,活动排期仍以 template 的 [start_ms,end_ms) 为准。
|
||||
message ActivityTemplateRuntime {
|
||||
ActivityTemplate template = 1;
|
||||
int64 version_no = 2;
|
||||
int64 runtime_from_ms = 3;
|
||||
int64 runtime_to_ms = 4;
|
||||
int64 server_time_ms = 5;
|
||||
}
|
||||
|
||||
// ActivityTemplateRuntimeTask 是发布版本内一档 UTC 每日任务及当前用户进度。
|
||||
message ActivityTemplateRuntimeTask {
|
||||
string template_id = 1;
|
||||
string template_code = 2;
|
||||
int64 version_no = 3;
|
||||
string task_day = 4;
|
||||
string task_key = 5;
|
||||
string task_type = 6;
|
||||
int64 target_value = 7;
|
||||
int64 progress_value = 8;
|
||||
int64 reward_resource_group_id = 9;
|
||||
string status = 10;
|
||||
bool claimable = 11;
|
||||
int64 completed_at_ms = 12;
|
||||
int64 claimed_at_ms = 13;
|
||||
int32 sort_order = 14;
|
||||
string reward_snapshot_id = 15;
|
||||
string reward_snapshot_hash = 16;
|
||||
string reward_name = 17;
|
||||
repeated ActivityTemplateRewardItem reward_items = 18;
|
||||
}
|
||||
|
||||
// ActivityTemplateTaskClaim 是 activity-service 与 wallet-service 之间可审计、可重放的领奖事实。
|
||||
message ActivityTemplateTaskClaim {
|
||||
string claim_id = 1;
|
||||
string command_id = 2;
|
||||
string template_id = 3;
|
||||
string template_code = 4;
|
||||
int64 version_no = 5;
|
||||
string task_day = 6;
|
||||
string task_key = 7;
|
||||
int64 user_id = 8;
|
||||
int64 reward_resource_group_id = 9;
|
||||
string wallet_command_id = 10;
|
||||
string wallet_grant_id = 11;
|
||||
string status = 12;
|
||||
string failure_reason = 13;
|
||||
int32 attempt_count = 14;
|
||||
int64 created_at_ms = 15;
|
||||
int64 updated_at_ms = 16;
|
||||
int64 granted_at_ms = 17;
|
||||
string reward_snapshot_id = 18;
|
||||
string reward_name = 19;
|
||||
repeated ActivityTemplateRewardItem reward_items = 20;
|
||||
string reward_snapshot_hash = 21;
|
||||
// earning_region_id/attribution_at_ms are frozen from the progress event that completed the task.
|
||||
int64 earning_region_id = 22;
|
||||
int64 attribution_at_ms = 23;
|
||||
}
|
||||
|
||||
// ActivityTemplateLeaderboardEntry 同时用于实时榜和结算后冻结快照。
|
||||
message ActivityTemplateLeaderboardEntry {
|
||||
int32 rank_no = 1;
|
||||
int64 user_id = 2;
|
||||
int64 score = 3;
|
||||
int64 gift_count = 4;
|
||||
int64 coin_amount = 5;
|
||||
int64 first_scored_at_ms = 6;
|
||||
bool snapshot = 7;
|
||||
}
|
||||
|
||||
message GetCurrentActivityTemplateRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 user_id = 2;
|
||||
int64 region_id = 3;
|
||||
string locale = 4;
|
||||
}
|
||||
|
||||
message GetCurrentActivityTemplateResponse {
|
||||
ActivityTemplateRuntime runtime = 1;
|
||||
bool found = 2;
|
||||
}
|
||||
|
||||
// VisitActivityTemplate 是 daily_visit 的唯一事实入口;command_id 负责请求重放,
|
||||
// 自然幂等键仍是用户+发布版本+UTC task_day。
|
||||
message VisitActivityTemplateRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 user_id = 2;
|
||||
int64 region_id = 3;
|
||||
string template_code = 4;
|
||||
string command_id = 5;
|
||||
// version_no comes from GetCurrentActivityTemplate and prevents disable/re-publish races.
|
||||
int64 version_no = 6;
|
||||
}
|
||||
|
||||
message VisitActivityTemplateResponse {
|
||||
ActivityTemplateRuntime runtime = 1;
|
||||
repeated ActivityTemplateRuntimeTask tasks = 2;
|
||||
bool first_visit = 3;
|
||||
}
|
||||
|
||||
message ListActivityTemplateTasksRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 user_id = 2;
|
||||
int64 region_id = 3;
|
||||
string template_code = 4;
|
||||
string task_day = 5;
|
||||
// version_no 与 task_day 同时提供时读取该用户确实参与过的历史不可变任务版本。
|
||||
int64 version_no = 6;
|
||||
}
|
||||
|
||||
message ListActivityTemplateTasksResponse {
|
||||
ActivityTemplateRuntime runtime = 1;
|
||||
repeated ActivityTemplateRuntimeTask tasks = 2;
|
||||
int64 next_refresh_at_ms = 3;
|
||||
}
|
||||
|
||||
message ClaimActivityTemplateTaskRewardRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 user_id = 2;
|
||||
int64 region_id = 3;
|
||||
string template_code = 4;
|
||||
string task_day = 5;
|
||||
string task_key = 6;
|
||||
string command_id = 7;
|
||||
// version_no 来自 tasks/runtime 响应,强制把同日重发前后的任务领奖绑定到正确不可变版本。
|
||||
int64 version_no = 8;
|
||||
}
|
||||
|
||||
message ClaimActivityTemplateTaskRewardResponse {
|
||||
ActivityTemplateTaskClaim claim = 1;
|
||||
bool claimed = 2;
|
||||
}
|
||||
|
||||
message ListActivityTemplateLeaderboardRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 user_id = 2;
|
||||
int64 region_id = 3;
|
||||
string template_code = 4;
|
||||
string board_type = 5;
|
||||
string period_key = 6;
|
||||
int32 page = 7;
|
||||
int32 page_size = 8;
|
||||
// version_no > 0 时严格读取该发布版本;省略仅用于当前/最近版本兼容查询。
|
||||
int64 version_no = 9;
|
||||
}
|
||||
|
||||
message ListActivityTemplateLeaderboardResponse {
|
||||
ActivityTemplateRuntime runtime = 1;
|
||||
string board_type = 2;
|
||||
string period_key = 3;
|
||||
int64 period_start_ms = 4;
|
||||
int64 period_end_ms = 5;
|
||||
string period_status = 6;
|
||||
repeated ActivityTemplateLeaderboardEntry entries = 7;
|
||||
ActivityTemplateLeaderboardEntry my_entry = 8;
|
||||
int64 total = 9;
|
||||
// settle_after_ms 是含迟到窗口的排他结算水位;到达该毫秒后榜单不再接收事件。
|
||||
int64 settle_after_ms = 10;
|
||||
}
|
||||
|
||||
// ActivityTemplateRankPeriod 是 UTC 日榜或活动总榜的冻结/发奖状态机。
|
||||
message ActivityTemplateRankPeriod {
|
||||
string template_id = 1;
|
||||
string template_code = 2;
|
||||
int64 version_no = 3;
|
||||
string board_type = 4;
|
||||
string period_key = 5;
|
||||
int64 period_start_ms = 6;
|
||||
int64 period_end_ms = 7;
|
||||
// pending -> settling(物化中)-> rewarding(奖励 job 独立推进)-> settled;也可 cancelled。
|
||||
string status = 8;
|
||||
int64 settled_at_ms = 9;
|
||||
int64 settle_after_ms = 10;
|
||||
}
|
||||
|
||||
message ActivityTemplateRewardJob {
|
||||
string reward_job_id = 1;
|
||||
string template_id = 2;
|
||||
string template_code = 3;
|
||||
int64 version_no = 4;
|
||||
string board_type = 5;
|
||||
string period_key = 6;
|
||||
int32 rank_no = 7;
|
||||
int64 user_id = 8;
|
||||
int64 score = 9;
|
||||
int64 resource_group_id = 10;
|
||||
string wallet_command_id = 11;
|
||||
string wallet_grant_id = 12;
|
||||
string status = 13;
|
||||
string failure_reason = 14;
|
||||
int32 attempt_count = 15;
|
||||
int64 created_at_ms = 16;
|
||||
int64 updated_at_ms = 17;
|
||||
int64 granted_at_ms = 18;
|
||||
string reward_snapshot_id = 19;
|
||||
string reward_snapshot_hash = 20;
|
||||
string reward_name = 21;
|
||||
repeated ActivityTemplateRewardItem reward_items = 22;
|
||||
int32 max_attempts = 23;
|
||||
int64 next_retry_at_ms = 24;
|
||||
}
|
||||
|
||||
message AdminListActivityTemplateLeaderboardRequest {
|
||||
RequestMeta meta = 1;
|
||||
string template_id = 2;
|
||||
int64 version_no = 3;
|
||||
string board_type = 4;
|
||||
string period_key = 5;
|
||||
int32 page = 6;
|
||||
int32 page_size = 7;
|
||||
}
|
||||
|
||||
message AdminListActivityTemplateLeaderboardResponse {
|
||||
ActivityTemplateRankPeriod period = 1;
|
||||
repeated ActivityTemplateLeaderboardEntry entries = 2;
|
||||
int64 total = 3;
|
||||
}
|
||||
|
||||
message ListActivityTemplateRewardJobsRequest {
|
||||
RequestMeta meta = 1;
|
||||
string template_id = 2;
|
||||
int64 version_no = 3;
|
||||
string board_type = 4;
|
||||
string period_key = 5;
|
||||
string status = 6;
|
||||
int32 page = 7;
|
||||
int32 page_size = 8;
|
||||
}
|
||||
|
||||
message ListActivityTemplateRewardJobsResponse {
|
||||
repeated ActivityTemplateRewardJob jobs = 1;
|
||||
int64 total = 2;
|
||||
}
|
||||
|
||||
message RetryActivityTemplateRewardJobRequest {
|
||||
RequestMeta meta = 1;
|
||||
string template_id = 2;
|
||||
string reward_job_id = 3;
|
||||
int64 operator_admin_id = 4;
|
||||
}
|
||||
|
||||
message RetryActivityTemplateRewardJobResponse {
|
||||
ActivityTemplateRewardJob job = 1;
|
||||
}
|
||||
|
||||
message ListActivityTemplateTaskClaimsRequest {
|
||||
RequestMeta meta = 1;
|
||||
string template_id = 2;
|
||||
int64 version_no = 3;
|
||||
string task_day = 4;
|
||||
string task_key = 5;
|
||||
int64 user_id = 6;
|
||||
string status = 7;
|
||||
int32 page = 8;
|
||||
int32 page_size = 9;
|
||||
}
|
||||
|
||||
message ListActivityTemplateTaskClaimsResponse {
|
||||
repeated ActivityTemplateTaskClaim claims = 1;
|
||||
int64 total = 2;
|
||||
}
|
||||
|
||||
message RetryActivityTemplateTaskClaimRequest {
|
||||
RequestMeta meta = 1;
|
||||
string template_id = 2;
|
||||
string claim_id = 3;
|
||||
int64 operator_admin_id = 4;
|
||||
}
|
||||
|
||||
message RetryActivityTemplateTaskClaimResponse {
|
||||
ActivityTemplateTaskClaim claim = 1;
|
||||
}
|
||||
|
||||
// ActivityService 只暴露同步查询,不把事件消费伪装成 RPC。
|
||||
service ActivityService {
|
||||
rpc PingActivity(PingActivityRequest) returns (PingActivityResponse);
|
||||
@ -2412,6 +2939,16 @@ service ActivityCronService {
|
||||
rpc ProcessWeeklyStarSettlementBatch(CronBatchRequest) returns (CronBatchResponse);
|
||||
rpc ProcessCPWeeklyRankSettlementBatch(CronBatchRequest) returns (CronBatchResponse);
|
||||
rpc ProcessAgencyOpeningSettlementBatch(CronBatchRequest) returns (CronBatchResponse);
|
||||
rpc ProcessActivityTemplateSettlementBatch(CronBatchRequest) returns (CronBatchResponse);
|
||||
}
|
||||
|
||||
// ActivityTemplateRuntimeService 是 App/H5 读取已发布版本、任务进度、榜单和领奖的 owner 入口。
|
||||
service ActivityTemplateRuntimeService {
|
||||
rpc GetCurrentActivityTemplate(GetCurrentActivityTemplateRequest) returns (GetCurrentActivityTemplateResponse);
|
||||
rpc VisitActivityTemplate(VisitActivityTemplateRequest) returns (VisitActivityTemplateResponse);
|
||||
rpc ListActivityTemplateTasks(ListActivityTemplateTasksRequest) returns (ListActivityTemplateTasksResponse);
|
||||
rpc ClaimActivityTemplateTaskReward(ClaimActivityTemplateTaskRewardRequest) returns (ClaimActivityTemplateTaskRewardResponse);
|
||||
rpc ListActivityTemplateLeaderboard(ListActivityTemplateLeaderboardRequest) returns (ListActivityTemplateLeaderboardResponse);
|
||||
}
|
||||
|
||||
// TaskService 拥有 App 任务查询、事件进度消费和领奖状态。
|
||||
@ -2582,6 +3119,25 @@ service AdminAgencyOpeningService {
|
||||
rpc ReviewAgencyOpeningApplication(ReviewAgencyOpeningApplicationRequest) returns (ReviewAgencyOpeningApplicationResponse);
|
||||
}
|
||||
|
||||
// AdminActivityTemplateService 是后台活动模版访问 activity-service 的唯一入口。
|
||||
// admin-server 只做鉴权、协议转换和审计,不能直写 activity-service 的配置表。
|
||||
service AdminActivityTemplateService {
|
||||
rpc ListActivityTemplates(ListActivityTemplatesRequest) returns (ListActivityTemplatesResponse);
|
||||
rpc CreateActivityTemplate(CreateActivityTemplateRequest) returns (CreateActivityTemplateResponse);
|
||||
rpc GetActivityTemplate(GetActivityTemplateRequest) returns (GetActivityTemplateResponse);
|
||||
rpc UpdateActivityTemplate(UpdateActivityTemplateRequest) returns (UpdateActivityTemplateResponse);
|
||||
rpc SetActivityTemplateStatus(SetActivityTemplateStatusRequest) returns (SetActivityTemplateStatusResponse);
|
||||
rpc DeleteActivityTemplate(DeleteActivityTemplateRequest) returns (DeleteActivityTemplateResponse);
|
||||
rpc ListActivityTemplateVersions(ListActivityTemplateVersionsRequest) returns (ListActivityTemplateVersionsResponse);
|
||||
rpc CloneActivityTemplate(CloneActivityTemplateRequest) returns (CloneActivityTemplateResponse);
|
||||
rpc ValidateActivityTemplate(ValidateActivityTemplateRequest) returns (ValidateActivityTemplateResponse);
|
||||
rpc ListActivityTemplateLeaderboard(AdminListActivityTemplateLeaderboardRequest) returns (AdminListActivityTemplateLeaderboardResponse);
|
||||
rpc ListActivityTemplateRewardJobs(ListActivityTemplateRewardJobsRequest) returns (ListActivityTemplateRewardJobsResponse);
|
||||
rpc RetryActivityTemplateRewardJob(RetryActivityTemplateRewardJobRequest) returns (RetryActivityTemplateRewardJobResponse);
|
||||
rpc ListActivityTemplateTaskClaims(ListActivityTemplateTaskClaimsRequest) returns (ListActivityTemplateTaskClaimsResponse);
|
||||
rpc RetryActivityTemplateTaskClaim(RetryActivityTemplateTaskClaimRequest) returns (RetryActivityTemplateTaskClaimResponse);
|
||||
}
|
||||
|
||||
// SevenDayCheckInService 拥有 App 七日签到查询和签到命令。
|
||||
service SevenDayCheckInService {
|
||||
rpc GetSevenDayCheckInStatus(GetSevenDayCheckInStatusRequest) returns (GetSevenDayCheckInStatusResponse);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.1
|
||||
// protoc v7.35.0
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v5.29.2
|
||||
// source: proto/events/luckygift/v1/events.proto
|
||||
|
||||
package luckygifteventsv1
|
||||
@ -11,6 +11,7 @@ import (
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -23,17 +24,16 @@ const (
|
||||
// EventEnvelope 是 lucky-gift-service 发布 owner outbox 事实的稳定信封。
|
||||
// occurred_at_ms 固定为抽奖事实创建时的 Unix epoch milliseconds;MQ 实际发送时间不改变业务发生时间。
|
||||
type EventEnvelope struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
EventId string `protobuf:"bytes,1,opt,name=event_id,json=eventId,proto3" json:"event_id,omitempty"`
|
||||
EventType string `protobuf:"bytes,2,opt,name=event_type,json=eventType,proto3" json:"event_type,omitempty"`
|
||||
AppCode string `protobuf:"bytes,3,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"`
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
EventId string `protobuf:"bytes,1,opt,name=event_id,json=eventId,proto3" json:"event_id,omitempty"`
|
||||
EventType string `protobuf:"bytes,2,opt,name=event_type,json=eventType,proto3" json:"event_type,omitempty"`
|
||||
AppCode string `protobuf:"bytes,3,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"`
|
||||
// draw_id 是本次送礼命令的聚合中奖标识;批量子抽明细仍只保存在 lucky owner 数据库,不能放大 MQ 负载。
|
||||
DrawId string `protobuf:"bytes,4,opt,name=draw_id,json=drawId,proto3" json:"draw_id,omitempty"`
|
||||
OccurredAtMs int64 `protobuf:"varint,5,opt,name=occurred_at_ms,json=occurredAtMs,proto3" json:"occurred_at_ms,omitempty"`
|
||||
Body []byte `protobuf:"bytes,6,opt,name=body,proto3" json:"body,omitempty"`
|
||||
DrawId string `protobuf:"bytes,4,opt,name=draw_id,json=drawId,proto3" json:"draw_id,omitempty"`
|
||||
OccurredAtMs int64 `protobuf:"varint,5,opt,name=occurred_at_ms,json=occurredAtMs,proto3" json:"occurred_at_ms,omitempty"`
|
||||
Body []byte `protobuf:"bytes,6,opt,name=body,proto3" json:"body,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *EventEnvelope) Reset() {
|
||||
@ -112,18 +112,15 @@ func (x *EventEnvelope) GetBody() []byte {
|
||||
// lucky-gift-service 必须先完成 wallet-service 入账和本地 granted 收敛,再发布本事件;
|
||||
// room-service 与 activity-service 只从该事实派生房内 IM 和区域飘屏,不能反向改变抽奖或账务状态。
|
||||
type LuckyGiftDrawn struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
DrawId string `protobuf:"bytes,1,opt,name=draw_id,json=drawId,proto3" json:"draw_id,omitempty"`
|
||||
CommandId string `protobuf:"bytes,2,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"`
|
||||
RoomId string `protobuf:"bytes,3,opt,name=room_id,json=roomId,proto3" json:"room_id,omitempty"`
|
||||
PoolId string `protobuf:"bytes,4,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"`
|
||||
GiftId string `protobuf:"bytes,5,opt,name=gift_id,json=giftId,proto3" json:"gift_id,omitempty"`
|
||||
GiftCount int32 `protobuf:"varint,6,opt,name=gift_count,json=giftCount,proto3" json:"gift_count,omitempty"`
|
||||
SenderUserId int64 `protobuf:"varint,7,opt,name=sender_user_id,json=senderUserId,proto3" json:"sender_user_id,omitempty"`
|
||||
TargetUserId int64 `protobuf:"varint,8,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
DrawId string `protobuf:"bytes,1,opt,name=draw_id,json=drawId,proto3" json:"draw_id,omitempty"`
|
||||
CommandId string `protobuf:"bytes,2,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"`
|
||||
RoomId string `protobuf:"bytes,3,opt,name=room_id,json=roomId,proto3" json:"room_id,omitempty"`
|
||||
PoolId string `protobuf:"bytes,4,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"`
|
||||
GiftId string `protobuf:"bytes,5,opt,name=gift_id,json=giftId,proto3" json:"gift_id,omitempty"`
|
||||
GiftCount int32 `protobuf:"varint,6,opt,name=gift_count,json=giftCount,proto3" json:"gift_count,omitempty"`
|
||||
SenderUserId int64 `protobuf:"varint,7,opt,name=sender_user_id,json=senderUserId,proto3" json:"sender_user_id,omitempty"`
|
||||
TargetUserId int64 `protobuf:"varint,8,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
|
||||
// 展示资料是送礼入口固化的快照;消费者不应为了飘屏高频同步反查 user-service。
|
||||
SenderName string `protobuf:"bytes,9,opt,name=sender_name,json=senderName,proto3" json:"sender_name,omitempty"`
|
||||
SenderAvatar string `protobuf:"bytes,10,opt,name=sender_avatar,json=senderAvatar,proto3" json:"sender_avatar,omitempty"`
|
||||
@ -147,6 +144,8 @@ type LuckyGiftDrawn struct {
|
||||
// 两个时间均为 UTC Unix epoch milliseconds:抽奖发生时间与钱包返奖确认时间不能混用。
|
||||
DrawCreatedAtMs int64 `protobuf:"varint,26,opt,name=draw_created_at_ms,json=drawCreatedAtMs,proto3" json:"draw_created_at_ms,omitempty"`
|
||||
RewardGrantedAtMs int64 `protobuf:"varint,27,opt,name=reward_granted_at_ms,json=rewardGrantedAtMs,proto3" json:"reward_granted_at_ms,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *LuckyGiftDrawn) Reset() {
|
||||
@ -370,105 +369,60 @@ func (x *LuckyGiftDrawn) GetRewardGrantedAtMs() int64 {
|
||||
|
||||
var File_proto_events_luckygift_v1_events_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_proto_events_luckygift_v1_events_proto_rawDesc = []byte{
|
||||
0x0a, 0x26, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x6c,
|
||||
0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x76, 0x65, 0x6e,
|
||||
0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x19, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e,
|
||||
0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74,
|
||||
0x2e, 0x76, 0x31, 0x22, 0xb7, 0x01, 0x0a, 0x0d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x76,
|
||||
0x65, 0x6c, 0x6f, 0x70, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x69,
|
||||
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x49, 0x64,
|
||||
0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12,
|
||||
0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x72,
|
||||
0x61, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x72, 0x61,
|
||||
0x77, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x6f, 0x63, 0x63, 0x75, 0x72, 0x72, 0x65, 0x64, 0x5f,
|
||||
0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6f, 0x63, 0x63,
|
||||
0x75, 0x72, 0x72, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64,
|
||||
0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x22, 0xab, 0x08,
|
||||
0x0a, 0x0e, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x6e,
|
||||
0x12, 0x17, 0x0a, 0x07, 0x64, 0x72, 0x61, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x06, 0x64, 0x72, 0x61, 0x77, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d,
|
||||
0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63,
|
||||
0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x6f, 0x6f, 0x6d,
|
||||
0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x6f, 0x6d, 0x49,
|
||||
0x64, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6f, 0x6f, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x06, 0x70, 0x6f, 0x6f, 0x6c, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x67, 0x69,
|
||||
0x66, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x69, 0x66,
|
||||
0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e,
|
||||
0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x67, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x75,
|
||||
0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65,
|
||||
0x72, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x6e, 0x64,
|
||||
0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67,
|
||||
0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03,
|
||||
0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1f,
|
||||
0x0a, 0x0b, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x09, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12,
|
||||
0x23, 0x0a, 0x0d, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72,
|
||||
0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x41, 0x76,
|
||||
0x61, 0x74, 0x61, 0x72, 0x12, 0x33, 0x0a, 0x16, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x64,
|
||||
0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0b,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x44, 0x69, 0x73, 0x70,
|
||||
0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x40, 0x0a, 0x1d, 0x73, 0x65, 0x6e,
|
||||
0x64, 0x65, 0x72, 0x5f, 0x70, 0x72, 0x65, 0x74, 0x74, 0x79, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c,
|
||||
0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x19, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x50, 0x72, 0x65, 0x74, 0x74, 0x79, 0x44, 0x69,
|
||||
0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x11, 0x76,
|
||||
0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64,
|
||||
0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x52,
|
||||
0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x75, 0x6e, 0x74,
|
||||
0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x6f, 0x75,
|
||||
0x6e, 0x74, 0x72, 0x79, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x69, 0x6e, 0x5f, 0x73,
|
||||
0x70, 0x65, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x6f, 0x69, 0x6e,
|
||||
0x53, 0x70, 0x65, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x75, 0x6c, 0x65, 0x5f, 0x76, 0x65,
|
||||
0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x10, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x72, 0x75, 0x6c,
|
||||
0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x27, 0x0a, 0x0f, 0x65, 0x78, 0x70, 0x65,
|
||||
0x72, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x5f, 0x70, 0x6f, 0x6f, 0x6c, 0x18, 0x11, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x0e, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x50, 0x6f, 0x6f,
|
||||
0x6c, 0x12, 0x28, 0x0a, 0x10, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69,
|
||||
0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x65, 0x6c,
|
||||
0x65, 0x63, 0x74, 0x65, 0x64, 0x54, 0x69, 0x65, 0x72, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x6d,
|
||||
0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x69, 0x65, 0x72, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x13, 0x20,
|
||||
0x01, 0x28, 0x03, 0x52, 0x0d, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x69, 0x65, 0x72, 0x50,
|
||||
0x70, 0x6d, 0x12, 0x2a, 0x0a, 0x11, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x72, 0x65, 0x77, 0x61, 0x72,
|
||||
0x64, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x62,
|
||||
0x61, 0x73, 0x65, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x43, 0x6f, 0x69, 0x6e, 0x73, 0x12, 0x34,
|
||||
0x0a, 0x16, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x72, 0x65, 0x77, 0x61,
|
||||
0x72, 0x64, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73, 0x18, 0x15, 0x20, 0x01, 0x28, 0x03, 0x52, 0x14,
|
||||
0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x43,
|
||||
0x6f, 0x69, 0x6e, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x74, 0x61, 0x67, 0x65, 0x5f, 0x66, 0x65,
|
||||
0x65, 0x64, 0x62, 0x61, 0x63, 0x6b, 0x18, 0x16, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x73, 0x74,
|
||||
0x61, 0x67, 0x65, 0x46, 0x65, 0x65, 0x64, 0x62, 0x61, 0x63, 0x6b, 0x12, 0x27, 0x0a, 0x0f, 0x68,
|
||||
0x69, 0x67, 0x68, 0x5f, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x69, 0x65, 0x72, 0x18, 0x17,
|
||||
0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x68, 0x69, 0x67, 0x68, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x70,
|
||||
0x6c, 0x69, 0x65, 0x72, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x73,
|
||||
0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x18, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x77,
|
||||
0x61, 0x72, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x77, 0x61, 0x6c,
|
||||
0x6c, 0x65, 0x74, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f,
|
||||
0x69, 0x64, 0x18, 0x19, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74,
|
||||
0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x2b, 0x0a,
|
||||
0x12, 0x64, 0x72, 0x61, 0x77, 0x5f, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74,
|
||||
0x5f, 0x6d, 0x73, 0x18, 0x1a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x64, 0x72, 0x61, 0x77, 0x43,
|
||||
0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x2f, 0x0a, 0x14, 0x72, 0x65,
|
||||
0x77, 0x61, 0x72, 0x64, 0x5f, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f,
|
||||
0x6d, 0x73, 0x18, 0x1b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64,
|
||||
0x47, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x42, 0x3d, 0x5a, 0x3b, 0x68,
|
||||
0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x6c, 0x75, 0x63, 0x6b,
|
||||
0x79, 0x67, 0x69, 0x66, 0x74, 0x2f, 0x76, 0x31, 0x3b, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69,
|
||||
0x66, 0x74, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x33,
|
||||
}
|
||||
const file_proto_events_luckygift_v1_events_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"&proto/events/luckygift/v1/events.proto\x12\x19hyapp.events.luckygift.v1\"\xb7\x01\n" +
|
||||
"\rEventEnvelope\x12\x19\n" +
|
||||
"\bevent_id\x18\x01 \x01(\tR\aeventId\x12\x1d\n" +
|
||||
"\n" +
|
||||
"event_type\x18\x02 \x01(\tR\teventType\x12\x19\n" +
|
||||
"\bapp_code\x18\x03 \x01(\tR\aappCode\x12\x17\n" +
|
||||
"\adraw_id\x18\x04 \x01(\tR\x06drawId\x12$\n" +
|
||||
"\x0eoccurred_at_ms\x18\x05 \x01(\x03R\foccurredAtMs\x12\x12\n" +
|
||||
"\x04body\x18\x06 \x01(\fR\x04body\"\xab\b\n" +
|
||||
"\x0eLuckyGiftDrawn\x12\x17\n" +
|
||||
"\adraw_id\x18\x01 \x01(\tR\x06drawId\x12\x1d\n" +
|
||||
"\n" +
|
||||
"command_id\x18\x02 \x01(\tR\tcommandId\x12\x17\n" +
|
||||
"\aroom_id\x18\x03 \x01(\tR\x06roomId\x12\x17\n" +
|
||||
"\apool_id\x18\x04 \x01(\tR\x06poolId\x12\x17\n" +
|
||||
"\agift_id\x18\x05 \x01(\tR\x06giftId\x12\x1d\n" +
|
||||
"\n" +
|
||||
"gift_count\x18\x06 \x01(\x05R\tgiftCount\x12$\n" +
|
||||
"\x0esender_user_id\x18\a \x01(\x03R\fsenderUserId\x12$\n" +
|
||||
"\x0etarget_user_id\x18\b \x01(\x03R\ftargetUserId\x12\x1f\n" +
|
||||
"\vsender_name\x18\t \x01(\tR\n" +
|
||||
"senderName\x12#\n" +
|
||||
"\rsender_avatar\x18\n" +
|
||||
" \x01(\tR\fsenderAvatar\x123\n" +
|
||||
"\x16sender_display_user_id\x18\v \x01(\tR\x13senderDisplayUserId\x12@\n" +
|
||||
"\x1dsender_pretty_display_user_id\x18\f \x01(\tR\x19senderPrettyDisplayUserId\x12*\n" +
|
||||
"\x11visible_region_id\x18\r \x01(\x03R\x0fvisibleRegionId\x12\x1d\n" +
|
||||
"\n" +
|
||||
"country_id\x18\x0e \x01(\x03R\tcountryId\x12\x1d\n" +
|
||||
"\n" +
|
||||
"coin_spent\x18\x0f \x01(\x03R\tcoinSpent\x12!\n" +
|
||||
"\frule_version\x18\x10 \x01(\x03R\vruleVersion\x12'\n" +
|
||||
"\x0fexperience_pool\x18\x11 \x01(\tR\x0eexperiencePool\x12(\n" +
|
||||
"\x10selected_tier_id\x18\x12 \x01(\tR\x0eselectedTierId\x12%\n" +
|
||||
"\x0emultiplier_ppm\x18\x13 \x01(\x03R\rmultiplierPpm\x12*\n" +
|
||||
"\x11base_reward_coins\x18\x14 \x01(\x03R\x0fbaseRewardCoins\x124\n" +
|
||||
"\x16effective_reward_coins\x18\x15 \x01(\x03R\x14effectiveRewardCoins\x12%\n" +
|
||||
"\x0estage_feedback\x18\x16 \x01(\bR\rstageFeedback\x12'\n" +
|
||||
"\x0fhigh_multiplier\x18\x17 \x01(\bR\x0ehighMultiplier\x12#\n" +
|
||||
"\rreward_status\x18\x18 \x01(\tR\frewardStatus\x122\n" +
|
||||
"\x15wallet_transaction_id\x18\x19 \x01(\tR\x13walletTransactionId\x12+\n" +
|
||||
"\x12draw_created_at_ms\x18\x1a \x01(\x03R\x0fdrawCreatedAtMs\x12/\n" +
|
||||
"\x14reward_granted_at_ms\x18\x1b \x01(\x03R\x11rewardGrantedAtMsB=Z;hyapp.local/api/proto/events/luckygift/v1;luckygifteventsv1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_proto_events_luckygift_v1_events_proto_rawDescOnce sync.Once
|
||||
file_proto_events_luckygift_v1_events_proto_rawDescData = file_proto_events_luckygift_v1_events_proto_rawDesc
|
||||
file_proto_events_luckygift_v1_events_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_proto_events_luckygift_v1_events_proto_rawDescGZIP() []byte {
|
||||
file_proto_events_luckygift_v1_events_proto_rawDescOnce.Do(func() {
|
||||
file_proto_events_luckygift_v1_events_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_events_luckygift_v1_events_proto_rawDescData)
|
||||
file_proto_events_luckygift_v1_events_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_events_luckygift_v1_events_proto_rawDesc), len(file_proto_events_luckygift_v1_events_proto_rawDesc)))
|
||||
})
|
||||
return file_proto_events_luckygift_v1_events_proto_rawDescData
|
||||
}
|
||||
@ -495,7 +449,7 @@ func file_proto_events_luckygift_v1_events_proto_init() {
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_proto_events_luckygift_v1_events_proto_rawDesc,
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_events_luckygift_v1_events_proto_rawDesc), len(file_proto_events_luckygift_v1_events_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 2,
|
||||
NumExtensions: 0,
|
||||
@ -506,7 +460,6 @@ func file_proto_events_luckygift_v1_events_proto_init() {
|
||||
MessageInfos: file_proto_events_luckygift_v1_events_proto_msgTypes,
|
||||
}.Build()
|
||||
File_proto_events_luckygift_v1_events_proto = out.File
|
||||
file_proto_events_luckygift_v1_events_proto_rawDesc = nil
|
||||
file_proto_events_luckygift_v1_events_proto_goTypes = nil
|
||||
file_proto_events_luckygift_v1_events_proto_depIdxs = nil
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v7.35.0
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v5.29.2
|
||||
// source: proto/game/v1/game.proto
|
||||
|
||||
package gamev1
|
||||
@ -271,55 +271,55 @@ type GameAppServiceServer interface {
|
||||
type UnimplementedGameAppServiceServer struct{}
|
||||
|
||||
func (UnimplementedGameAppServiceServer) ListGames(context.Context, *ListGamesRequest) (*ListGamesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListGames not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListGames not implemented")
|
||||
}
|
||||
func (UnimplementedGameAppServiceServer) ListRecentGames(context.Context, *ListRecentGamesRequest) (*ListGamesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRecentGames not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRecentGames not implemented")
|
||||
}
|
||||
func (UnimplementedGameAppServiceServer) ListExploreWinners(context.Context, *ListExploreWinnersRequest) (*ListExploreWinnersResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListExploreWinners not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListExploreWinners not implemented")
|
||||
}
|
||||
func (UnimplementedGameAppServiceServer) GetBridgeScript(context.Context, *GetBridgeScriptRequest) (*GetBridgeScriptResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetBridgeScript not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetBridgeScript not implemented")
|
||||
}
|
||||
func (UnimplementedGameAppServiceServer) LaunchGame(context.Context, *LaunchGameRequest) (*LaunchGameResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method LaunchGame not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method LaunchGame not implemented")
|
||||
}
|
||||
func (UnimplementedGameAppServiceServer) GetDiceConfig(context.Context, *GetDiceConfigRequest) (*DiceConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetDiceConfig not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetDiceConfig not implemented")
|
||||
}
|
||||
func (UnimplementedGameAppServiceServer) MatchDice(context.Context, *MatchDiceRequest) (*DiceMatchResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method MatchDice not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method MatchDice not implemented")
|
||||
}
|
||||
func (UnimplementedGameAppServiceServer) CreateDiceMatch(context.Context, *CreateDiceMatchRequest) (*DiceMatchResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateDiceMatch not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateDiceMatch not implemented")
|
||||
}
|
||||
func (UnimplementedGameAppServiceServer) JoinDiceMatch(context.Context, *JoinDiceMatchRequest) (*DiceMatchResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method JoinDiceMatch not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method JoinDiceMatch not implemented")
|
||||
}
|
||||
func (UnimplementedGameAppServiceServer) GetDiceMatch(context.Context, *GetDiceMatchRequest) (*DiceMatchResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetDiceMatch not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetDiceMatch not implemented")
|
||||
}
|
||||
func (UnimplementedGameAppServiceServer) RollDiceMatch(context.Context, *RollDiceMatchRequest) (*DiceMatchResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RollDiceMatch not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RollDiceMatch not implemented")
|
||||
}
|
||||
func (UnimplementedGameAppServiceServer) CancelDiceMatch(context.Context, *CancelDiceMatchRequest) (*DiceMatchResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CancelDiceMatch not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CancelDiceMatch not implemented")
|
||||
}
|
||||
func (UnimplementedGameAppServiceServer) GetRoomRPSConfig(context.Context, *GetRoomRPSConfigRequest) (*RoomRPSConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetRoomRPSConfig not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetRoomRPSConfig not implemented")
|
||||
}
|
||||
func (UnimplementedGameAppServiceServer) ListRoomRPSChallenges(context.Context, *ListRoomRPSChallengesRequest) (*ListRoomRPSChallengesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRoomRPSChallenges not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRoomRPSChallenges not implemented")
|
||||
}
|
||||
func (UnimplementedGameAppServiceServer) CreateRoomRPSChallenge(context.Context, *CreateRoomRPSChallengeRequest) (*RoomRPSChallengeResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateRoomRPSChallenge not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateRoomRPSChallenge not implemented")
|
||||
}
|
||||
func (UnimplementedGameAppServiceServer) AcceptRoomRPSChallenge(context.Context, *AcceptRoomRPSChallengeRequest) (*RoomRPSChallengeResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AcceptRoomRPSChallenge not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AcceptRoomRPSChallenge not implemented")
|
||||
}
|
||||
func (UnimplementedGameAppServiceServer) GetRoomRPSChallenge(context.Context, *GetRoomRPSChallengeRequest) (*RoomRPSChallengeResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetRoomRPSChallenge not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetRoomRPSChallenge not implemented")
|
||||
}
|
||||
func (UnimplementedGameAppServiceServer) mustEmbedUnimplementedGameAppServiceServer() {}
|
||||
func (UnimplementedGameAppServiceServer) testEmbeddedByValue() {}
|
||||
@ -332,7 +332,7 @@ type UnsafeGameAppServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterGameAppServiceServer(s grpc.ServiceRegistrar, srv GameAppServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedGameAppServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedGameAppServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -773,7 +773,7 @@ type GameCallbackServiceServer interface {
|
||||
type UnimplementedGameCallbackServiceServer struct{}
|
||||
|
||||
func (UnimplementedGameCallbackServiceServer) HandleCallback(context.Context, *CallbackRequest) (*CallbackResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method HandleCallback not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method HandleCallback not implemented")
|
||||
}
|
||||
func (UnimplementedGameCallbackServiceServer) mustEmbedUnimplementedGameCallbackServiceServer() {}
|
||||
func (UnimplementedGameCallbackServiceServer) testEmbeddedByValue() {}
|
||||
@ -786,7 +786,7 @@ type UnsafeGameCallbackServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterGameCallbackServiceServer(s grpc.ServiceRegistrar, srv GameCallbackServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedGameCallbackServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedGameCallbackServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -879,7 +879,7 @@ type GameCronServiceServer interface {
|
||||
type UnimplementedGameCronServiceServer struct{}
|
||||
|
||||
func (UnimplementedGameCronServiceServer) ProcessLevelEventOutboxBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ProcessLevelEventOutboxBatch not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ProcessLevelEventOutboxBatch not implemented")
|
||||
}
|
||||
func (UnimplementedGameCronServiceServer) mustEmbedUnimplementedGameCronServiceServer() {}
|
||||
func (UnimplementedGameCronServiceServer) testEmbeddedByValue() {}
|
||||
@ -892,7 +892,7 @@ type UnsafeGameCronServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterGameCronServiceServer(s grpc.ServiceRegistrar, srv GameCronServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedGameCronServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedGameCronServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -1319,85 +1319,85 @@ type GameAdminServiceServer interface {
|
||||
type UnimplementedGameAdminServiceServer struct{}
|
||||
|
||||
func (UnimplementedGameAdminServiceServer) ListPlatforms(context.Context, *ListPlatformsRequest) (*ListPlatformsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListPlatforms not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListPlatforms not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) UpsertPlatform(context.Context, *UpsertPlatformRequest) (*PlatformResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpsertPlatform not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpsertPlatform not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) ListCatalog(context.Context, *ListCatalogRequest) (*ListCatalogResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListCatalog not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListCatalog not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) UpsertCatalog(context.Context, *UpsertCatalogRequest) (*CatalogResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpsertCatalog not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpsertCatalog not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) SetGameStatus(context.Context, *SetGameStatusRequest) (*CatalogResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetGameStatus not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetGameStatus not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) DeleteCatalog(context.Context, *DeleteCatalogRequest) (*DeleteCatalogResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteCatalog not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteCatalog not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) SetGameWhitelistEnabled(context.Context, *SetGameWhitelistEnabledRequest) (*CatalogResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetGameWhitelistEnabled not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetGameWhitelistEnabled not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) ListGameWhitelistUsers(context.Context, *ListGameWhitelistUsersRequest) (*ListGameWhitelistUsersResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListGameWhitelistUsers not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListGameWhitelistUsers not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) AddGameWhitelistUser(context.Context, *AddGameWhitelistUserRequest) (*GameWhitelistUserResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AddGameWhitelistUser not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AddGameWhitelistUser not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) DeleteGameWhitelistUser(context.Context, *DeleteGameWhitelistUserRequest) (*DeleteGameWhitelistUserResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteGameWhitelistUser not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteGameWhitelistUser not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) ListSelfGames(context.Context, *ListSelfGamesRequest) (*ListSelfGamesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListSelfGames not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListSelfGames not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) UpdateDiceConfig(context.Context, *UpdateDiceConfigRequest) (*DiceConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateDiceConfig not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateDiceConfig not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) GetSelfGameNewUserPolicy(context.Context, *GetSelfGameNewUserPolicyRequest) (*SelfGameNewUserPolicyResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetSelfGameNewUserPolicy not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetSelfGameNewUserPolicy not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) UpdateSelfGameNewUserPolicy(context.Context, *UpdateSelfGameNewUserPolicyRequest) (*SelfGameNewUserPolicyResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateSelfGameNewUserPolicy not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateSelfGameNewUserPolicy not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) ListSelfGameStakePools(context.Context, *ListSelfGameStakePoolsRequest) (*ListSelfGameStakePoolsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListSelfGameStakePools not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListSelfGameStakePools not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) UpdateSelfGameStakePool(context.Context, *UpdateSelfGameStakePoolRequest) (*SelfGameStakePoolResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateSelfGameStakePool not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateSelfGameStakePool not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) AdjustSelfGameStakePool(context.Context, *AdjustSelfGameStakePoolRequest) (*AdjustSelfGameStakePoolResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdjustSelfGameStakePool not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdjustSelfGameStakePool not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) ListDiceRobots(context.Context, *ListDiceRobotsRequest) (*ListDiceRobotsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListDiceRobots not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListDiceRobots not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) RegisterDiceRobots(context.Context, *RegisterDiceRobotsRequest) (*RegisterDiceRobotsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RegisterDiceRobots not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RegisterDiceRobots not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) SetDiceRobotStatus(context.Context, *SetDiceRobotStatusRequest) (*DiceRobotResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetDiceRobotStatus not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetDiceRobotStatus not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) DeleteDiceRobot(context.Context, *DeleteDiceRobotRequest) (*DeleteDiceRobotResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteDiceRobot not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteDiceRobot not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) GetRoomRPSConfig(context.Context, *GetRoomRPSConfigRequest) (*RoomRPSConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetRoomRPSConfig not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetRoomRPSConfig not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) UpdateRoomRPSConfig(context.Context, *UpdateRoomRPSConfigRequest) (*RoomRPSConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateRoomRPSConfig not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateRoomRPSConfig not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) ListRoomRPSChallenges(context.Context, *ListRoomRPSChallengesRequest) (*ListRoomRPSChallengesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRoomRPSChallenges not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRoomRPSChallenges not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) GetRoomRPSChallenge(context.Context, *GetRoomRPSChallengeRequest) (*RoomRPSChallengeResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetRoomRPSChallenge not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetRoomRPSChallenge not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) RetryRoomRPSSettlement(context.Context, *RetryRoomRPSSettlementRequest) (*RoomRPSChallengeResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RetryRoomRPSSettlement not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RetryRoomRPSSettlement not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) ExpireRoomRPSChallenge(context.Context, *ExpireRoomRPSChallengeRequest) (*RoomRPSChallengeResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ExpireRoomRPSChallenge not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ExpireRoomRPSChallenge not implemented")
|
||||
}
|
||||
func (UnimplementedGameAdminServiceServer) mustEmbedUnimplementedGameAdminServiceServer() {}
|
||||
func (UnimplementedGameAdminServiceServer) testEmbeddedByValue() {}
|
||||
@ -1410,7 +1410,7 @@ type UnsafeGameAdminServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterGameAdminServiceServer(s grpc.ServiceRegistrar, srv GameAdminServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedGameAdminServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedGameAdminServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v7.35.0
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v5.29.2
|
||||
// source: proto/luckygift/v1/luckygift.proto
|
||||
|
||||
package luckygiftv1
|
||||
@ -102,16 +102,16 @@ type LuckyGiftServiceServer interface {
|
||||
type UnimplementedLuckyGiftServiceServer struct{}
|
||||
|
||||
func (UnimplementedLuckyGiftServiceServer) CheckLuckyGift(context.Context, *CheckLuckyGiftRequest) (*CheckLuckyGiftResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CheckLuckyGift not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CheckLuckyGift not implemented")
|
||||
}
|
||||
func (UnimplementedLuckyGiftServiceServer) ExecuteLuckyGiftDraw(context.Context, *ExecuteLuckyGiftDrawRequest) (*ExecuteLuckyGiftDrawResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ExecuteLuckyGiftDraw not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ExecuteLuckyGiftDraw not implemented")
|
||||
}
|
||||
func (UnimplementedLuckyGiftServiceServer) BatchExecuteLuckyGiftDraw(context.Context, *BatchExecuteLuckyGiftDrawRequest) (*BatchExecuteLuckyGiftDrawResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BatchExecuteLuckyGiftDraw not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method BatchExecuteLuckyGiftDraw not implemented")
|
||||
}
|
||||
func (UnimplementedLuckyGiftServiceServer) ExecuteExternalGiftDraw(context.Context, *ExternalGiftDrawRequest) (*ExternalGiftDrawResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ExecuteExternalGiftDraw not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ExecuteExternalGiftDraw not implemented")
|
||||
}
|
||||
func (UnimplementedLuckyGiftServiceServer) mustEmbedUnimplementedLuckyGiftServiceServer() {}
|
||||
func (UnimplementedLuckyGiftServiceServer) testEmbeddedByValue() {}
|
||||
@ -124,7 +124,7 @@ type UnsafeLuckyGiftServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterLuckyGiftServiceServer(s grpc.ServiceRegistrar, srv LuckyGiftServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedLuckyGiftServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedLuckyGiftServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -344,22 +344,22 @@ type AdminLuckyGiftServiceServer interface {
|
||||
type UnimplementedAdminLuckyGiftServiceServer struct{}
|
||||
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) GetLuckyGiftConfig(context.Context, *GetLuckyGiftConfigRequest) (*GetLuckyGiftConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetLuckyGiftConfig not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetLuckyGiftConfig not implemented")
|
||||
}
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) UpsertLuckyGiftConfig(context.Context, *UpsertLuckyGiftConfigRequest) (*UpsertLuckyGiftConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpsertLuckyGiftConfig not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpsertLuckyGiftConfig not implemented")
|
||||
}
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftConfigs(context.Context, *ListLuckyGiftConfigsRequest) (*ListLuckyGiftConfigsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListLuckyGiftConfigs not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListLuckyGiftConfigs not implemented")
|
||||
}
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftDraws(context.Context, *ListLuckyGiftDrawsRequest) (*ListLuckyGiftDrawsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListLuckyGiftDraws not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListLuckyGiftDraws not implemented")
|
||||
}
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) GetLuckyGiftDrawSummary(context.Context, *GetLuckyGiftDrawSummaryRequest) (*GetLuckyGiftDrawSummaryResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetLuckyGiftDrawSummary not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetLuckyGiftDrawSummary not implemented")
|
||||
}
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftPoolBalances(context.Context, *ListLuckyGiftPoolBalancesRequest) (*ListLuckyGiftPoolBalancesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListLuckyGiftPoolBalances not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListLuckyGiftPoolBalances not implemented")
|
||||
}
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) mustEmbedUnimplementedAdminLuckyGiftServiceServer() {}
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) testEmbeddedByValue() {}
|
||||
@ -372,7 +372,7 @@ type UnsafeAdminLuckyGiftServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterAdminLuckyGiftServiceServer(s grpc.ServiceRegistrar, srv AdminLuckyGiftServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedAdminLuckyGiftServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedAdminLuckyGiftServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.1
|
||||
// protoc v7.35.0
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v5.29.2
|
||||
// source: proto/robot/v1/robot.proto
|
||||
|
||||
package robotv1
|
||||
@ -11,6 +11,7 @@ import (
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -22,15 +23,14 @@ const (
|
||||
|
||||
// RequestMeta 是 robot-service 内部 RPC 的租户和追踪上下文;不要把 app_code 放到业务字段里绕过统一鉴权链路。
|
||||
type RequestMeta struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
|
||||
Caller string `protobuf:"bytes,2,opt,name=caller,proto3" json:"caller,omitempty"`
|
||||
ActorUserId int64 `protobuf:"varint,3,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
|
||||
SentAtMs int64 `protobuf:"varint,4,opt,name=sent_at_ms,json=sentAtMs,proto3" json:"sent_at_ms,omitempty"`
|
||||
AppCode string `protobuf:"bytes,5,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
|
||||
Caller string `protobuf:"bytes,2,opt,name=caller,proto3" json:"caller,omitempty"`
|
||||
ActorUserId int64 `protobuf:"varint,3,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
|
||||
SentAtMs int64 `protobuf:"varint,4,opt,name=sent_at_ms,json=sentAtMs,proto3" json:"sent_at_ms,omitempty"`
|
||||
AppCode string `protobuf:"bytes,5,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RequestMeta) Reset() {
|
||||
@ -99,21 +99,20 @@ func (x *RequestMeta) GetAppCode() string {
|
||||
}
|
||||
|
||||
type Robot struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
AppCode string `protobuf:"bytes,1,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"`
|
||||
RobotScope string `protobuf:"bytes,2,opt,name=robot_scope,json=robotScope,proto3" json:"robot_scope,omitempty"`
|
||||
GameId string `protobuf:"bytes,3,opt,name=game_id,json=gameId,proto3" json:"game_id,omitempty"`
|
||||
RoomScene string `protobuf:"bytes,4,opt,name=room_scene,json=roomScene,proto3" json:"room_scene,omitempty"`
|
||||
UserId int64 `protobuf:"varint,5,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||
Status string `protobuf:"bytes,6,opt,name=status,proto3" json:"status,omitempty"`
|
||||
CreatedByAdminId int64 `protobuf:"varint,7,opt,name=created_by_admin_id,json=createdByAdminId,proto3" json:"created_by_admin_id,omitempty"`
|
||||
CreatedAtMs int64 `protobuf:"varint,8,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"`
|
||||
UpdatedAtMs int64 `protobuf:"varint,9,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"`
|
||||
LastUsedAtMs int64 `protobuf:"varint,10,opt,name=last_used_at_ms,json=lastUsedAtMs,proto3" json:"last_used_at_ms,omitempty"`
|
||||
UsedCount int64 `protobuf:"varint,11,opt,name=used_count,json=usedCount,proto3" json:"used_count,omitempty"`
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
AppCode string `protobuf:"bytes,1,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"`
|
||||
RobotScope string `protobuf:"bytes,2,opt,name=robot_scope,json=robotScope,proto3" json:"robot_scope,omitempty"`
|
||||
GameId string `protobuf:"bytes,3,opt,name=game_id,json=gameId,proto3" json:"game_id,omitempty"`
|
||||
RoomScene string `protobuf:"bytes,4,opt,name=room_scene,json=roomScene,proto3" json:"room_scene,omitempty"`
|
||||
UserId int64 `protobuf:"varint,5,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||
Status string `protobuf:"bytes,6,opt,name=status,proto3" json:"status,omitempty"`
|
||||
CreatedByAdminId int64 `protobuf:"varint,7,opt,name=created_by_admin_id,json=createdByAdminId,proto3" json:"created_by_admin_id,omitempty"`
|
||||
CreatedAtMs int64 `protobuf:"varint,8,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"`
|
||||
UpdatedAtMs int64 `protobuf:"varint,9,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"`
|
||||
LastUsedAtMs int64 `protobuf:"varint,10,opt,name=last_used_at_ms,json=lastUsedAtMs,proto3" json:"last_used_at_ms,omitempty"`
|
||||
UsedCount int64 `protobuf:"varint,11,opt,name=used_count,json=usedCount,proto3" json:"used_count,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Robot) Reset() {
|
||||
@ -224,15 +223,14 @@ func (x *Robot) GetUsedCount() int64 {
|
||||
}
|
||||
|
||||
type ListGameRobotsRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
GameId string `protobuf:"bytes,2,opt,name=game_id,json=gameId,proto3" json:"game_id,omitempty"`
|
||||
Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"`
|
||||
PageSize int32 `protobuf:"varint,4,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"`
|
||||
Cursor string `protobuf:"bytes,5,opt,name=cursor,proto3" json:"cursor,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
GameId string `protobuf:"bytes,2,opt,name=game_id,json=gameId,proto3" json:"game_id,omitempty"`
|
||||
Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"`
|
||||
PageSize int32 `protobuf:"varint,4,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"`
|
||||
Cursor string `protobuf:"bytes,5,opt,name=cursor,proto3" json:"cursor,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListGameRobotsRequest) Reset() {
|
||||
@ -301,13 +299,12 @@ func (x *ListGameRobotsRequest) GetCursor() string {
|
||||
}
|
||||
|
||||
type ListGameRobotsResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Robots []*Robot `protobuf:"bytes,1,rep,name=robots,proto3" json:"robots,omitempty"`
|
||||
NextCursor string `protobuf:"bytes,2,opt,name=next_cursor,json=nextCursor,proto3" json:"next_cursor,omitempty"`
|
||||
ServerTimeMs int64 `protobuf:"varint,3,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Robots []*Robot `protobuf:"bytes,1,rep,name=robots,proto3" json:"robots,omitempty"`
|
||||
NextCursor string `protobuf:"bytes,2,opt,name=next_cursor,json=nextCursor,proto3" json:"next_cursor,omitempty"`
|
||||
ServerTimeMs int64 `protobuf:"varint,3,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListGameRobotsResponse) Reset() {
|
||||
@ -362,13 +359,12 @@ func (x *ListGameRobotsResponse) GetServerTimeMs() int64 {
|
||||
}
|
||||
|
||||
type RegisterGameRobotsRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
GameId string `protobuf:"bytes,2,opt,name=game_id,json=gameId,proto3" json:"game_id,omitempty"`
|
||||
UserIds []int64 `protobuf:"varint,3,rep,packed,name=user_ids,json=userIds,proto3" json:"user_ids,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
GameId string `protobuf:"bytes,2,opt,name=game_id,json=gameId,proto3" json:"game_id,omitempty"`
|
||||
UserIds []int64 `protobuf:"varint,3,rep,packed,name=user_ids,json=userIds,proto3" json:"user_ids,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RegisterGameRobotsRequest) Reset() {
|
||||
@ -423,12 +419,11 @@ func (x *RegisterGameRobotsRequest) GetUserIds() []int64 {
|
||||
}
|
||||
|
||||
type RegisterGameRobotsResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Robots []*Robot `protobuf:"bytes,1,rep,name=robots,proto3" json:"robots,omitempty"`
|
||||
ServerTimeMs int64 `protobuf:"varint,2,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Robots []*Robot `protobuf:"bytes,1,rep,name=robots,proto3" json:"robots,omitempty"`
|
||||
ServerTimeMs int64 `protobuf:"varint,2,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RegisterGameRobotsResponse) Reset() {
|
||||
@ -476,15 +471,14 @@ func (x *RegisterGameRobotsResponse) GetServerTimeMs() int64 {
|
||||
}
|
||||
|
||||
type PickGameRobotRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
GameId string `protobuf:"bytes,2,opt,name=game_id,json=gameId,proto3" json:"game_id,omitempty"`
|
||||
MatchId string `protobuf:"bytes,3,opt,name=match_id,json=matchId,proto3" json:"match_id,omitempty"`
|
||||
SelectedAtMs int64 `protobuf:"varint,4,opt,name=selected_at_ms,json=selectedAtMs,proto3" json:"selected_at_ms,omitempty"`
|
||||
ExcludeUserIds []int64 `protobuf:"varint,5,rep,packed,name=exclude_user_ids,json=excludeUserIds,proto3" json:"exclude_user_ids,omitempty"`
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
GameId string `protobuf:"bytes,2,opt,name=game_id,json=gameId,proto3" json:"game_id,omitempty"`
|
||||
MatchId string `protobuf:"bytes,3,opt,name=match_id,json=matchId,proto3" json:"match_id,omitempty"`
|
||||
SelectedAtMs int64 `protobuf:"varint,4,opt,name=selected_at_ms,json=selectedAtMs,proto3" json:"selected_at_ms,omitempty"`
|
||||
ExcludeUserIds []int64 `protobuf:"varint,5,rep,packed,name=exclude_user_ids,json=excludeUserIds,proto3" json:"exclude_user_ids,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *PickGameRobotRequest) Reset() {
|
||||
@ -553,12 +547,11 @@ func (x *PickGameRobotRequest) GetExcludeUserIds() []int64 {
|
||||
}
|
||||
|
||||
type GameRobotResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Robot *Robot `protobuf:"bytes,1,opt,name=robot,proto3" json:"robot,omitempty"`
|
||||
ServerTimeMs int64 `protobuf:"varint,2,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Robot *Robot `protobuf:"bytes,1,opt,name=robot,proto3" json:"robot,omitempty"`
|
||||
ServerTimeMs int64 `protobuf:"varint,2,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GameRobotResponse) Reset() {
|
||||
@ -606,14 +599,13 @@ func (x *GameRobotResponse) GetServerTimeMs() int64 {
|
||||
}
|
||||
|
||||
type SetGameRobotStatusRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
GameId string `protobuf:"bytes,2,opt,name=game_id,json=gameId,proto3" json:"game_id,omitempty"`
|
||||
UserId int64 `protobuf:"varint,3,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||
Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
GameId string `protobuf:"bytes,2,opt,name=game_id,json=gameId,proto3" json:"game_id,omitempty"`
|
||||
UserId int64 `protobuf:"varint,3,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||
Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SetGameRobotStatusRequest) Reset() {
|
||||
@ -675,13 +667,12 @@ func (x *SetGameRobotStatusRequest) GetStatus() string {
|
||||
}
|
||||
|
||||
type DeleteGameRobotRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
GameId string `protobuf:"bytes,2,opt,name=game_id,json=gameId,proto3" json:"game_id,omitempty"`
|
||||
UserId int64 `protobuf:"varint,3,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
GameId string `protobuf:"bytes,2,opt,name=game_id,json=gameId,proto3" json:"game_id,omitempty"`
|
||||
UserId int64 `protobuf:"varint,3,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeleteGameRobotRequest) Reset() {
|
||||
@ -736,12 +727,11 @@ func (x *DeleteGameRobotRequest) GetUserId() int64 {
|
||||
}
|
||||
|
||||
type DeleteGameRobotResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Deleted bool `protobuf:"varint,1,opt,name=deleted,proto3" json:"deleted,omitempty"`
|
||||
ServerTimeMs int64 `protobuf:"varint,2,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Deleted bool `protobuf:"varint,1,opt,name=deleted,proto3" json:"deleted,omitempty"`
|
||||
ServerTimeMs int64 `protobuf:"varint,2,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeleteGameRobotResponse) Reset() {
|
||||
@ -789,15 +779,14 @@ func (x *DeleteGameRobotResponse) GetServerTimeMs() int64 {
|
||||
}
|
||||
|
||||
type ListRoomRobotsRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
RoomScene string `protobuf:"bytes,2,opt,name=room_scene,json=roomScene,proto3" json:"room_scene,omitempty"`
|
||||
Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"`
|
||||
PageSize int32 `protobuf:"varint,4,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"`
|
||||
Cursor string `protobuf:"bytes,5,opt,name=cursor,proto3" json:"cursor,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
RoomScene string `protobuf:"bytes,2,opt,name=room_scene,json=roomScene,proto3" json:"room_scene,omitempty"`
|
||||
Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"`
|
||||
PageSize int32 `protobuf:"varint,4,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"`
|
||||
Cursor string `protobuf:"bytes,5,opt,name=cursor,proto3" json:"cursor,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListRoomRobotsRequest) Reset() {
|
||||
@ -866,13 +855,12 @@ func (x *ListRoomRobotsRequest) GetCursor() string {
|
||||
}
|
||||
|
||||
type ListRoomRobotsResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Robots []*Robot `protobuf:"bytes,1,rep,name=robots,proto3" json:"robots,omitempty"`
|
||||
NextCursor string `protobuf:"bytes,2,opt,name=next_cursor,json=nextCursor,proto3" json:"next_cursor,omitempty"`
|
||||
ServerTimeMs int64 `protobuf:"varint,3,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Robots []*Robot `protobuf:"bytes,1,rep,name=robots,proto3" json:"robots,omitempty"`
|
||||
NextCursor string `protobuf:"bytes,2,opt,name=next_cursor,json=nextCursor,proto3" json:"next_cursor,omitempty"`
|
||||
ServerTimeMs int64 `protobuf:"varint,3,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListRoomRobotsResponse) Reset() {
|
||||
@ -927,13 +915,12 @@ func (x *ListRoomRobotsResponse) GetServerTimeMs() int64 {
|
||||
}
|
||||
|
||||
type RegisterRoomRobotsRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
RoomScene string `protobuf:"bytes,2,opt,name=room_scene,json=roomScene,proto3" json:"room_scene,omitempty"`
|
||||
UserIds []int64 `protobuf:"varint,3,rep,packed,name=user_ids,json=userIds,proto3" json:"user_ids,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
RoomScene string `protobuf:"bytes,2,opt,name=room_scene,json=roomScene,proto3" json:"room_scene,omitempty"`
|
||||
UserIds []int64 `protobuf:"varint,3,rep,packed,name=user_ids,json=userIds,proto3" json:"user_ids,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RegisterRoomRobotsRequest) Reset() {
|
||||
@ -988,12 +975,11 @@ func (x *RegisterRoomRobotsRequest) GetUserIds() []int64 {
|
||||
}
|
||||
|
||||
type RegisterRoomRobotsResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Robots []*Robot `protobuf:"bytes,1,rep,name=robots,proto3" json:"robots,omitempty"`
|
||||
ServerTimeMs int64 `protobuf:"varint,2,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Robots []*Robot `protobuf:"bytes,1,rep,name=robots,proto3" json:"robots,omitempty"`
|
||||
ServerTimeMs int64 `protobuf:"varint,2,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RegisterRoomRobotsResponse) Reset() {
|
||||
@ -1041,14 +1027,13 @@ func (x *RegisterRoomRobotsResponse) GetServerTimeMs() int64 {
|
||||
}
|
||||
|
||||
type SetRoomRobotStatusRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
RoomScene string `protobuf:"bytes,2,opt,name=room_scene,json=roomScene,proto3" json:"room_scene,omitempty"`
|
||||
UserId int64 `protobuf:"varint,3,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||
Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
RoomScene string `protobuf:"bytes,2,opt,name=room_scene,json=roomScene,proto3" json:"room_scene,omitempty"`
|
||||
UserId int64 `protobuf:"varint,3,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||
Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SetRoomRobotStatusRequest) Reset() {
|
||||
@ -1110,13 +1095,12 @@ func (x *SetRoomRobotStatusRequest) GetStatus() string {
|
||||
}
|
||||
|
||||
type DeleteRoomRobotRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
RoomScene string `protobuf:"bytes,2,opt,name=room_scene,json=roomScene,proto3" json:"room_scene,omitempty"`
|
||||
UserId int64 `protobuf:"varint,3,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
RoomScene string `protobuf:"bytes,2,opt,name=room_scene,json=roomScene,proto3" json:"room_scene,omitempty"`
|
||||
UserId int64 `protobuf:"varint,3,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeleteRoomRobotRequest) Reset() {
|
||||
@ -1171,12 +1155,11 @@ func (x *DeleteRoomRobotRequest) GetUserId() int64 {
|
||||
}
|
||||
|
||||
type DeleteRoomRobotResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Deleted bool `protobuf:"varint,1,opt,name=deleted,proto3" json:"deleted,omitempty"`
|
||||
ServerTimeMs int64 `protobuf:"varint,2,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Deleted bool `protobuf:"varint,1,opt,name=deleted,proto3" json:"deleted,omitempty"`
|
||||
ServerTimeMs int64 `protobuf:"varint,2,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeleteRoomRobotResponse) Reset() {
|
||||
@ -1224,14 +1207,13 @@ func (x *DeleteRoomRobotResponse) GetServerTimeMs() int64 {
|
||||
}
|
||||
|
||||
type PickRoomRobotRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
RoomScene string `protobuf:"bytes,2,opt,name=room_scene,json=roomScene,proto3" json:"room_scene,omitempty"`
|
||||
RoomId string `protobuf:"bytes,3,opt,name=room_id,json=roomId,proto3" json:"room_id,omitempty"`
|
||||
SelectedAtMs int64 `protobuf:"varint,4,opt,name=selected_at_ms,json=selectedAtMs,proto3" json:"selected_at_ms,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
RoomScene string `protobuf:"bytes,2,opt,name=room_scene,json=roomScene,proto3" json:"room_scene,omitempty"`
|
||||
RoomId string `protobuf:"bytes,3,opt,name=room_id,json=roomId,proto3" json:"room_id,omitempty"`
|
||||
SelectedAtMs int64 `protobuf:"varint,4,opt,name=selected_at_ms,json=selectedAtMs,proto3" json:"selected_at_ms,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *PickRoomRobotRequest) Reset() {
|
||||
@ -1293,12 +1275,11 @@ func (x *PickRoomRobotRequest) GetSelectedAtMs() int64 {
|
||||
}
|
||||
|
||||
type RoomRobotResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Robot *Robot `protobuf:"bytes,1,opt,name=robot,proto3" json:"robot,omitempty"`
|
||||
ServerTimeMs int64 `protobuf:"varint,2,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Robot *Robot `protobuf:"bytes,1,opt,name=robot,proto3" json:"robot,omitempty"`
|
||||
ServerTimeMs int64 `protobuf:"varint,2,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RoomRobotResponse) Reset() {
|
||||
@ -1347,276 +1328,136 @@ func (x *RoomRobotResponse) GetServerTimeMs() int64 {
|
||||
|
||||
var File_proto_robot_v1_robot_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_proto_robot_v1_robot_proto_rawDesc = []byte{
|
||||
0x0a, 0x1a, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2f, 0x76, 0x31,
|
||||
0x2f, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0e, 0x68, 0x79,
|
||||
0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31, 0x22, 0xa1, 0x01, 0x0a,
|
||||
0x0b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1d, 0x0a, 0x0a,
|
||||
0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x63,
|
||||
0x61, 0x6c, 0x6c, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x61, 0x6c,
|
||||
0x6c, 0x65, 0x72, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65,
|
||||
0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f,
|
||||
0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x0a, 0x73, 0x65, 0x6e, 0x74, 0x5f,
|
||||
0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x73, 0x65, 0x6e,
|
||||
0x74, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64,
|
||||
0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65,
|
||||
0x22, 0xe9, 0x02, 0x0a, 0x05, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70,
|
||||
0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70,
|
||||
0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x5f, 0x73,
|
||||
0x63, 0x6f, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x6f, 0x62, 0x6f,
|
||||
0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x67, 0x61, 0x6d, 0x65, 0x5f, 0x69,
|
||||
0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x61, 0x6d, 0x65, 0x49, 0x64, 0x12,
|
||||
0x1d, 0x0a, 0x0a, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x73, 0x63, 0x65, 0x6e, 0x65, 0x18, 0x04, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x6f, 0x6f, 0x6d, 0x53, 0x63, 0x65, 0x6e, 0x65, 0x12, 0x17,
|
||||
0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52,
|
||||
0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75,
|
||||
0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12,
|
||||
0x2d, 0x0a, 0x13, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x5f, 0x61, 0x64,
|
||||
0x6d, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x10, 0x63, 0x72,
|
||||
0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x22,
|
||||
0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18,
|
||||
0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74,
|
||||
0x4d, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74,
|
||||
0x5f, 0x6d, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74,
|
||||
0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x25, 0x0a, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x75,
|
||||
0x73, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52,
|
||||
0x0c, 0x6c, 0x61, 0x73, 0x74, 0x55, 0x73, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x1d, 0x0a,
|
||||
0x0a, 0x75, 0x73, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28,
|
||||
0x03, 0x52, 0x09, 0x75, 0x73, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xae, 0x01, 0x0a,
|
||||
0x15, 0x4c, 0x69, 0x73, 0x74, 0x47, 0x61, 0x6d, 0x65, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x73, 0x52,
|
||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x62,
|
||||
0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74,
|
||||
0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x67, 0x61, 0x6d, 0x65, 0x5f,
|
||||
0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x61, 0x6d, 0x65, 0x49, 0x64,
|
||||
0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65,
|
||||
0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x61, 0x67,
|
||||
0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x18,
|
||||
0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x22, 0x8e, 0x01,
|
||||
0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x47, 0x61, 0x6d, 0x65, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x73,
|
||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x06, 0x72, 0x6f, 0x62, 0x6f,
|
||||
0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70,
|
||||
0x2e, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x52,
|
||||
0x06, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x6e, 0x65, 0x78, 0x74, 0x5f,
|
||||
0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x65,
|
||||
0x78, 0x74, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x72, 0x76,
|
||||
0x65, 0x72, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03,
|
||||
0x52, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x69, 0x6d, 0x65, 0x4d, 0x73, 0x22, 0x80,
|
||||
0x01, 0x0a, 0x19, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x47, 0x61, 0x6d, 0x65, 0x52,
|
||||
0x6f, 0x62, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x04,
|
||||
0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x68, 0x79, 0x61,
|
||||
0x70, 0x70, 0x2e, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75,
|
||||
0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a,
|
||||
0x07, 0x67, 0x61, 0x6d, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
|
||||
0x67, 0x61, 0x6d, 0x65, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69,
|
||||
0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x03, 0x52, 0x07, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64,
|
||||
0x73, 0x22, 0x71, 0x0a, 0x1a, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x47, 0x61, 0x6d,
|
||||
0x65, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
||||
0x2d, 0x0a, 0x06, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32,
|
||||
0x15, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31,
|
||||
0x2e, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x52, 0x06, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x73, 0x12, 0x24,
|
||||
0x0a, 0x0e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6d, 0x73,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x69,
|
||||
0x6d, 0x65, 0x4d, 0x73, 0x22, 0xcb, 0x01, 0x0a, 0x14, 0x50, 0x69, 0x63, 0x6b, 0x47, 0x61, 0x6d,
|
||||
0x65, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a,
|
||||
0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x68, 0x79,
|
||||
0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17,
|
||||
0x0a, 0x07, 0x67, 0x61, 0x6d, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x06, 0x67, 0x61, 0x6d, 0x65, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x6d, 0x61, 0x74, 0x63, 0x68,
|
||||
0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x61, 0x74, 0x63, 0x68,
|
||||
0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x61,
|
||||
0x74, 0x5f, 0x6d, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x6c, 0x65,
|
||||
0x63, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x65, 0x78, 0x63, 0x6c,
|
||||
0x75, 0x64, 0x65, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x05, 0x20, 0x03,
|
||||
0x28, 0x03, 0x52, 0x0e, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x55, 0x73, 0x65, 0x72, 0x49,
|
||||
0x64, 0x73, 0x22, 0x66, 0x0a, 0x11, 0x47, 0x61, 0x6d, 0x65, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x72, 0x6f, 0x62, 0x6f, 0x74,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x72,
|
||||
0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x52, 0x05, 0x72,
|
||||
0x6f, 0x62, 0x6f, 0x74, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x74,
|
||||
0x69, 0x6d, 0x65, 0x5f, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65,
|
||||
0x72, 0x76, 0x65, 0x72, 0x54, 0x69, 0x6d, 0x65, 0x4d, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x19, 0x53,
|
||||
0x65, 0x74, 0x47, 0x61, 0x6d, 0x65, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75,
|
||||
0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x72,
|
||||
0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d,
|
||||
0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x67, 0x61, 0x6d,
|
||||
0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x61, 0x6d, 0x65,
|
||||
0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20,
|
||||
0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73,
|
||||
0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61,
|
||||
0x74, 0x75, 0x73, 0x22, 0x7b, 0x0a, 0x16, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x47, 0x61, 0x6d,
|
||||
0x65, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a,
|
||||
0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x68, 0x79,
|
||||
0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17,
|
||||
0x0a, 0x07, 0x67, 0x61, 0x6d, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x06, 0x67, 0x61, 0x6d, 0x65, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f,
|
||||
0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64,
|
||||
0x22, 0x59, 0x0a, 0x17, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x47, 0x61, 0x6d, 0x65, 0x52, 0x6f,
|
||||
0x62, 0x6f, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x64,
|
||||
0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x64, 0x65,
|
||||
0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f,
|
||||
0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73,
|
||||
0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x69, 0x6d, 0x65, 0x4d, 0x73, 0x22, 0xb4, 0x01, 0x0a, 0x15,
|
||||
0x4c, 0x69, 0x73, 0x74, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x73, 0x52, 0x65,
|
||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20,
|
||||
0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x62, 0x6f,
|
||||
0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61,
|
||||
0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x73,
|
||||
0x63, 0x65, 0x6e, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x6f, 0x6f, 0x6d,
|
||||
0x53, 0x63, 0x65, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18,
|
||||
0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1b, 0x0a,
|
||||
0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05,
|
||||
0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x75,
|
||||
0x72, 0x73, 0x6f, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x75, 0x72, 0x73,
|
||||
0x6f, 0x72, 0x22, 0x8e, 0x01, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x6f, 0x6f, 0x6d, 0x52,
|
||||
0x6f, 0x62, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a,
|
||||
0x06, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e,
|
||||
0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52,
|
||||
0x6f, 0x62, 0x6f, 0x74, 0x52, 0x06, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x73, 0x12, 0x1f, 0x0a, 0x0b,
|
||||
0x6e, 0x65, 0x78, 0x74, 0x5f, 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x0a, 0x6e, 0x65, 0x78, 0x74, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x12, 0x24, 0x0a,
|
||||
0x0e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6d, 0x73, 0x18,
|
||||
0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x69, 0x6d,
|
||||
0x65, 0x4d, 0x73, 0x22, 0x86, 0x01, 0x0a, 0x19, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72,
|
||||
0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
||||
0x74, 0x12, 0x2f, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||
0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31,
|
||||
0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65,
|
||||
0x74, 0x61, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x73, 0x63, 0x65, 0x6e, 0x65,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x6f, 0x6f, 0x6d, 0x53, 0x63, 0x65, 0x6e,
|
||||
0x65, 0x12, 0x19, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x03, 0x20,
|
||||
0x03, 0x28, 0x03, 0x52, 0x07, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x73, 0x22, 0x71, 0x0a, 0x1a,
|
||||
0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x62, 0x6f,
|
||||
0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x06, 0x72, 0x6f,
|
||||
0x62, 0x6f, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x79, 0x61,
|
||||
0x70, 0x70, 0x2e, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x62, 0x6f,
|
||||
0x74, 0x52, 0x06, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x73, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x72,
|
||||
0x76, 0x65, 0x72, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||
0x03, 0x52, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x69, 0x6d, 0x65, 0x4d, 0x73, 0x22,
|
||||
0x9c, 0x01, 0x0a, 0x19, 0x53, 0x65, 0x74, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x62, 0x6f, 0x74,
|
||||
0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a,
|
||||
0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x68, 0x79,
|
||||
0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x1d,
|
||||
0x0a, 0x0a, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x73, 0x63, 0x65, 0x6e, 0x65, 0x18, 0x02, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x09, 0x72, 0x6f, 0x6f, 0x6d, 0x53, 0x63, 0x65, 0x6e, 0x65, 0x12, 0x17, 0x0a,
|
||||
0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06,
|
||||
0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
|
||||
0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x81,
|
||||
0x01, 0x0a, 0x16, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x62,
|
||||
0x6f, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x04, 0x6d, 0x65, 0x74,
|
||||
0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e,
|
||||
0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
||||
0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x6f,
|
||||
0x6f, 0x6d, 0x5f, 0x73, 0x63, 0x65, 0x6e, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
|
||||
0x72, 0x6f, 0x6f, 0x6d, 0x53, 0x63, 0x65, 0x6e, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65,
|
||||
0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72,
|
||||
0x49, 0x64, 0x22, 0x59, 0x0a, 0x17, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x6f, 0x6f, 0x6d,
|
||||
0x52, 0x6f, 0x62, 0x6f, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a,
|
||||
0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07,
|
||||
0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x72, 0x76, 0x65,
|
||||
0x72, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52,
|
||||
0x0c, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x69, 0x6d, 0x65, 0x4d, 0x73, 0x22, 0xa5, 0x01,
|
||||
0x0a, 0x14, 0x50, 0x69, 0x63, 0x6b, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x52,
|
||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x62,
|
||||
0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74,
|
||||
0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x6f, 0x6f, 0x6d, 0x5f,
|
||||
0x73, 0x63, 0x65, 0x6e, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x6f, 0x6f,
|
||||
0x6d, 0x53, 0x63, 0x65, 0x6e, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x69,
|
||||
0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x6f, 0x6d, 0x49, 0x64, 0x12,
|
||||
0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d,
|
||||
0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65,
|
||||
0x64, 0x41, 0x74, 0x4d, 0x73, 0x22, 0x66, 0x0a, 0x11, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x62,
|
||||
0x6f, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x72, 0x6f,
|
||||
0x62, 0x6f, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x79, 0x61, 0x70,
|
||||
0x70, 0x2e, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x62, 0x6f, 0x74,
|
||||
0x52, 0x05, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x72, 0x76, 0x65,
|
||||
0x72, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52,
|
||||
0x0c, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x69, 0x6d, 0x65, 0x4d, 0x73, 0x32, 0x82, 0x04,
|
||||
0x0a, 0x10, 0x47, 0x61, 0x6d, 0x65, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69,
|
||||
0x63, 0x65, 0x12, 0x5f, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x47, 0x61, 0x6d, 0x65, 0x52, 0x6f,
|
||||
0x62, 0x6f, 0x74, 0x73, 0x12, 0x25, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x62,
|
||||
0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x47, 0x61, 0x6d, 0x65, 0x52, 0x6f,
|
||||
0x62, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x68, 0x79,
|
||||
0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73,
|
||||
0x74, 0x47, 0x61, 0x6d, 0x65, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||
0x6e, 0x73, 0x65, 0x12, 0x6b, 0x0a, 0x12, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x47,
|
||||
0x61, 0x6d, 0x65, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x73, 0x12, 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70,
|
||||
0x70, 0x2e, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73,
|
||||
0x74, 0x65, 0x72, 0x47, 0x61, 0x6d, 0x65, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x62,
|
||||
0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x47, 0x61,
|
||||
0x6d, 0x65, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
||||
0x12, 0x58, 0x0a, 0x0d, 0x50, 0x69, 0x63, 0x6b, 0x47, 0x61, 0x6d, 0x65, 0x52, 0x6f, 0x62, 0x6f,
|
||||
0x74, 0x12, 0x24, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e,
|
||||
0x76, 0x31, 0x2e, 0x50, 0x69, 0x63, 0x6b, 0x47, 0x61, 0x6d, 0x65, 0x52, 0x6f, 0x62, 0x6f, 0x74,
|
||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e,
|
||||
0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x61, 0x6d, 0x65, 0x52, 0x6f, 0x62,
|
||||
0x6f, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x12, 0x53, 0x65,
|
||||
0x74, 0x47, 0x61, 0x6d, 0x65, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
|
||||
0x12, 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76,
|
||||
0x31, 0x2e, 0x53, 0x65, 0x74, 0x47, 0x61, 0x6d, 0x65, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x53, 0x74,
|
||||
0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x68, 0x79,
|
||||
0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x61, 0x6d,
|
||||
0x65, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62,
|
||||
0x0a, 0x0f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x47, 0x61, 0x6d, 0x65, 0x52, 0x6f, 0x62, 0x6f,
|
||||
0x74, 0x12, 0x26, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e,
|
||||
0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x47, 0x61, 0x6d, 0x65, 0x52, 0x6f, 0x62,
|
||||
0x6f, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x68, 0x79, 0x61, 0x70,
|
||||
0x70, 0x2e, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74,
|
||||
0x65, 0x47, 0x61, 0x6d, 0x65, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
||||
0x73, 0x65, 0x32, 0x82, 0x04, 0x0a, 0x10, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x62, 0x6f, 0x74,
|
||||
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x5f, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x52,
|
||||
0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x73, 0x12, 0x25, 0x2e, 0x68, 0x79, 0x61, 0x70,
|
||||
0x70, 0x2e, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52,
|
||||
0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
||||
0x1a, 0x26, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76,
|
||||
0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x73,
|
||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6b, 0x0a, 0x12, 0x52, 0x65, 0x67, 0x69,
|
||||
0x73, 0x74, 0x65, 0x72, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x73, 0x12, 0x29,
|
||||
0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e,
|
||||
0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x62, 0x6f,
|
||||
0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x68, 0x79, 0x61, 0x70,
|
||||
0x70, 0x2e, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73,
|
||||
0x74, 0x65, 0x72, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x73,
|
||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x0d, 0x50, 0x69, 0x63, 0x6b, 0x52, 0x6f, 0x6f,
|
||||
0x6d, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x12, 0x24, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x72,
|
||||
0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x69, 0x63, 0x6b, 0x52, 0x6f, 0x6f, 0x6d,
|
||||
0x52, 0x6f, 0x62, 0x6f, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x68,
|
||||
0x79, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f,
|
||||
0x6f, 0x6d, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
||||
0x62, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x53,
|
||||
0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f,
|
||||
0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f,
|
||||
0x62, 0x6f, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
||||
0x1a, 0x21, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76,
|
||||
0x31, 0x2e, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||
0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x0f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x6f, 0x6f,
|
||||
0x6d, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x12, 0x26, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x72,
|
||||
0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x6f,
|
||||
0x6f, 0x6d, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27,
|
||||
0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2e, 0x76, 0x31, 0x2e,
|
||||
0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x28, 0x5a, 0x26, 0x68, 0x79, 0x61, 0x70, 0x70,
|
||||
0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x2f, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x2f, 0x76, 0x31, 0x3b, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x76,
|
||||
0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
const file_proto_robot_v1_robot_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x1aproto/robot/v1/robot.proto\x12\x0ehyapp.robot.v1\"\xa1\x01\n" +
|
||||
"\vRequestMeta\x12\x1d\n" +
|
||||
"\n" +
|
||||
"request_id\x18\x01 \x01(\tR\trequestId\x12\x16\n" +
|
||||
"\x06caller\x18\x02 \x01(\tR\x06caller\x12\"\n" +
|
||||
"\ractor_user_id\x18\x03 \x01(\x03R\vactorUserId\x12\x1c\n" +
|
||||
"\n" +
|
||||
"sent_at_ms\x18\x04 \x01(\x03R\bsentAtMs\x12\x19\n" +
|
||||
"\bapp_code\x18\x05 \x01(\tR\aappCode\"\xe9\x02\n" +
|
||||
"\x05Robot\x12\x19\n" +
|
||||
"\bapp_code\x18\x01 \x01(\tR\aappCode\x12\x1f\n" +
|
||||
"\vrobot_scope\x18\x02 \x01(\tR\n" +
|
||||
"robotScope\x12\x17\n" +
|
||||
"\agame_id\x18\x03 \x01(\tR\x06gameId\x12\x1d\n" +
|
||||
"\n" +
|
||||
"room_scene\x18\x04 \x01(\tR\troomScene\x12\x17\n" +
|
||||
"\auser_id\x18\x05 \x01(\x03R\x06userId\x12\x16\n" +
|
||||
"\x06status\x18\x06 \x01(\tR\x06status\x12-\n" +
|
||||
"\x13created_by_admin_id\x18\a \x01(\x03R\x10createdByAdminId\x12\"\n" +
|
||||
"\rcreated_at_ms\x18\b \x01(\x03R\vcreatedAtMs\x12\"\n" +
|
||||
"\rupdated_at_ms\x18\t \x01(\x03R\vupdatedAtMs\x12%\n" +
|
||||
"\x0flast_used_at_ms\x18\n" +
|
||||
" \x01(\x03R\flastUsedAtMs\x12\x1d\n" +
|
||||
"\n" +
|
||||
"used_count\x18\v \x01(\x03R\tusedCount\"\xae\x01\n" +
|
||||
"\x15ListGameRobotsRequest\x12/\n" +
|
||||
"\x04meta\x18\x01 \x01(\v2\x1b.hyapp.robot.v1.RequestMetaR\x04meta\x12\x17\n" +
|
||||
"\agame_id\x18\x02 \x01(\tR\x06gameId\x12\x16\n" +
|
||||
"\x06status\x18\x03 \x01(\tR\x06status\x12\x1b\n" +
|
||||
"\tpage_size\x18\x04 \x01(\x05R\bpageSize\x12\x16\n" +
|
||||
"\x06cursor\x18\x05 \x01(\tR\x06cursor\"\x8e\x01\n" +
|
||||
"\x16ListGameRobotsResponse\x12-\n" +
|
||||
"\x06robots\x18\x01 \x03(\v2\x15.hyapp.robot.v1.RobotR\x06robots\x12\x1f\n" +
|
||||
"\vnext_cursor\x18\x02 \x01(\tR\n" +
|
||||
"nextCursor\x12$\n" +
|
||||
"\x0eserver_time_ms\x18\x03 \x01(\x03R\fserverTimeMs\"\x80\x01\n" +
|
||||
"\x19RegisterGameRobotsRequest\x12/\n" +
|
||||
"\x04meta\x18\x01 \x01(\v2\x1b.hyapp.robot.v1.RequestMetaR\x04meta\x12\x17\n" +
|
||||
"\agame_id\x18\x02 \x01(\tR\x06gameId\x12\x19\n" +
|
||||
"\buser_ids\x18\x03 \x03(\x03R\auserIds\"q\n" +
|
||||
"\x1aRegisterGameRobotsResponse\x12-\n" +
|
||||
"\x06robots\x18\x01 \x03(\v2\x15.hyapp.robot.v1.RobotR\x06robots\x12$\n" +
|
||||
"\x0eserver_time_ms\x18\x02 \x01(\x03R\fserverTimeMs\"\xcb\x01\n" +
|
||||
"\x14PickGameRobotRequest\x12/\n" +
|
||||
"\x04meta\x18\x01 \x01(\v2\x1b.hyapp.robot.v1.RequestMetaR\x04meta\x12\x17\n" +
|
||||
"\agame_id\x18\x02 \x01(\tR\x06gameId\x12\x19\n" +
|
||||
"\bmatch_id\x18\x03 \x01(\tR\amatchId\x12$\n" +
|
||||
"\x0eselected_at_ms\x18\x04 \x01(\x03R\fselectedAtMs\x12(\n" +
|
||||
"\x10exclude_user_ids\x18\x05 \x03(\x03R\x0eexcludeUserIds\"f\n" +
|
||||
"\x11GameRobotResponse\x12+\n" +
|
||||
"\x05robot\x18\x01 \x01(\v2\x15.hyapp.robot.v1.RobotR\x05robot\x12$\n" +
|
||||
"\x0eserver_time_ms\x18\x02 \x01(\x03R\fserverTimeMs\"\x96\x01\n" +
|
||||
"\x19SetGameRobotStatusRequest\x12/\n" +
|
||||
"\x04meta\x18\x01 \x01(\v2\x1b.hyapp.robot.v1.RequestMetaR\x04meta\x12\x17\n" +
|
||||
"\agame_id\x18\x02 \x01(\tR\x06gameId\x12\x17\n" +
|
||||
"\auser_id\x18\x03 \x01(\x03R\x06userId\x12\x16\n" +
|
||||
"\x06status\x18\x04 \x01(\tR\x06status\"{\n" +
|
||||
"\x16DeleteGameRobotRequest\x12/\n" +
|
||||
"\x04meta\x18\x01 \x01(\v2\x1b.hyapp.robot.v1.RequestMetaR\x04meta\x12\x17\n" +
|
||||
"\agame_id\x18\x02 \x01(\tR\x06gameId\x12\x17\n" +
|
||||
"\auser_id\x18\x03 \x01(\x03R\x06userId\"Y\n" +
|
||||
"\x17DeleteGameRobotResponse\x12\x18\n" +
|
||||
"\adeleted\x18\x01 \x01(\bR\adeleted\x12$\n" +
|
||||
"\x0eserver_time_ms\x18\x02 \x01(\x03R\fserverTimeMs\"\xb4\x01\n" +
|
||||
"\x15ListRoomRobotsRequest\x12/\n" +
|
||||
"\x04meta\x18\x01 \x01(\v2\x1b.hyapp.robot.v1.RequestMetaR\x04meta\x12\x1d\n" +
|
||||
"\n" +
|
||||
"room_scene\x18\x02 \x01(\tR\troomScene\x12\x16\n" +
|
||||
"\x06status\x18\x03 \x01(\tR\x06status\x12\x1b\n" +
|
||||
"\tpage_size\x18\x04 \x01(\x05R\bpageSize\x12\x16\n" +
|
||||
"\x06cursor\x18\x05 \x01(\tR\x06cursor\"\x8e\x01\n" +
|
||||
"\x16ListRoomRobotsResponse\x12-\n" +
|
||||
"\x06robots\x18\x01 \x03(\v2\x15.hyapp.robot.v1.RobotR\x06robots\x12\x1f\n" +
|
||||
"\vnext_cursor\x18\x02 \x01(\tR\n" +
|
||||
"nextCursor\x12$\n" +
|
||||
"\x0eserver_time_ms\x18\x03 \x01(\x03R\fserverTimeMs\"\x86\x01\n" +
|
||||
"\x19RegisterRoomRobotsRequest\x12/\n" +
|
||||
"\x04meta\x18\x01 \x01(\v2\x1b.hyapp.robot.v1.RequestMetaR\x04meta\x12\x1d\n" +
|
||||
"\n" +
|
||||
"room_scene\x18\x02 \x01(\tR\troomScene\x12\x19\n" +
|
||||
"\buser_ids\x18\x03 \x03(\x03R\auserIds\"q\n" +
|
||||
"\x1aRegisterRoomRobotsResponse\x12-\n" +
|
||||
"\x06robots\x18\x01 \x03(\v2\x15.hyapp.robot.v1.RobotR\x06robots\x12$\n" +
|
||||
"\x0eserver_time_ms\x18\x02 \x01(\x03R\fserverTimeMs\"\x9c\x01\n" +
|
||||
"\x19SetRoomRobotStatusRequest\x12/\n" +
|
||||
"\x04meta\x18\x01 \x01(\v2\x1b.hyapp.robot.v1.RequestMetaR\x04meta\x12\x1d\n" +
|
||||
"\n" +
|
||||
"room_scene\x18\x02 \x01(\tR\troomScene\x12\x17\n" +
|
||||
"\auser_id\x18\x03 \x01(\x03R\x06userId\x12\x16\n" +
|
||||
"\x06status\x18\x04 \x01(\tR\x06status\"\x81\x01\n" +
|
||||
"\x16DeleteRoomRobotRequest\x12/\n" +
|
||||
"\x04meta\x18\x01 \x01(\v2\x1b.hyapp.robot.v1.RequestMetaR\x04meta\x12\x1d\n" +
|
||||
"\n" +
|
||||
"room_scene\x18\x02 \x01(\tR\troomScene\x12\x17\n" +
|
||||
"\auser_id\x18\x03 \x01(\x03R\x06userId\"Y\n" +
|
||||
"\x17DeleteRoomRobotResponse\x12\x18\n" +
|
||||
"\adeleted\x18\x01 \x01(\bR\adeleted\x12$\n" +
|
||||
"\x0eserver_time_ms\x18\x02 \x01(\x03R\fserverTimeMs\"\xa5\x01\n" +
|
||||
"\x14PickRoomRobotRequest\x12/\n" +
|
||||
"\x04meta\x18\x01 \x01(\v2\x1b.hyapp.robot.v1.RequestMetaR\x04meta\x12\x1d\n" +
|
||||
"\n" +
|
||||
"room_scene\x18\x02 \x01(\tR\troomScene\x12\x17\n" +
|
||||
"\aroom_id\x18\x03 \x01(\tR\x06roomId\x12$\n" +
|
||||
"\x0eselected_at_ms\x18\x04 \x01(\x03R\fselectedAtMs\"f\n" +
|
||||
"\x11RoomRobotResponse\x12+\n" +
|
||||
"\x05robot\x18\x01 \x01(\v2\x15.hyapp.robot.v1.RobotR\x05robot\x12$\n" +
|
||||
"\x0eserver_time_ms\x18\x02 \x01(\x03R\fserverTimeMs2\x82\x04\n" +
|
||||
"\x10GameRobotService\x12_\n" +
|
||||
"\x0eListGameRobots\x12%.hyapp.robot.v1.ListGameRobotsRequest\x1a&.hyapp.robot.v1.ListGameRobotsResponse\x12k\n" +
|
||||
"\x12RegisterGameRobots\x12).hyapp.robot.v1.RegisterGameRobotsRequest\x1a*.hyapp.robot.v1.RegisterGameRobotsResponse\x12X\n" +
|
||||
"\rPickGameRobot\x12$.hyapp.robot.v1.PickGameRobotRequest\x1a!.hyapp.robot.v1.GameRobotResponse\x12b\n" +
|
||||
"\x12SetGameRobotStatus\x12).hyapp.robot.v1.SetGameRobotStatusRequest\x1a!.hyapp.robot.v1.GameRobotResponse\x12b\n" +
|
||||
"\x0fDeleteGameRobot\x12&.hyapp.robot.v1.DeleteGameRobotRequest\x1a'.hyapp.robot.v1.DeleteGameRobotResponse2\x82\x04\n" +
|
||||
"\x10RoomRobotService\x12_\n" +
|
||||
"\x0eListRoomRobots\x12%.hyapp.robot.v1.ListRoomRobotsRequest\x1a&.hyapp.robot.v1.ListRoomRobotsResponse\x12k\n" +
|
||||
"\x12RegisterRoomRobots\x12).hyapp.robot.v1.RegisterRoomRobotsRequest\x1a*.hyapp.robot.v1.RegisterRoomRobotsResponse\x12X\n" +
|
||||
"\rPickRoomRobot\x12$.hyapp.robot.v1.PickRoomRobotRequest\x1a!.hyapp.robot.v1.RoomRobotResponse\x12b\n" +
|
||||
"\x12SetRoomRobotStatus\x12).hyapp.robot.v1.SetRoomRobotStatusRequest\x1a!.hyapp.robot.v1.RoomRobotResponse\x12b\n" +
|
||||
"\x0fDeleteRoomRobot\x12&.hyapp.robot.v1.DeleteRoomRobotRequest\x1a'.hyapp.robot.v1.DeleteRoomRobotResponseB(Z&hyapp.local/api/proto/robot/v1;robotv1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_proto_robot_v1_robot_proto_rawDescOnce sync.Once
|
||||
file_proto_robot_v1_robot_proto_rawDescData = file_proto_robot_v1_robot_proto_rawDesc
|
||||
file_proto_robot_v1_robot_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_proto_robot_v1_robot_proto_rawDescGZIP() []byte {
|
||||
file_proto_robot_v1_robot_proto_rawDescOnce.Do(func() {
|
||||
file_proto_robot_v1_robot_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_robot_v1_robot_proto_rawDescData)
|
||||
file_proto_robot_v1_robot_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_robot_v1_robot_proto_rawDesc), len(file_proto_robot_v1_robot_proto_rawDesc)))
|
||||
})
|
||||
return file_proto_robot_v1_robot_proto_rawDescData
|
||||
}
|
||||
@ -1697,7 +1538,7 @@ func file_proto_robot_v1_robot_proto_init() {
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_proto_robot_v1_robot_proto_rawDesc,
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_robot_v1_robot_proto_rawDesc), len(file_proto_robot_v1_robot_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 20,
|
||||
NumExtensions: 0,
|
||||
@ -1708,7 +1549,6 @@ func file_proto_robot_v1_robot_proto_init() {
|
||||
MessageInfos: file_proto_robot_v1_robot_proto_msgTypes,
|
||||
}.Build()
|
||||
File_proto_robot_v1_robot_proto = out.File
|
||||
file_proto_robot_v1_robot_proto_rawDesc = nil
|
||||
file_proto_robot_v1_robot_proto_goTypes = nil
|
||||
file_proto_robot_v1_robot_proto_depIdxs = nil
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v7.35.0
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v5.29.2
|
||||
// source: proto/robot/v1/robot.proto
|
||||
|
||||
package robotv1
|
||||
@ -115,19 +115,19 @@ type GameRobotServiceServer interface {
|
||||
type UnimplementedGameRobotServiceServer struct{}
|
||||
|
||||
func (UnimplementedGameRobotServiceServer) ListGameRobots(context.Context, *ListGameRobotsRequest) (*ListGameRobotsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListGameRobots not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListGameRobots not implemented")
|
||||
}
|
||||
func (UnimplementedGameRobotServiceServer) RegisterGameRobots(context.Context, *RegisterGameRobotsRequest) (*RegisterGameRobotsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RegisterGameRobots not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RegisterGameRobots not implemented")
|
||||
}
|
||||
func (UnimplementedGameRobotServiceServer) PickGameRobot(context.Context, *PickGameRobotRequest) (*GameRobotResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method PickGameRobot not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method PickGameRobot not implemented")
|
||||
}
|
||||
func (UnimplementedGameRobotServiceServer) SetGameRobotStatus(context.Context, *SetGameRobotStatusRequest) (*GameRobotResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetGameRobotStatus not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetGameRobotStatus not implemented")
|
||||
}
|
||||
func (UnimplementedGameRobotServiceServer) DeleteGameRobot(context.Context, *DeleteGameRobotRequest) (*DeleteGameRobotResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteGameRobot not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteGameRobot not implemented")
|
||||
}
|
||||
func (UnimplementedGameRobotServiceServer) mustEmbedUnimplementedGameRobotServiceServer() {}
|
||||
func (UnimplementedGameRobotServiceServer) testEmbeddedByValue() {}
|
||||
@ -140,7 +140,7 @@ type UnsafeGameRobotServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterGameRobotServiceServer(s grpc.ServiceRegistrar, srv GameRobotServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedGameRobotServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedGameRobotServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -369,19 +369,19 @@ type RoomRobotServiceServer interface {
|
||||
type UnimplementedRoomRobotServiceServer struct{}
|
||||
|
||||
func (UnimplementedRoomRobotServiceServer) ListRoomRobots(context.Context, *ListRoomRobotsRequest) (*ListRoomRobotsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRoomRobots not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRoomRobots not implemented")
|
||||
}
|
||||
func (UnimplementedRoomRobotServiceServer) RegisterRoomRobots(context.Context, *RegisterRoomRobotsRequest) (*RegisterRoomRobotsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RegisterRoomRobots not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RegisterRoomRobots not implemented")
|
||||
}
|
||||
func (UnimplementedRoomRobotServiceServer) PickRoomRobot(context.Context, *PickRoomRobotRequest) (*RoomRobotResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method PickRoomRobot not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method PickRoomRobot not implemented")
|
||||
}
|
||||
func (UnimplementedRoomRobotServiceServer) SetRoomRobotStatus(context.Context, *SetRoomRobotStatusRequest) (*RoomRobotResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetRoomRobotStatus not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetRoomRobotStatus not implemented")
|
||||
}
|
||||
func (UnimplementedRoomRobotServiceServer) DeleteRoomRobot(context.Context, *DeleteRoomRobotRequest) (*DeleteRoomRobotResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteRoomRobot not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteRoomRobot not implemented")
|
||||
}
|
||||
func (UnimplementedRoomRobotServiceServer) mustEmbedUnimplementedRoomRobotServiceServer() {}
|
||||
func (UnimplementedRoomRobotServiceServer) testEmbeddedByValue() {}
|
||||
@ -394,7 +394,7 @@ type UnsafeRoomRobotServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterRoomRobotServiceServer(s grpc.ServiceRegistrar, srv RoomRobotServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedRoomRobotServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedRoomRobotServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v7.35.0
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v5.29.2
|
||||
// source: proto/room/v1/room.proto
|
||||
|
||||
package roomv1
|
||||
@ -522,112 +522,112 @@ type RoomCommandServiceServer interface {
|
||||
type UnimplementedRoomCommandServiceServer struct{}
|
||||
|
||||
func (UnimplementedRoomCommandServiceServer) CreateRoom(context.Context, *CreateRoomRequest) (*CreateRoomResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateRoom not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateRoom not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) UpdateRoomProfile(context.Context, *UpdateRoomProfileRequest) (*UpdateRoomProfileResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateRoomProfile not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateRoomProfile not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) SaveRoomBackground(context.Context, *SaveRoomBackgroundRequest) (*SaveRoomBackgroundResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SaveRoomBackground not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SaveRoomBackground not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) SetRoomBackground(context.Context, *SetRoomBackgroundRequest) (*SetRoomBackgroundResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetRoomBackground not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetRoomBackground not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) JoinRoom(context.Context, *JoinRoomRequest) (*JoinRoomResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method JoinRoom not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method JoinRoom not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) RoomHeartbeat(context.Context, *RoomHeartbeatRequest) (*RoomHeartbeatResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RoomHeartbeat not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RoomHeartbeat not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) LeaveRoom(context.Context, *LeaveRoomRequest) (*LeaveRoomResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method LeaveRoom not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method LeaveRoom not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) CloseRoom(context.Context, *CloseRoomRequest) (*CloseRoomResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CloseRoom not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CloseRoom not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) AdminUpdateRoom(context.Context, *AdminUpdateRoomRequest) (*AdminUpdateRoomResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminUpdateRoom not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminUpdateRoom not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) AdminDeleteRoom(context.Context, *AdminDeleteRoomRequest) (*AdminDeleteRoomResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminDeleteRoom not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminDeleteRoom not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) AdminUpdateRoomRocketConfig(context.Context, *AdminUpdateRoomRocketConfigRequest) (*AdminUpdateRoomRocketConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminUpdateRoomRocketConfig not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminUpdateRoomRocketConfig not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) AdminUpdateRoomSeatConfig(context.Context, *AdminUpdateRoomSeatConfigRequest) (*AdminUpdateRoomSeatConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminUpdateRoomSeatConfig not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminUpdateRoomSeatConfig not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) AdminUpdateHumanRoomRobotConfig(context.Context, *AdminUpdateHumanRoomRobotConfigRequest) (*AdminUpdateHumanRoomRobotConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminUpdateHumanRoomRobotConfig not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminUpdateHumanRoomRobotConfig not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) AdminCreateRoomPin(context.Context, *AdminCreateRoomPinRequest) (*AdminCreateRoomPinResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminCreateRoomPin not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminCreateRoomPin not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) AdminCancelRoomPin(context.Context, *AdminCancelRoomPinRequest) (*AdminCancelRoomPinResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminCancelRoomPin not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminCancelRoomPin not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) AdminCreateRobotRoom(context.Context, *AdminCreateRobotRoomRequest) (*AdminCreateRobotRoomResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminCreateRobotRoom not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminCreateRobotRoom not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) AdminSetRobotRoomStatus(context.Context, *AdminSetRobotRoomStatusRequest) (*AdminSetRobotRoomStatusResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminSetRobotRoomStatus not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminSetRobotRoomStatus not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) AdminRenameOwnerCountryCode(context.Context, *AdminRenameOwnerCountryCodeRequest) (*AdminRenameOwnerCountryCodeResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminRenameOwnerCountryCode not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminRenameOwnerCountryCode not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) MicUp(context.Context, *MicUpRequest) (*MicUpResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method MicUp not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method MicUp not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) MicDown(context.Context, *MicDownRequest) (*MicDownResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method MicDown not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method MicDown not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) ChangeMicSeat(context.Context, *ChangeMicSeatRequest) (*ChangeMicSeatResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ChangeMicSeat not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ChangeMicSeat not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) ConfirmMicPublishing(context.Context, *ConfirmMicPublishingRequest) (*ConfirmMicPublishingResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ConfirmMicPublishing not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ConfirmMicPublishing not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) MicHeartbeat(context.Context, *MicHeartbeatRequest) (*MicHeartbeatResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method MicHeartbeat not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method MicHeartbeat not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) SetMicMute(context.Context, *SetMicMuteRequest) (*SetMicMuteResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetMicMute not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetMicMute not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) ApplyRTCEvent(context.Context, *ApplyRTCEventRequest) (*ApplyRTCEventResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ApplyRTCEvent not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ApplyRTCEvent not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) SetMicSeatLock(context.Context, *SetMicSeatLockRequest) (*SetMicSeatLockResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetMicSeatLock not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetMicSeatLock not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) SetChatEnabled(context.Context, *SetChatEnabledRequest) (*SetChatEnabledResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetChatEnabled not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetChatEnabled not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) SetRoomPassword(context.Context, *SetRoomPasswordRequest) (*SetRoomPasswordResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetRoomPassword not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetRoomPassword not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) SetRoomAdmin(context.Context, *SetRoomAdminRequest) (*SetRoomAdminResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetRoomAdmin not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetRoomAdmin not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) MuteUser(context.Context, *MuteUserRequest) (*MuteUserResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method MuteUser not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method MuteUser not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) KickUser(context.Context, *KickUserRequest) (*KickUserResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method KickUser not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method KickUser not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) UnbanUser(context.Context, *UnbanUserRequest) (*UnbanUserResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UnbanUser not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UnbanUser not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) SystemEvictUser(context.Context, *SystemEvictUserRequest) (*SystemEvictUserResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SystemEvictUser not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SystemEvictUser not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) SendGift(context.Context, *SendGiftRequest) (*SendGiftResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SendGift not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SendGift not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) FollowRoom(context.Context, *FollowRoomRequest) (*FollowRoomResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method FollowRoom not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method FollowRoom not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) UnfollowRoom(context.Context, *UnfollowRoomRequest) (*UnfollowRoomResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UnfollowRoom not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UnfollowRoom not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) mustEmbedUnimplementedRoomCommandServiceServer() {}
|
||||
func (UnimplementedRoomCommandServiceServer) testEmbeddedByValue() {}
|
||||
@ -640,7 +640,7 @@ type UnsafeRoomCommandServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterRoomCommandServiceServer(s grpc.ServiceRegistrar, srv RoomCommandServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedRoomCommandServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedRoomCommandServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -1529,13 +1529,13 @@ type RoomGuardServiceServer interface {
|
||||
type UnimplementedRoomGuardServiceServer struct{}
|
||||
|
||||
func (UnimplementedRoomGuardServiceServer) CheckSpeakPermission(context.Context, *CheckSpeakPermissionRequest) (*CheckSpeakPermissionResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CheckSpeakPermission not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CheckSpeakPermission not implemented")
|
||||
}
|
||||
func (UnimplementedRoomGuardServiceServer) VerifyRoomPresence(context.Context, *VerifyRoomPresenceRequest) (*VerifyRoomPresenceResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VerifyRoomPresence not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VerifyRoomPresence not implemented")
|
||||
}
|
||||
func (UnimplementedRoomGuardServiceServer) ResolveRoomAppCode(context.Context, *ResolveRoomAppCodeRequest) (*ResolveRoomAppCodeResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ResolveRoomAppCode not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ResolveRoomAppCode not implemented")
|
||||
}
|
||||
func (UnimplementedRoomGuardServiceServer) mustEmbedUnimplementedRoomGuardServiceServer() {}
|
||||
func (UnimplementedRoomGuardServiceServer) testEmbeddedByValue() {}
|
||||
@ -1548,7 +1548,7 @@ type UnsafeRoomGuardServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterRoomGuardServiceServer(s grpc.ServiceRegistrar, srv RoomGuardServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedRoomGuardServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedRoomGuardServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -1932,64 +1932,64 @@ type RoomQueryServiceServer interface {
|
||||
type UnimplementedRoomQueryServiceServer struct{}
|
||||
|
||||
func (UnimplementedRoomQueryServiceServer) AdminListRooms(context.Context, *AdminListRoomsRequest) (*AdminListRoomsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminListRooms not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminListRooms not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) AdminGetRoom(context.Context, *AdminGetRoomRequest) (*AdminGetRoomResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminGetRoom not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminGetRoom not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) AdminGetRoomRocketConfig(context.Context, *AdminGetRoomRocketConfigRequest) (*AdminGetRoomRocketConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminGetRoomRocketConfig not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminGetRoomRocketConfig not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) AdminGetRoomSeatConfig(context.Context, *AdminGetRoomSeatConfigRequest) (*AdminGetRoomSeatConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminGetRoomSeatConfig not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminGetRoomSeatConfig not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) AdminGetHumanRoomRobotConfig(context.Context, *AdminGetHumanRoomRobotConfigRequest) (*AdminGetHumanRoomRobotConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminGetHumanRoomRobotConfig not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminGetHumanRoomRobotConfig not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) AdminListRoomPins(context.Context, *AdminListRoomPinsRequest) (*AdminListRoomPinsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminListRoomPins not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminListRoomPins not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) AdminListRobotRooms(context.Context, *AdminListRobotRoomsRequest) (*AdminListRobotRoomsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminListRobotRooms not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminListRobotRooms not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) AdminFilterAvailableRoomRobots(context.Context, *AdminFilterAvailableRoomRobotsRequest) (*AdminFilterAvailableRoomRobotsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminFilterAvailableRoomRobots not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminFilterAvailableRoomRobots not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) ListRooms(context.Context, *ListRoomsRequest) (*ListRoomsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRooms not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRooms not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) ListRoomsByOwners(context.Context, *ListRoomsByOwnersRequest) (*ListRoomsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRoomsByOwners not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRoomsByOwners not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) ListRoomFeeds(context.Context, *ListRoomFeedsRequest) (*ListRoomsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRoomFeeds not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRoomFeeds not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) ListRoomGiftLeaderboard(context.Context, *ListRoomGiftLeaderboardRequest) (*ListRoomGiftLeaderboardResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRoomGiftLeaderboard not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRoomGiftLeaderboard not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) ListRoomContributionRank(context.Context, *ListRoomContributionRankRequest) (*ListRoomContributionRankResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRoomContributionRank not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRoomContributionRank not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) GetMyRoom(context.Context, *GetMyRoomRequest) (*GetMyRoomResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetMyRoom not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetMyRoom not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) GetCurrentRoom(context.Context, *GetCurrentRoomRequest) (*GetCurrentRoomResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetCurrentRoom not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetCurrentRoom not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) GetRoomSnapshot(context.Context, *GetRoomSnapshotRequest) (*GetRoomSnapshotResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetRoomSnapshot not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetRoomSnapshot not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) ListRoomBackgrounds(context.Context, *ListRoomBackgroundsRequest) (*ListRoomBackgroundsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRoomBackgrounds not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRoomBackgrounds not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) GetRoomRocket(context.Context, *GetRoomRocketRequest) (*GetRoomRocketResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetRoomRocket not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetRoomRocket not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) ListRoomOnlineUsers(context.Context, *ListRoomOnlineUsersRequest) (*ListRoomOnlineUsersResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRoomOnlineUsers not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRoomOnlineUsers not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) ListRoomBannedUsers(context.Context, *ListRoomBannedUsersRequest) (*ListRoomBannedUsersResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRoomBannedUsers not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRoomBannedUsers not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) mustEmbedUnimplementedRoomQueryServiceServer() {}
|
||||
func (UnimplementedRoomQueryServiceServer) testEmbeddedByValue() {}
|
||||
@ -2002,7 +2002,7 @@ type UnsafeRoomQueryServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterRoomQueryServiceServer(s grpc.ServiceRegistrar, srv RoomQueryServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedRoomQueryServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedRoomQueryServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v7.35.0
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v5.29.2
|
||||
// source: proto/user/v1/auth.proto
|
||||
|
||||
package userv1
|
||||
@ -236,46 +236,46 @@ type AuthServiceServer interface {
|
||||
type UnimplementedAuthServiceServer struct{}
|
||||
|
||||
func (UnimplementedAuthServiceServer) LoginPassword(context.Context, *LoginPasswordRequest) (*AuthResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method LoginPassword not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method LoginPassword not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) LoginThirdParty(context.Context, *LoginThirdPartyRequest) (*AuthResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method LoginThirdParty not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method LoginThirdParty not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) RegisterThirdParty(context.Context, *RegisterThirdPartyRequest) (*AuthResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RegisterThirdParty not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RegisterThirdParty not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) CheckThirdPartyRegistered(context.Context, *CheckThirdPartyRegisteredRequest) (*CheckThirdPartyRegisteredResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CheckThirdPartyRegistered not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CheckThirdPartyRegistered not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) SetPassword(context.Context, *SetPasswordRequest) (*SetPasswordResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetPassword not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetPassword not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) QuickCreateAccount(context.Context, *QuickCreateAccountRequest) (*QuickCreateAccountResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method QuickCreateAccount not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method QuickCreateAccount not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) RefreshToken(context.Context, *RefreshTokenRequest) (*RefreshTokenResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RefreshToken not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RefreshToken not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) Logout(context.Context, *LogoutRequest) (*LogoutResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Logout not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method Logout not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) RecordLoginBlocked(context.Context, *RecordLoginBlockedRequest) (*RecordLoginBlockedResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RecordLoginBlocked not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RecordLoginBlocked not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) CheckLoginRiskIPWhitelist(context.Context, *CheckLoginRiskIPWhitelistRequest) (*CheckLoginRiskIPWhitelistResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CheckLoginRiskIPWhitelist not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CheckLoginRiskIPWhitelist not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) RefreshLoginRiskWhitelistCache(context.Context, *RefreshLoginRiskWhitelistCacheRequest) (*RefreshLoginRiskWhitelistCacheResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RefreshLoginRiskWhitelistCache not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RefreshLoginRiskWhitelistCache not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) GetRegisterRiskConfig(context.Context, *GetRegisterRiskConfigRequest) (*GetRegisterRiskConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetRegisterRiskConfig not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetRegisterRiskConfig not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) UpdateRegisterRiskConfig(context.Context, *UpdateRegisterRiskConfigRequest) (*UpdateRegisterRiskConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateRegisterRiskConfig not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateRegisterRiskConfig not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) AppHeartbeat(context.Context, *AppHeartbeatRequest) (*AppHeartbeatResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AppHeartbeat not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AppHeartbeat not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) mustEmbedUnimplementedAuthServiceServer() {}
|
||||
func (UnimplementedAuthServiceServer) testEmbeddedByValue() {}
|
||||
@ -288,7 +288,7 @@ type UnsafeAuthServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterAuthServiceServer(s grpc.ServiceRegistrar, srv AuthServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedAuthServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedAuthServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v7.35.0
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v5.29.2
|
||||
// source: proto/user/v1/host.proto
|
||||
|
||||
package userv1
|
||||
@ -472,100 +472,100 @@ type UserHostServiceServer interface {
|
||||
type UnimplementedUserHostServiceServer struct{}
|
||||
|
||||
func (UnimplementedUserHostServiceServer) SearchAgencies(context.Context, *SearchAgenciesRequest) (*SearchAgenciesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SearchAgencies not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SearchAgencies not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) ApplyToAgency(context.Context, *ApplyToAgencyRequest) (*ApplyToAgencyResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ApplyToAgency not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ApplyToAgency not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) ReviewAgencyApplication(context.Context, *ReviewAgencyApplicationRequest) (*ReviewAgencyApplicationResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ReviewAgencyApplication not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ReviewAgencyApplication not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) KickAgencyHost(context.Context, *KickAgencyHostRequest) (*KickAgencyHostResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method KickAgencyHost not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method KickAgencyHost not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) InviteAgency(context.Context, *InviteAgencyRequest) (*InviteAgencyResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method InviteAgency not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method InviteAgency not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) InviteBD(context.Context, *InviteBDRequest) (*InviteBDResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method InviteBD not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method InviteBD not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) InviteHost(context.Context, *InviteHostRequest) (*InviteHostResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method InviteHost not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method InviteHost not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) ListBDLeaderBDs(context.Context, *ListBDLeaderBDsRequest) (*ListBDLeaderBDsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListBDLeaderBDs not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListBDLeaderBDs not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) ListBDLeaderAgencies(context.Context, *ListBDLeaderAgenciesRequest) (*ListBDLeaderAgenciesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListBDLeaderAgencies not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListBDLeaderAgencies not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) ListBDAgencies(context.Context, *ListBDAgenciesRequest) (*ListBDAgenciesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListBDAgencies not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListBDAgencies not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) ListManagerTeamAgencies(context.Context, *ListManagerTeamAgenciesRequest) (*ListManagerTeamAgenciesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListManagerTeamAgencies not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListManagerTeamAgencies not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) ListRoleInvitations(context.Context, *ListRoleInvitationsRequest) (*ListRoleInvitationsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRoleInvitations not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRoleInvitations not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) GetRoleInvitation(context.Context, *GetRoleInvitationRequest) (*GetRoleInvitationResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetRoleInvitation not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetRoleInvitation not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) ProcessRoleInvitation(context.Context, *ProcessRoleInvitationRequest) (*ProcessRoleInvitationResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ProcessRoleInvitation not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ProcessRoleInvitation not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) GetHostProfile(context.Context, *GetHostProfileRequest) (*GetHostProfileResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetHostProfile not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetHostProfile not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) BatchGetHostProfiles(context.Context, *BatchGetHostProfilesRequest) (*BatchGetHostProfilesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BatchGetHostProfiles not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method BatchGetHostProfiles not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) GetBDProfile(context.Context, *GetBDProfileRequest) (*GetBDProfileResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetBDProfile not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetBDProfile not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) GetCoinSellerProfile(context.Context, *GetCoinSellerProfileRequest) (*GetCoinSellerProfileResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetCoinSellerProfile not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetCoinSellerProfile not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) ListActiveCoinSellersInMyRegion(context.Context, *ListActiveCoinSellersInMyRegionRequest) (*ListActiveCoinSellersInMyRegionResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListActiveCoinSellersInMyRegion not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListActiveCoinSellersInMyRegion not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) CreateSubCoinSeller(context.Context, *CreateSubCoinSellerRequest) (*CreateSubCoinSellerResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateSubCoinSeller not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateSubCoinSeller not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) SubmitSubCoinSellerApplication(context.Context, *SubmitSubCoinSellerApplicationRequest) (*SubmitSubCoinSellerApplicationResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SubmitSubCoinSellerApplication not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SubmitSubCoinSellerApplication not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) ListSubCoinSellers(context.Context, *ListSubCoinSellersRequest) (*ListSubCoinSellersResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListSubCoinSellers not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListSubCoinSellers not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) CheckCoinSellerSubRelation(context.Context, *CheckCoinSellerSubRelationRequest) (*CheckCoinSellerSubRelationResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CheckCoinSellerSubRelation not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CheckCoinSellerSubRelation not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) GetUserRoleSummary(context.Context, *GetUserRoleSummaryRequest) (*GetUserRoleSummaryResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetUserRoleSummary not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetUserRoleSummary not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) GetAgency(context.Context, *GetAgencyRequest) (*GetAgencyResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetAgency not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetAgency not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) CheckBusinessCapability(context.Context, *CheckBusinessCapabilityRequest) (*CheckBusinessCapabilityResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CheckBusinessCapability not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CheckBusinessCapability not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) GetRoleScopePolicy(context.Context, *GetRoleScopePolicyRequest) (*GetRoleScopePolicyResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetRoleScopePolicy not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetRoleScopePolicy not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) GetAgencyMembers(context.Context, *GetAgencyMembersRequest) (*GetAgencyMembersResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetAgencyMembers not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetAgencyMembers not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) GetAgencyApplications(context.Context, *GetAgencyApplicationsRequest) (*GetAgencyApplicationsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetAgencyApplications not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetAgencyApplications not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) UpdateAgencyProfile(context.Context, *UpdateAgencyProfileRequest) (*UpdateAgencyProfileResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateAgencyProfile not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateAgencyProfile not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) GetHostEngagementStats(context.Context, *GetHostEngagementStatsRequest) (*GetHostEngagementStatsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetHostEngagementStats not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetHostEngagementStats not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) RecordPrivateMessageEvent(context.Context, *RecordPrivateMessageEventRequest) (*RecordPrivateMessageEventResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RecordPrivateMessageEvent not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RecordPrivateMessageEvent not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) mustEmbedUnimplementedUserHostServiceServer() {}
|
||||
func (UnimplementedUserHostServiceServer) testEmbeddedByValue() {}
|
||||
@ -578,7 +578,7 @@ type UnsafeUserHostServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterUserHostServiceServer(s grpc.ServiceRegistrar, srv UserHostServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedUserHostServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedUserHostServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -1496,40 +1496,40 @@ type UserHostAdminServiceServer interface {
|
||||
type UnimplementedUserHostAdminServiceServer struct{}
|
||||
|
||||
func (UnimplementedUserHostAdminServiceServer) CreateBDLeader(context.Context, *CreateBDLeaderRequest) (*CreateBDLeaderResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateBDLeader not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateBDLeader not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostAdminServiceServer) CreateBD(context.Context, *CreateBDRequest) (*CreateBDResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateBD not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateBD not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostAdminServiceServer) SetBDStatus(context.Context, *SetBDStatusRequest) (*SetBDStatusResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetBDStatus not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetBDStatus not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostAdminServiceServer) CreateCoinSeller(context.Context, *CreateCoinSellerRequest) (*CreateCoinSellerResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateCoinSeller not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateCoinSeller not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostAdminServiceServer) SetCoinSellerStatus(context.Context, *SetCoinSellerStatusRequest) (*SetCoinSellerStatusResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetCoinSellerStatus not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetCoinSellerStatus not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostAdminServiceServer) ReviewSubCoinSellerApplication(context.Context, *ReviewSubCoinSellerApplicationRequest) (*ReviewSubCoinSellerApplicationResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ReviewSubCoinSellerApplication not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ReviewSubCoinSellerApplication not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostAdminServiceServer) CreateAgency(context.Context, *CreateAgencyRequest) (*CreateAgencyResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateAgency not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateAgency not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostAdminServiceServer) AdminAddAgencyHost(context.Context, *AdminAddAgencyHostRequest) (*AdminAddAgencyHostResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminAddAgencyHost not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminAddAgencyHost not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostAdminServiceServer) CloseAgency(context.Context, *CloseAgencyRequest) (*CloseAgencyResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CloseAgency not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CloseAgency not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostAdminServiceServer) DeleteAgency(context.Context, *DeleteAgencyRequest) (*DeleteAgencyResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteAgency not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteAgency not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostAdminServiceServer) SetAgencyStatus(context.Context, *SetAgencyStatusRequest) (*SetAgencyStatusResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetAgencyStatus not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetAgencyStatus not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostAdminServiceServer) SetAgencyJoinEnabled(context.Context, *SetAgencyJoinEnabledRequest) (*SetAgencyJoinEnabledResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetAgencyJoinEnabled not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetAgencyJoinEnabled not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostAdminServiceServer) mustEmbedUnimplementedUserHostAdminServiceServer() {}
|
||||
func (UnimplementedUserHostAdminServiceServer) testEmbeddedByValue() {}
|
||||
@ -1542,7 +1542,7 @@ type UnsafeUserHostAdminServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterUserHostAdminServiceServer(s grpc.ServiceRegistrar, srv UserHostAdminServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedUserHostAdminServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedUserHostAdminServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v7.35.0
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v5.29.2
|
||||
// source: proto/user/v1/user.proto
|
||||
|
||||
package userv1
|
||||
@ -379,79 +379,79 @@ type UserServiceServer interface {
|
||||
type UnimplementedUserServiceServer struct{}
|
||||
|
||||
func (UnimplementedUserServiceServer) GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetUser not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetUser not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) GetInviteAttribution(context.Context, *GetInviteAttributionRequest) (*GetInviteAttributionResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetInviteAttribution not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetInviteAttribution not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) BusinessUserLookup(context.Context, *BusinessUserLookupRequest) (*BusinessUserLookupResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BusinessUserLookup not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method BusinessUserLookup not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) GetMyProfileStats(context.Context, *GetMyProfileStatsRequest) (*GetMyProfileStatsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetMyProfileStats not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetMyProfileStats not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) BatchGetUsers(context.Context, *BatchGetUsersRequest) (*BatchGetUsersResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BatchGetUsers not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method BatchGetUsers not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) BatchGetUserAdminProfiles(context.Context, *BatchGetUserAdminProfilesRequest) (*BatchGetUserAdminProfilesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BatchGetUserAdminProfiles not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method BatchGetUserAdminProfiles not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) AdminIssueUserAccessToken(context.Context, *AdminIssueUserAccessTokenRequest) (*AdminIssueUserAccessTokenResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminIssueUserAccessToken not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminIssueUserAccessToken not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) BatchGetRoomBasicUsers(context.Context, *BatchGetRoomBasicUsersRequest) (*BatchGetRoomBasicUsersResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BatchGetRoomBasicUsers not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method BatchGetRoomBasicUsers not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) ListUserIDs(context.Context, *ListUserIDsRequest) (*ListUserIDsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListUserIDs not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListUserIDs not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) GetUserMicLifetimeStats(context.Context, *GetUserMicLifetimeStatsRequest) (*GetUserMicLifetimeStatsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetUserMicLifetimeStats not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetUserMicLifetimeStats not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) UpdateUserProfile(context.Context, *UpdateUserProfileRequest) (*UpdateUserProfileResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateUserProfile not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateUserProfile not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) UpdateUserProfileBackground(context.Context, *UpdateUserProfileBackgroundRequest) (*UpdateUserProfileBackgroundResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateUserProfileBackground not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateUserProfileBackground not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) UpdateUserContactInfo(context.Context, *UpdateUserContactInfoRequest) (*UpdateUserContactInfoResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateUserContactInfo not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateUserContactInfo not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) UpdateUserWithdrawAddress(context.Context, *UpdateUserWithdrawAddressRequest) (*UpdateUserWithdrawAddressResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateUserWithdrawAddress not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateUserWithdrawAddress not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) ChangeUserCountry(context.Context, *ChangeUserCountryRequest) (*ChangeUserCountryResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ChangeUserCountry not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ChangeUserCountry not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) AdminChangeUserCountry(context.Context, *AdminChangeUserCountryRequest) (*ChangeUserCountryResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminChangeUserCountry not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminChangeUserCountry not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) SetUserStatus(context.Context, *SetUserStatusRequest) (*SetUserStatusResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetUserStatus not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetUserStatus not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) AdminBanUser(context.Context, *AdminBanUserRequest) (*AdminBanUserResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminBanUser not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminBanUser not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) AdminUnbanUser(context.Context, *AdminUnbanUserRequest) (*AdminUnbanUserResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminUnbanUser not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminUnbanUser not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) CreateManagerUserBlock(context.Context, *CreateManagerUserBlockRequest) (*CreateManagerUserBlockResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateManagerUserBlock not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateManagerUserBlock not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) ListManagerUserBlocks(context.Context, *ListManagerUserBlocksRequest) (*ListManagerUserBlocksResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListManagerUserBlocks not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListManagerUserBlocks not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) UnblockManagerUser(context.Context, *UnblockManagerUserRequest) (*UnblockManagerUserResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UnblockManagerUser not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UnblockManagerUser not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) CompleteOnboarding(context.Context, *CompleteOnboardingRequest) (*CompleteOnboardingResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CompleteOnboarding not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CompleteOnboarding not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) SearchInviteReferrer(context.Context, *SearchInviteReferrerRequest) (*SearchInviteReferrerResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SearchInviteReferrer not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SearchInviteReferrer not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) BindInviteReferrer(context.Context, *BindInviteReferrerRequest) (*BindInviteReferrerResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BindInviteReferrer not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method BindInviteReferrer not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) mustEmbedUnimplementedUserServiceServer() {}
|
||||
func (UnimplementedUserServiceServer) testEmbeddedByValue() {}
|
||||
@ -464,7 +464,7 @@ type UnsafeUserServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedUserServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedUserServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -1215,37 +1215,37 @@ type UserSocialServiceServer interface {
|
||||
type UnimplementedUserSocialServiceServer struct{}
|
||||
|
||||
func (UnimplementedUserSocialServiceServer) RecordProfileVisit(context.Context, *RecordProfileVisitRequest) (*RecordProfileVisitResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RecordProfileVisit not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RecordProfileVisit not implemented")
|
||||
}
|
||||
func (UnimplementedUserSocialServiceServer) ListProfileVisitors(context.Context, *ListProfileVisitorsRequest) (*ListProfileVisitorsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListProfileVisitors not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListProfileVisitors not implemented")
|
||||
}
|
||||
func (UnimplementedUserSocialServiceServer) FollowUser(context.Context, *FollowUserRequest) (*FollowUserResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method FollowUser not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method FollowUser not implemented")
|
||||
}
|
||||
func (UnimplementedUserSocialServiceServer) UnfollowUser(context.Context, *UnfollowUserRequest) (*UnfollowUserResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UnfollowUser not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UnfollowUser not implemented")
|
||||
}
|
||||
func (UnimplementedUserSocialServiceServer) ListFollowing(context.Context, *ListFollowingRequest) (*ListFollowingResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListFollowing not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListFollowing not implemented")
|
||||
}
|
||||
func (UnimplementedUserSocialServiceServer) ApplyFriend(context.Context, *ApplyFriendRequest) (*ApplyFriendResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ApplyFriend not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ApplyFriend not implemented")
|
||||
}
|
||||
func (UnimplementedUserSocialServiceServer) AcceptFriendApplication(context.Context, *AcceptFriendApplicationRequest) (*AcceptFriendApplicationResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AcceptFriendApplication not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AcceptFriendApplication not implemented")
|
||||
}
|
||||
func (UnimplementedUserSocialServiceServer) DeleteFriend(context.Context, *DeleteFriendRequest) (*DeleteFriendResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteFriend not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteFriend not implemented")
|
||||
}
|
||||
func (UnimplementedUserSocialServiceServer) ListFriends(context.Context, *ListFriendsRequest) (*ListFriendsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListFriends not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListFriends not implemented")
|
||||
}
|
||||
func (UnimplementedUserSocialServiceServer) ListFriendApplications(context.Context, *ListFriendApplicationsRequest) (*ListFriendApplicationsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListFriendApplications not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListFriendApplications not implemented")
|
||||
}
|
||||
func (UnimplementedUserSocialServiceServer) SubmitReport(context.Context, *SubmitReportRequest) (*SubmitReportResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SubmitReport not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SubmitReport not implemented")
|
||||
}
|
||||
func (UnimplementedUserSocialServiceServer) mustEmbedUnimplementedUserSocialServiceServer() {}
|
||||
func (UnimplementedUserSocialServiceServer) testEmbeddedByValue() {}
|
||||
@ -1258,7 +1258,7 @@ type UnsafeUserSocialServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterUserSocialServiceServer(s grpc.ServiceRegistrar, srv UserSocialServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedUserSocialServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedUserSocialServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -1662,28 +1662,28 @@ type UserCPServiceServer interface {
|
||||
type UnimplementedUserCPServiceServer struct{}
|
||||
|
||||
func (UnimplementedUserCPServiceServer) ListCPApplications(context.Context, *ListCPApplicationsRequest) (*ListCPApplicationsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListCPApplications not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListCPApplications not implemented")
|
||||
}
|
||||
func (UnimplementedUserCPServiceServer) AcceptCPApplication(context.Context, *AcceptCPApplicationRequest) (*AcceptCPApplicationResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AcceptCPApplication not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AcceptCPApplication not implemented")
|
||||
}
|
||||
func (UnimplementedUserCPServiceServer) RejectCPApplication(context.Context, *RejectCPApplicationRequest) (*RejectCPApplicationResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RejectCPApplication not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RejectCPApplication not implemented")
|
||||
}
|
||||
func (UnimplementedUserCPServiceServer) ListCPRelationships(context.Context, *ListCPRelationshipsRequest) (*ListCPRelationshipsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListCPRelationships not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListCPRelationships not implemented")
|
||||
}
|
||||
func (UnimplementedUserCPServiceServer) ListCPIntimacyLeaderboard(context.Context, *ListCPIntimacyLeaderboardRequest) (*ListCPIntimacyLeaderboardResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListCPIntimacyLeaderboard not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListCPIntimacyLeaderboard not implemented")
|
||||
}
|
||||
func (UnimplementedUserCPServiceServer) PrepareBreakCPRelationship(context.Context, *PrepareBreakCPRelationshipRequest) (*PrepareBreakCPRelationshipResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method PrepareBreakCPRelationship not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method PrepareBreakCPRelationship not implemented")
|
||||
}
|
||||
func (UnimplementedUserCPServiceServer) ConfirmBreakCPRelationship(context.Context, *ConfirmBreakCPRelationshipRequest) (*ConfirmBreakCPRelationshipResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ConfirmBreakCPRelationship not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ConfirmBreakCPRelationship not implemented")
|
||||
}
|
||||
func (UnimplementedUserCPServiceServer) CancelBreakCPRelationship(context.Context, *CancelBreakCPRelationshipRequest) (*CancelBreakCPRelationshipResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CancelBreakCPRelationship not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CancelBreakCPRelationship not implemented")
|
||||
}
|
||||
func (UnimplementedUserCPServiceServer) mustEmbedUnimplementedUserCPServiceServer() {}
|
||||
func (UnimplementedUserCPServiceServer) testEmbeddedByValue() {}
|
||||
@ -1696,7 +1696,7 @@ type UnsafeUserCPServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterUserCPServiceServer(s grpc.ServiceRegistrar, srv UserCPServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedUserCPServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedUserCPServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -1956,10 +1956,10 @@ type UserCPInternalServiceServer interface {
|
||||
type UnimplementedUserCPInternalServiceServer struct{}
|
||||
|
||||
func (UnimplementedUserCPInternalServiceServer) ConsumeRoomGiftCPEvent(context.Context, *ConsumeRoomGiftCPEventRequest) (*ConsumeRoomGiftCPEventResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ConsumeRoomGiftCPEvent not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ConsumeRoomGiftCPEvent not implemented")
|
||||
}
|
||||
func (UnimplementedUserCPInternalServiceServer) ListCPWeeklyRankEntries(context.Context, *ListCPWeeklyRankEntriesRequest) (*ListCPWeeklyRankEntriesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListCPWeeklyRankEntries not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListCPWeeklyRankEntries not implemented")
|
||||
}
|
||||
func (UnimplementedUserCPInternalServiceServer) mustEmbedUnimplementedUserCPInternalServiceServer() {}
|
||||
func (UnimplementedUserCPInternalServiceServer) testEmbeddedByValue() {}
|
||||
@ -1972,7 +1972,7 @@ type UnsafeUserCPInternalServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterUserCPInternalServiceServer(s grpc.ServiceRegistrar, srv UserCPInternalServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedUserCPInternalServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedUserCPInternalServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -2165,25 +2165,25 @@ type UserCronServiceServer interface {
|
||||
type UnimplementedUserCronServiceServer struct{}
|
||||
|
||||
func (UnimplementedUserCronServiceServer) ProcessLoginIPRiskBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ProcessLoginIPRiskBatch not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ProcessLoginIPRiskBatch not implemented")
|
||||
}
|
||||
func (UnimplementedUserCronServiceServer) ProcessRegionRebuildBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ProcessRegionRebuildBatch not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ProcessRegionRebuildBatch not implemented")
|
||||
}
|
||||
func (UnimplementedUserCronServiceServer) CompensateMicOpenSessions(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CompensateMicOpenSessions not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CompensateMicOpenSessions not implemented")
|
||||
}
|
||||
func (UnimplementedUserCronServiceServer) CompensateRoomOpenSessions(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CompensateRoomOpenSessions not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CompensateRoomOpenSessions not implemented")
|
||||
}
|
||||
func (UnimplementedUserCronServiceServer) ExpireManagerUserBlocks(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ExpireManagerUserBlocks not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ExpireManagerUserBlocks not implemented")
|
||||
}
|
||||
func (UnimplementedUserCronServiceServer) ExpireAdminUserBans(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ExpireAdminUserBans not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ExpireAdminUserBans not implemented")
|
||||
}
|
||||
func (UnimplementedUserCronServiceServer) RefreshCPIntimacyLeaderboard(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RefreshCPIntimacyLeaderboard not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RefreshCPIntimacyLeaderboard not implemented")
|
||||
}
|
||||
func (UnimplementedUserCronServiceServer) mustEmbedUnimplementedUserCronServiceServer() {}
|
||||
func (UnimplementedUserCronServiceServer) testEmbeddedByValue() {}
|
||||
@ -2196,7 +2196,7 @@ type UnsafeUserCronServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterUserCronServiceServer(s grpc.ServiceRegistrar, srv UserCronServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedUserCronServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedUserCronServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -2434,10 +2434,10 @@ type UserDeviceServiceServer interface {
|
||||
type UnimplementedUserDeviceServiceServer struct{}
|
||||
|
||||
func (UnimplementedUserDeviceServiceServer) BindPushToken(context.Context, *BindPushTokenRequest) (*BindPushTokenResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BindPushToken not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method BindPushToken not implemented")
|
||||
}
|
||||
func (UnimplementedUserDeviceServiceServer) DeletePushToken(context.Context, *DeletePushTokenRequest) (*DeletePushTokenResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeletePushToken not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DeletePushToken not implemented")
|
||||
}
|
||||
func (UnimplementedUserDeviceServiceServer) mustEmbedUnimplementedUserDeviceServiceServer() {}
|
||||
func (UnimplementedUserDeviceServiceServer) testEmbeddedByValue() {}
|
||||
@ -2450,7 +2450,7 @@ type UnsafeUserDeviceServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterUserDeviceServiceServer(s grpc.ServiceRegistrar, srv UserDeviceServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedUserDeviceServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedUserDeviceServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -2565,7 +2565,7 @@ type AppRegistryServiceServer interface {
|
||||
type UnimplementedAppRegistryServiceServer struct{}
|
||||
|
||||
func (UnimplementedAppRegistryServiceServer) ResolveApp(context.Context, *ResolveAppRequest) (*ResolveAppResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ResolveApp not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ResolveApp not implemented")
|
||||
}
|
||||
func (UnimplementedAppRegistryServiceServer) mustEmbedUnimplementedAppRegistryServiceServer() {}
|
||||
func (UnimplementedAppRegistryServiceServer) testEmbeddedByValue() {}
|
||||
@ -2578,7 +2578,7 @@ type UnsafeAppRegistryServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterAppRegistryServiceServer(s grpc.ServiceRegistrar, srv AppRegistryServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedAppRegistryServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedAppRegistryServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -2686,10 +2686,10 @@ type CountryAdminServiceServer interface {
|
||||
type UnimplementedCountryAdminServiceServer struct{}
|
||||
|
||||
func (UnimplementedCountryAdminServiceServer) ListCountries(context.Context, *ListCountriesRequest) (*ListCountriesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListCountries not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListCountries not implemented")
|
||||
}
|
||||
func (UnimplementedCountryAdminServiceServer) UpdateCountry(context.Context, *UpdateCountryRequest) (*CountryResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateCountry not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateCountry not implemented")
|
||||
}
|
||||
func (UnimplementedCountryAdminServiceServer) mustEmbedUnimplementedCountryAdminServiceServer() {}
|
||||
func (UnimplementedCountryAdminServiceServer) testEmbeddedByValue() {}
|
||||
@ -2702,7 +2702,7 @@ type UnsafeCountryAdminServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterCountryAdminServiceServer(s grpc.ServiceRegistrar, srv CountryAdminServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedCountryAdminServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedCountryAdminServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -2830,10 +2830,10 @@ type CountryQueryServiceServer interface {
|
||||
type UnimplementedCountryQueryServiceServer struct{}
|
||||
|
||||
func (UnimplementedCountryQueryServiceServer) ListRegistrationCountries(context.Context, *ListRegistrationCountriesRequest) (*ListRegistrationCountriesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRegistrationCountries not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRegistrationCountries not implemented")
|
||||
}
|
||||
func (UnimplementedCountryQueryServiceServer) ListLoginRiskBlockedCountries(context.Context, *ListLoginRiskBlockedCountriesRequest) (*ListLoginRiskBlockedCountriesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListLoginRiskBlockedCountries not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListLoginRiskBlockedCountries not implemented")
|
||||
}
|
||||
func (UnimplementedCountryQueryServiceServer) mustEmbedUnimplementedCountryQueryServiceServer() {}
|
||||
func (UnimplementedCountryQueryServiceServer) testEmbeddedByValue() {}
|
||||
@ -2846,7 +2846,7 @@ type UnsafeCountryQueryServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterCountryQueryServiceServer(s grpc.ServiceRegistrar, srv CountryQueryServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedCountryQueryServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedCountryQueryServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -3002,16 +3002,16 @@ type RegionAdminServiceServer interface {
|
||||
type UnimplementedRegionAdminServiceServer struct{}
|
||||
|
||||
func (UnimplementedRegionAdminServiceServer) ListRegions(context.Context, *ListRegionsRequest) (*ListRegionsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRegions not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRegions not implemented")
|
||||
}
|
||||
func (UnimplementedRegionAdminServiceServer) GetRegion(context.Context, *GetRegionRequest) (*RegionResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetRegion not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetRegion not implemented")
|
||||
}
|
||||
func (UnimplementedRegionAdminServiceServer) UpdateRegion(context.Context, *UpdateRegionRequest) (*RegionResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateRegion not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateRegion not implemented")
|
||||
}
|
||||
func (UnimplementedRegionAdminServiceServer) ReplaceRegionCountries(context.Context, *ReplaceRegionCountriesRequest) (*RegionResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ReplaceRegionCountries not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ReplaceRegionCountries not implemented")
|
||||
}
|
||||
func (UnimplementedRegionAdminServiceServer) mustEmbedUnimplementedRegionAdminServiceServer() {}
|
||||
func (UnimplementedRegionAdminServiceServer) testEmbeddedByValue() {}
|
||||
@ -3024,7 +3024,7 @@ type UnsafeRegionAdminServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterRegionAdminServiceServer(s grpc.ServiceRegistrar, srv RegionAdminServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedRegionAdminServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedRegionAdminServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -3261,25 +3261,25 @@ type UserIdentityServiceServer interface {
|
||||
type UnimplementedUserIdentityServiceServer struct{}
|
||||
|
||||
func (UnimplementedUserIdentityServiceServer) GetUserIdentity(context.Context, *GetUserIdentityRequest) (*GetUserIdentityResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetUserIdentity not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetUserIdentity not implemented")
|
||||
}
|
||||
func (UnimplementedUserIdentityServiceServer) ResolveDisplayUserID(context.Context, *ResolveDisplayUserIDRequest) (*ResolveDisplayUserIDResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ResolveDisplayUserID not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ResolveDisplayUserID not implemented")
|
||||
}
|
||||
func (UnimplementedUserIdentityServiceServer) ChangeDisplayUserID(context.Context, *ChangeDisplayUserIDRequest) (*ChangeDisplayUserIDResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ChangeDisplayUserID not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ChangeDisplayUserID not implemented")
|
||||
}
|
||||
func (UnimplementedUserIdentityServiceServer) ApplyPrettyDisplayUserID(context.Context, *ApplyPrettyDisplayUserIDRequest) (*ApplyPrettyDisplayUserIDResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ApplyPrettyDisplayUserID not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ApplyPrettyDisplayUserID not implemented")
|
||||
}
|
||||
func (UnimplementedUserIdentityServiceServer) ListAvailablePrettyDisplayIDs(context.Context, *ListAvailablePrettyDisplayIDsRequest) (*ListAvailablePrettyDisplayIDsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListAvailablePrettyDisplayIDs not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListAvailablePrettyDisplayIDs not implemented")
|
||||
}
|
||||
func (UnimplementedUserIdentityServiceServer) ApplyPrettyDisplayIDFromPool(context.Context, *ApplyPrettyDisplayIDFromPoolRequest) (*ApplyPrettyDisplayIDFromPoolResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ApplyPrettyDisplayIDFromPool not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ApplyPrettyDisplayIDFromPool not implemented")
|
||||
}
|
||||
func (UnimplementedUserIdentityServiceServer) ExpirePrettyDisplayUserID(context.Context, *ExpirePrettyDisplayUserIDRequest) (*ExpirePrettyDisplayUserIDResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ExpirePrettyDisplayUserID not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ExpirePrettyDisplayUserID not implemented")
|
||||
}
|
||||
func (UnimplementedUserIdentityServiceServer) mustEmbedUnimplementedUserIdentityServiceServer() {}
|
||||
func (UnimplementedUserIdentityServiceServer) testEmbeddedByValue() {}
|
||||
@ -3292,7 +3292,7 @@ type UnsafeUserIdentityServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterUserIdentityServiceServer(s grpc.ServiceRegistrar, srv UserIdentityServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedUserIdentityServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedUserIdentityServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -3608,28 +3608,28 @@ type UserPrettyDisplayIDAdminServiceServer interface {
|
||||
type UnimplementedUserPrettyDisplayIDAdminServiceServer struct{}
|
||||
|
||||
func (UnimplementedUserPrettyDisplayIDAdminServiceServer) ListPrettyDisplayIDPools(context.Context, *ListPrettyDisplayIDPoolsRequest) (*ListPrettyDisplayIDPoolsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListPrettyDisplayIDPools not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListPrettyDisplayIDPools not implemented")
|
||||
}
|
||||
func (UnimplementedUserPrettyDisplayIDAdminServiceServer) CreatePrettyDisplayIDPool(context.Context, *CreatePrettyDisplayIDPoolRequest) (*PrettyDisplayIDPoolResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreatePrettyDisplayIDPool not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreatePrettyDisplayIDPool not implemented")
|
||||
}
|
||||
func (UnimplementedUserPrettyDisplayIDAdminServiceServer) UpdatePrettyDisplayIDPool(context.Context, *UpdatePrettyDisplayIDPoolRequest) (*PrettyDisplayIDPoolResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdatePrettyDisplayIDPool not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdatePrettyDisplayIDPool not implemented")
|
||||
}
|
||||
func (UnimplementedUserPrettyDisplayIDAdminServiceServer) GeneratePrettyDisplayIDs(context.Context, *GeneratePrettyDisplayIDsRequest) (*GeneratePrettyDisplayIDsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GeneratePrettyDisplayIDs not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GeneratePrettyDisplayIDs not implemented")
|
||||
}
|
||||
func (UnimplementedUserPrettyDisplayIDAdminServiceServer) ListPrettyDisplayIDs(context.Context, *ListPrettyDisplayIDsRequest) (*ListPrettyDisplayIDsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListPrettyDisplayIDs not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListPrettyDisplayIDs not implemented")
|
||||
}
|
||||
func (UnimplementedUserPrettyDisplayIDAdminServiceServer) SetPrettyDisplayIDStatus(context.Context, *SetPrettyDisplayIDStatusRequest) (*PrettyDisplayIDResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetPrettyDisplayIDStatus not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetPrettyDisplayIDStatus not implemented")
|
||||
}
|
||||
func (UnimplementedUserPrettyDisplayIDAdminServiceServer) RecyclePrettyDisplayID(context.Context, *RecyclePrettyDisplayIDRequest) (*PrettyDisplayIDResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RecyclePrettyDisplayID not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RecyclePrettyDisplayID not implemented")
|
||||
}
|
||||
func (UnimplementedUserPrettyDisplayIDAdminServiceServer) AdminGrantPrettyDisplayID(context.Context, *AdminGrantPrettyDisplayIDRequest) (*AdminGrantPrettyDisplayIDResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminGrantPrettyDisplayID not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminGrantPrettyDisplayID not implemented")
|
||||
}
|
||||
func (UnimplementedUserPrettyDisplayIDAdminServiceServer) mustEmbedUnimplementedUserPrettyDisplayIDAdminServiceServer() {
|
||||
}
|
||||
@ -3643,7 +3643,7 @@ type UnsafeUserPrettyDisplayIDAdminServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterUserPrettyDisplayIDAdminServiceServer(s grpc.ServiceRegistrar, srv UserPrettyDisplayIDAdminServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedUserPrettyDisplayIDAdminServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedUserPrettyDisplayIDAdminServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -609,6 +609,45 @@ message ResourceGroup {
|
||||
int64 updated_at_ms = 12;
|
||||
}
|
||||
|
||||
// ResourceGroupSnapshot 是 owner 在业务发布时钉住的不可变奖励承诺。后续资源组编辑、停用
|
||||
// 或资源展示信息变化都不会改变 snapshot_id 对应的实际发放内容和 App 展示内容。
|
||||
message ResourceGroupSnapshot {
|
||||
string app_code = 1;
|
||||
string snapshot_id = 2;
|
||||
string pin_key = 3;
|
||||
int64 source_group_id = 4;
|
||||
int64 version_no = 5;
|
||||
string snapshot_hash = 6;
|
||||
ResourceGroup group = 7;
|
||||
int64 created_by_user_id = 8;
|
||||
int64 created_at_ms = 9;
|
||||
int64 source_group_updated_at_ms = 10;
|
||||
// source_content_hash 覆盖资源组、组项和嵌套资源的完整规范化内容;成员资源独立编辑也会改变该值。
|
||||
string source_content_hash = 11;
|
||||
}
|
||||
|
||||
message PinResourceGroupSnapshotRequest {
|
||||
string request_id = 1;
|
||||
string app_code = 2;
|
||||
int64 group_id = 3;
|
||||
// pin_key 由发布方按业务版本生成;重试同一 key 必须返回同一快照。
|
||||
string pin_key = 4;
|
||||
int64 operator_user_id = 5;
|
||||
// wallet 锁住来源组后必须再次匹配,避免 Get->Pin 间编辑竞态固化错误版本。
|
||||
int64 expected_group_updated_at_ms = 6;
|
||||
// GetResourceGroup 返回的 owner 内容版本。wallet 在事务内锁住组、组项和资源后重新计算并匹配。
|
||||
string expected_source_content_hash = 7;
|
||||
// required_region_ids 是发布方要求奖励可发送的可信 active region 集合;wallet 对 gift resource
|
||||
// 按 pin-time gift config 区域并集逐区 fail-close。空集合仅供不带区域语义的旧调用。
|
||||
repeated int64 required_region_ids = 8;
|
||||
// required_all_regions=true 时必须存在 region_id=0 的 pin-time gift config,确保发布后新增区域也可用。
|
||||
bool required_all_regions = 9;
|
||||
}
|
||||
|
||||
message PinResourceGroupSnapshotResponse {
|
||||
ResourceGroupSnapshot snapshot = 1;
|
||||
}
|
||||
|
||||
message GiftConfig {
|
||||
string app_code = 1;
|
||||
string gift_id = 2;
|
||||
@ -682,6 +721,11 @@ message UserResourceEntitlement {
|
||||
int64 created_at_ms = 12;
|
||||
int64 updated_at_ms = 13;
|
||||
bool equipped = 14;
|
||||
string source_snapshot_id = 15;
|
||||
string resource_snapshot_hash = 16;
|
||||
// pinned_gift_configs 仅在 source_snapshot_id 非空且资源类型为 gift 时返回。内容已由 wallet
|
||||
// 校验 hash/resource/app 绑定,App 背包必须优先使用它,legacy entitlement 才回退当前目录。
|
||||
repeated GiftConfig pinned_gift_configs = 17;
|
||||
}
|
||||
|
||||
message ResourceGrantItem {
|
||||
@ -857,6 +901,8 @@ message GetResourceGroupRequest {
|
||||
|
||||
message GetResourceGroupResponse {
|
||||
ResourceGroup group = 1;
|
||||
// 对 group、ordered items 和嵌套 resource 的规范化 SHA-256。
|
||||
string source_content_hash = 2;
|
||||
}
|
||||
|
||||
message CreateResourceGroupRequest {
|
||||
@ -1042,6 +1088,18 @@ message GrantResourceGroupRequest {
|
||||
string grant_source = 7;
|
||||
}
|
||||
|
||||
message GrantPinnedResourceGroupRequest {
|
||||
string command_id = 1;
|
||||
string app_code = 2;
|
||||
int64 target_user_id = 3;
|
||||
string snapshot_id = 4;
|
||||
string reason = 5;
|
||||
int64 operator_user_id = 6;
|
||||
string grant_source = 7;
|
||||
// 发布版本持有的快照哈希;wallet 必须与 snapshot_id 对应的 owner 快照精确匹配。
|
||||
string expected_snapshot_hash = 8;
|
||||
}
|
||||
|
||||
message RevokeResourceGrantRequest {
|
||||
string request_id = 1;
|
||||
string app_code = 2;
|
||||
@ -2929,6 +2987,7 @@ service WalletService {
|
||||
rpc CreateResourceGroup(CreateResourceGroupRequest) returns (ResourceGroupResponse);
|
||||
rpc UpdateResourceGroup(UpdateResourceGroupRequest) returns (ResourceGroupResponse);
|
||||
rpc SetResourceGroupStatus(SetResourceGroupStatusRequest) returns (ResourceGroupResponse);
|
||||
rpc PinResourceGroupSnapshot(PinResourceGroupSnapshotRequest) returns (PinResourceGroupSnapshotResponse);
|
||||
rpc ListGiftConfigs(ListGiftConfigsRequest) returns (ListGiftConfigsResponse);
|
||||
rpc ListGiftTypeConfigs(ListGiftTypeConfigsRequest) returns (ListGiftTypeConfigsResponse);
|
||||
rpc GetGiftCatalogVersion(GetGiftCatalogVersionRequest) returns (GetGiftCatalogVersionResponse);
|
||||
@ -2940,6 +2999,7 @@ service WalletService {
|
||||
rpc UpsertGiftTypeConfig(UpsertGiftTypeConfigRequest) returns (GiftTypeConfigResponse);
|
||||
rpc GrantResource(GrantResourceRequest) returns (ResourceGrantResponse);
|
||||
rpc GrantResourceGroup(GrantResourceGroupRequest) returns (ResourceGrantResponse);
|
||||
rpc GrantPinnedResourceGroup(GrantPinnedResourceGroupRequest) returns (ResourceGrantResponse);
|
||||
rpc RevokeResourceGrant(RevokeResourceGrantRequest) returns (ResourceGrantResponse);
|
||||
rpc ListUserResources(ListUserResourcesRequest) returns (ListUserResourcesResponse);
|
||||
rpc EquipUserResource(EquipUserResourceRequest) returns (EquipUserResourceResponse);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v7.35.0
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v5.29.2
|
||||
// source: proto/wallet/v1/wallet.proto
|
||||
|
||||
package walletv1
|
||||
@ -106,16 +106,16 @@ type WalletCronServiceServer interface {
|
||||
type UnimplementedWalletCronServiceServer struct{}
|
||||
|
||||
func (UnimplementedWalletCronServiceServer) ProcessHostSalaryDailySettlementBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ProcessHostSalaryDailySettlementBatch not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ProcessHostSalaryDailySettlementBatch not implemented")
|
||||
}
|
||||
func (UnimplementedWalletCronServiceServer) ProcessHostSalaryHalfMonthSettlementBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ProcessHostSalaryHalfMonthSettlementBatch not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ProcessHostSalaryHalfMonthSettlementBatch not implemented")
|
||||
}
|
||||
func (UnimplementedWalletCronServiceServer) ProcessHostSalaryMonthEndBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ProcessHostSalaryMonthEndBatch not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ProcessHostSalaryMonthEndBatch not implemented")
|
||||
}
|
||||
func (UnimplementedWalletCronServiceServer) ProcessVipDailyCoinRebateBatch(context.Context, *ProcessVipDailyCoinRebateBatchRequest) (*ProcessVipDailyCoinRebateBatchResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ProcessVipDailyCoinRebateBatch not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ProcessVipDailyCoinRebateBatch not implemented")
|
||||
}
|
||||
func (UnimplementedWalletCronServiceServer) mustEmbedUnimplementedWalletCronServiceServer() {}
|
||||
func (UnimplementedWalletCronServiceServer) testEmbeddedByValue() {}
|
||||
@ -128,7 +128,7 @@ type UnsafeWalletCronServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterWalletCronServiceServer(s grpc.ServiceRegistrar, srv WalletCronServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedWalletCronServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedWalletCronServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -278,6 +278,7 @@ const (
|
||||
WalletService_CreateResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateResourceGroup"
|
||||
WalletService_UpdateResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateResourceGroup"
|
||||
WalletService_SetResourceGroupStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetResourceGroupStatus"
|
||||
WalletService_PinResourceGroupSnapshot_FullMethodName = "/hyapp.wallet.v1.WalletService/PinResourceGroupSnapshot"
|
||||
WalletService_ListGiftConfigs_FullMethodName = "/hyapp.wallet.v1.WalletService/ListGiftConfigs"
|
||||
WalletService_ListGiftTypeConfigs_FullMethodName = "/hyapp.wallet.v1.WalletService/ListGiftTypeConfigs"
|
||||
WalletService_GetGiftCatalogVersion_FullMethodName = "/hyapp.wallet.v1.WalletService/GetGiftCatalogVersion"
|
||||
@ -289,6 +290,7 @@ const (
|
||||
WalletService_UpsertGiftTypeConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/UpsertGiftTypeConfig"
|
||||
WalletService_GrantResource_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantResource"
|
||||
WalletService_GrantResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantResourceGroup"
|
||||
WalletService_GrantPinnedResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantPinnedResourceGroup"
|
||||
WalletService_RevokeResourceGrant_FullMethodName = "/hyapp.wallet.v1.WalletService/RevokeResourceGrant"
|
||||
WalletService_ListUserResources_FullMethodName = "/hyapp.wallet.v1.WalletService/ListUserResources"
|
||||
WalletService_EquipUserResource_FullMethodName = "/hyapp.wallet.v1.WalletService/EquipUserResource"
|
||||
@ -411,6 +413,7 @@ type WalletServiceClient interface {
|
||||
CreateResourceGroup(ctx context.Context, in *CreateResourceGroupRequest, opts ...grpc.CallOption) (*ResourceGroupResponse, error)
|
||||
UpdateResourceGroup(ctx context.Context, in *UpdateResourceGroupRequest, opts ...grpc.CallOption) (*ResourceGroupResponse, error)
|
||||
SetResourceGroupStatus(ctx context.Context, in *SetResourceGroupStatusRequest, opts ...grpc.CallOption) (*ResourceGroupResponse, error)
|
||||
PinResourceGroupSnapshot(ctx context.Context, in *PinResourceGroupSnapshotRequest, opts ...grpc.CallOption) (*PinResourceGroupSnapshotResponse, error)
|
||||
ListGiftConfigs(ctx context.Context, in *ListGiftConfigsRequest, opts ...grpc.CallOption) (*ListGiftConfigsResponse, error)
|
||||
ListGiftTypeConfigs(ctx context.Context, in *ListGiftTypeConfigsRequest, opts ...grpc.CallOption) (*ListGiftTypeConfigsResponse, error)
|
||||
GetGiftCatalogVersion(ctx context.Context, in *GetGiftCatalogVersionRequest, opts ...grpc.CallOption) (*GetGiftCatalogVersionResponse, error)
|
||||
@ -422,6 +425,7 @@ type WalletServiceClient interface {
|
||||
UpsertGiftTypeConfig(ctx context.Context, in *UpsertGiftTypeConfigRequest, opts ...grpc.CallOption) (*GiftTypeConfigResponse, error)
|
||||
GrantResource(ctx context.Context, in *GrantResourceRequest, opts ...grpc.CallOption) (*ResourceGrantResponse, error)
|
||||
GrantResourceGroup(ctx context.Context, in *GrantResourceGroupRequest, opts ...grpc.CallOption) (*ResourceGrantResponse, error)
|
||||
GrantPinnedResourceGroup(ctx context.Context, in *GrantPinnedResourceGroupRequest, opts ...grpc.CallOption) (*ResourceGrantResponse, error)
|
||||
RevokeResourceGrant(ctx context.Context, in *RevokeResourceGrantRequest, opts ...grpc.CallOption) (*ResourceGrantResponse, error)
|
||||
ListUserResources(ctx context.Context, in *ListUserResourcesRequest, opts ...grpc.CallOption) (*ListUserResourcesResponse, error)
|
||||
EquipUserResource(ctx context.Context, in *EquipUserResourceRequest, opts ...grpc.CallOption) (*EquipUserResourceResponse, error)
|
||||
@ -896,6 +900,16 @@ func (c *walletServiceClient) SetResourceGroupStatus(ctx context.Context, in *Se
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) PinResourceGroupSnapshot(ctx context.Context, in *PinResourceGroupSnapshotRequest, opts ...grpc.CallOption) (*PinResourceGroupSnapshotResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(PinResourceGroupSnapshotResponse)
|
||||
err := c.cc.Invoke(ctx, WalletService_PinResourceGroupSnapshot_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) ListGiftConfigs(ctx context.Context, in *ListGiftConfigsRequest, opts ...grpc.CallOption) (*ListGiftConfigsResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListGiftConfigsResponse)
|
||||
@ -1006,6 +1020,16 @@ func (c *walletServiceClient) GrantResourceGroup(ctx context.Context, in *GrantR
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) GrantPinnedResourceGroup(ctx context.Context, in *GrantPinnedResourceGroupRequest, opts ...grpc.CallOption) (*ResourceGrantResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ResourceGrantResponse)
|
||||
err := c.cc.Invoke(ctx, WalletService_GrantPinnedResourceGroup_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) RevokeResourceGrant(ctx context.Context, in *RevokeResourceGrantRequest, opts ...grpc.CallOption) (*ResourceGrantResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ResourceGrantResponse)
|
||||
@ -1792,6 +1816,7 @@ type WalletServiceServer interface {
|
||||
CreateResourceGroup(context.Context, *CreateResourceGroupRequest) (*ResourceGroupResponse, error)
|
||||
UpdateResourceGroup(context.Context, *UpdateResourceGroupRequest) (*ResourceGroupResponse, error)
|
||||
SetResourceGroupStatus(context.Context, *SetResourceGroupStatusRequest) (*ResourceGroupResponse, error)
|
||||
PinResourceGroupSnapshot(context.Context, *PinResourceGroupSnapshotRequest) (*PinResourceGroupSnapshotResponse, error)
|
||||
ListGiftConfigs(context.Context, *ListGiftConfigsRequest) (*ListGiftConfigsResponse, error)
|
||||
ListGiftTypeConfigs(context.Context, *ListGiftTypeConfigsRequest) (*ListGiftTypeConfigsResponse, error)
|
||||
GetGiftCatalogVersion(context.Context, *GetGiftCatalogVersionRequest) (*GetGiftCatalogVersionResponse, error)
|
||||
@ -1803,6 +1828,7 @@ type WalletServiceServer interface {
|
||||
UpsertGiftTypeConfig(context.Context, *UpsertGiftTypeConfigRequest) (*GiftTypeConfigResponse, error)
|
||||
GrantResource(context.Context, *GrantResourceRequest) (*ResourceGrantResponse, error)
|
||||
GrantResourceGroup(context.Context, *GrantResourceGroupRequest) (*ResourceGrantResponse, error)
|
||||
GrantPinnedResourceGroup(context.Context, *GrantPinnedResourceGroupRequest) (*ResourceGrantResponse, error)
|
||||
RevokeResourceGrant(context.Context, *RevokeResourceGrantRequest) (*ResourceGrantResponse, error)
|
||||
ListUserResources(context.Context, *ListUserResourcesRequest) (*ListUserResourcesResponse, error)
|
||||
EquipUserResource(context.Context, *EquipUserResourceRequest) (*EquipUserResourceResponse, error)
|
||||
@ -1888,376 +1914,382 @@ type WalletServiceServer interface {
|
||||
type UnimplementedWalletServiceServer struct{}
|
||||
|
||||
func (UnimplementedWalletServiceServer) DebitGift(context.Context, *DebitGiftRequest) (*DebitGiftResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DebitGift not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DebitGift not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) DebitDirectGift(context.Context, *DebitDirectGiftRequest) (*DebitGiftResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DebitDirectGift not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DebitDirectGift not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) BatchDebitGift(context.Context, *BatchDebitGiftRequest) (*BatchDebitGiftResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BatchDebitGift not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method BatchDebitGift not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) DebitRobotGift(context.Context, *DebitRobotGiftRequest) (*DebitGiftResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DebitRobotGift not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DebitRobotGift not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetBalances(context.Context, *GetBalancesRequest) (*GetBalancesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetBalances not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetBalances not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetActiveHostSalaryPolicy(context.Context, *GetActiveHostSalaryPolicyRequest) (*GetActiveHostSalaryPolicyResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetActiveHostSalaryPolicy not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetActiveHostSalaryPolicy not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetHostSalaryProgress(context.Context, *GetHostSalaryProgressRequest) (*GetHostSalaryProgressResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetHostSalaryProgress not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetHostSalaryProgress not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetHostRevenueStats(context.Context, *GetHostRevenueStatsRequest) (*GetHostRevenueStatsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetHostRevenueStats not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetHostRevenueStats not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetAgencyPointShareStats(context.Context, *GetAgencyPointShareStatsRequest) (*GetAgencyPointShareStatsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetAgencyPointShareStats not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetAgencyPointShareStats not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetAgencyHostGiftStats(context.Context, *GetAgencyHostGiftStatsRequest) (*GetAgencyHostGiftStatsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetAgencyHostGiftStats not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetAgencyHostGiftStats not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetTeamHostSalaryStats(context.Context, *GetTeamHostSalaryStatsRequest) (*GetTeamHostSalaryStatsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetTeamHostSalaryStats not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetTeamHostSalaryStats not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) AdminCreditAsset(context.Context, *AdminCreditAssetRequest) (*AdminCreditAssetResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminCreditAsset not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminCreditAsset not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) AdminCreditCoinSellerStock(context.Context, *AdminCreditCoinSellerStockRequest) (*AdminCreditCoinSellerStockResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminCreditCoinSellerStock not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminCreditCoinSellerStock not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) TransferCoinFromSeller(context.Context, *TransferCoinFromSellerRequest) (*TransferCoinFromSellerResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method TransferCoinFromSeller not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method TransferCoinFromSeller not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) TransferCoinSellerStockToChild(context.Context, *TransferCoinSellerStockToChildRequest) (*TransferCoinSellerStockToChildResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method TransferCoinSellerStockToChild not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method TransferCoinSellerStockToChild not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListCoinSellerSalaryExchangeRateTiers(context.Context, *ListCoinSellerSalaryExchangeRateTiersRequest) (*ListCoinSellerSalaryExchangeRateTiersResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListCoinSellerSalaryExchangeRateTiers not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListCoinSellerSalaryExchangeRateTiers not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ExchangeSalaryToCoin(context.Context, *ExchangeSalaryToCoinRequest) (*ExchangeSalaryToCoinResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ExchangeSalaryToCoin not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ExchangeSalaryToCoin not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) TransferSalaryToCoinSeller(context.Context, *TransferSalaryToCoinSellerRequest) (*TransferSalaryToCoinSellerResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method TransferSalaryToCoinSeller not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method TransferSalaryToCoinSeller not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListPointWithdrawalCoinSellers(context.Context, *ListPointWithdrawalCoinSellersRequest) (*ListPointWithdrawalCoinSellersResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListPointWithdrawalCoinSellers not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListPointWithdrawalCoinSellers not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) TransferPointToCoinSeller(context.Context, *TransferPointToCoinSellerRequest) (*TransferPointToCoinSellerResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method TransferPointToCoinSeller not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method TransferPointToCoinSeller not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetPointWithdrawalConfig(context.Context, *GetPointWithdrawalConfigRequest) (*GetPointWithdrawalConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetPointWithdrawalConfig not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetPointWithdrawalConfig not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) FreezeSalaryWithdrawal(context.Context, *FreezeSalaryWithdrawalRequest) (*FreezeSalaryWithdrawalResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method FreezeSalaryWithdrawal not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method FreezeSalaryWithdrawal not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) SettleSalaryWithdrawal(context.Context, *SettleSalaryWithdrawalRequest) (*SettleSalaryWithdrawalResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SettleSalaryWithdrawal not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SettleSalaryWithdrawal not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ReleaseSalaryWithdrawal(context.Context, *ReleaseSalaryWithdrawalRequest) (*ReleaseSalaryWithdrawalResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ReleaseSalaryWithdrawal not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ReleaseSalaryWithdrawal not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) FreezePointWithdrawal(context.Context, *FreezePointWithdrawalRequest) (*FreezePointWithdrawalResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method FreezePointWithdrawal not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method FreezePointWithdrawal not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) SettlePointWithdrawal(context.Context, *SettlePointWithdrawalRequest) (*SettlePointWithdrawalResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SettlePointWithdrawal not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SettlePointWithdrawal not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ReleasePointWithdrawal(context.Context, *ReleasePointWithdrawalRequest) (*ReleasePointWithdrawalResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ReleasePointWithdrawal not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ReleasePointWithdrawal not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListResources(context.Context, *ListResourcesRequest) (*ListResourcesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListResources not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListResources not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetResource(context.Context, *GetResourceRequest) (*GetResourceResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetResource not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetResource not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) CreateResource(context.Context, *CreateResourceRequest) (*ResourceResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateResource not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateResource not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) UpdateResource(context.Context, *UpdateResourceRequest) (*ResourceResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateResource not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateResource not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) SetResourceStatus(context.Context, *SetResourceStatusRequest) (*ResourceResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetResourceStatus not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetResourceStatus not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) DeleteResource(context.Context, *DeleteResourceRequest) (*ResourceResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteResource not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteResource not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) BatchDeleteResources(context.Context, *BatchDeleteResourcesRequest) (*BatchDeleteResourcesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BatchDeleteResources not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method BatchDeleteResources not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListResourceGroups(context.Context, *ListResourceGroupsRequest) (*ListResourceGroupsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListResourceGroups not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListResourceGroups not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetResourceGroup(context.Context, *GetResourceGroupRequest) (*GetResourceGroupResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetResourceGroup not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetResourceGroup not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) CreateResourceGroup(context.Context, *CreateResourceGroupRequest) (*ResourceGroupResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateResourceGroup not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateResourceGroup not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) UpdateResourceGroup(context.Context, *UpdateResourceGroupRequest) (*ResourceGroupResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateResourceGroup not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateResourceGroup not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) SetResourceGroupStatus(context.Context, *SetResourceGroupStatusRequest) (*ResourceGroupResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetResourceGroupStatus not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetResourceGroupStatus not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) PinResourceGroupSnapshot(context.Context, *PinResourceGroupSnapshotRequest) (*PinResourceGroupSnapshotResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method PinResourceGroupSnapshot not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListGiftConfigs(context.Context, *ListGiftConfigsRequest) (*ListGiftConfigsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListGiftConfigs not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListGiftConfigs not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListGiftTypeConfigs(context.Context, *ListGiftTypeConfigsRequest) (*ListGiftTypeConfigsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListGiftTypeConfigs not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListGiftTypeConfigs not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetGiftCatalogVersion(context.Context, *GetGiftCatalogVersionRequest) (*GetGiftCatalogVersionResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetGiftCatalogVersion not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetGiftCatalogVersion not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) CreateGiftConfig(context.Context, *CreateGiftConfigRequest) (*GiftConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateGiftConfig not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateGiftConfig not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) BatchCreateGiftConfigs(context.Context, *BatchCreateGiftConfigsRequest) (*BatchCreateGiftConfigsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BatchCreateGiftConfigs not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method BatchCreateGiftConfigs not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) UpdateGiftConfig(context.Context, *UpdateGiftConfigRequest) (*GiftConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateGiftConfig not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateGiftConfig not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) SetGiftConfigStatus(context.Context, *SetGiftConfigStatusRequest) (*GiftConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetGiftConfigStatus not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetGiftConfigStatus not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) DeleteGiftConfig(context.Context, *DeleteGiftConfigRequest) (*GiftConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteGiftConfig not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteGiftConfig not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) UpsertGiftTypeConfig(context.Context, *UpsertGiftTypeConfigRequest) (*GiftTypeConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpsertGiftTypeConfig not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpsertGiftTypeConfig not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GrantResource(context.Context, *GrantResourceRequest) (*ResourceGrantResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GrantResource not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GrantResource not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GrantResourceGroup(context.Context, *GrantResourceGroupRequest) (*ResourceGrantResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GrantResourceGroup not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GrantResourceGroup not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GrantPinnedResourceGroup(context.Context, *GrantPinnedResourceGroupRequest) (*ResourceGrantResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GrantPinnedResourceGroup not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) RevokeResourceGrant(context.Context, *RevokeResourceGrantRequest) (*ResourceGrantResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RevokeResourceGrant not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RevokeResourceGrant not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListUserResources(context.Context, *ListUserResourcesRequest) (*ListUserResourcesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListUserResources not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListUserResources not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) EquipUserResource(context.Context, *EquipUserResourceRequest) (*EquipUserResourceResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method EquipUserResource not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method EquipUserResource not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) UnequipUserResource(context.Context, *UnequipUserResourceRequest) (*UnequipUserResourceResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UnequipUserResource not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UnequipUserResource not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) BatchGetUserEquippedResources(context.Context, *BatchGetUserEquippedResourcesRequest) (*BatchGetUserEquippedResourcesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BatchGetUserEquippedResources not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method BatchGetUserEquippedResources not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListResourceGrants(context.Context, *ListResourceGrantsRequest) (*ListResourceGrantsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListResourceGrants not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListResourceGrants not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListResourceShopItems(context.Context, *ListResourceShopItemsRequest) (*ListResourceShopItemsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListResourceShopItems not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListResourceShopItems not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) UpsertResourceShopItems(context.Context, *UpsertResourceShopItemsRequest) (*UpsertResourceShopItemsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpsertResourceShopItems not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpsertResourceShopItems not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) SetResourceShopItemStatus(context.Context, *SetResourceShopItemStatusRequest) (*ResourceShopItemResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetResourceShopItemStatus not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetResourceShopItemStatus not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListResourceShopPurchaseOrders(context.Context, *ListResourceShopPurchaseOrdersRequest) (*ListResourceShopPurchaseOrdersResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListResourceShopPurchaseOrders not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListResourceShopPurchaseOrders not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) PurchaseResourceShopItem(context.Context, *PurchaseResourceShopItemRequest) (*PurchaseResourceShopItemResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method PurchaseResourceShopItem not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method PurchaseResourceShopItem not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListRechargeBills(context.Context, *ListRechargeBillsRequest) (*ListRechargeBillsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRechargeBills not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRechargeBills not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetRechargeBillSummary(context.Context, *GetRechargeBillSummaryRequest) (*GetRechargeBillSummaryResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetRechargeBillSummary not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetRechargeBillSummary not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) BatchGetUserRechargeStats(context.Context, *BatchGetUserRechargeStatsRequest) (*BatchGetUserRechargeStatsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BatchGetUserRechargeStats not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method BatchGetUserRechargeStats not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetRechargeBillOverview(context.Context, *GetRechargeBillOverviewRequest) (*GetRechargeBillOverviewResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetRechargeBillOverview not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetRechargeBillOverview not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) RefreshGooglePaymentPrices(context.Context, *RefreshGooglePaymentPricesRequest) (*RefreshGooglePaymentPricesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RefreshGooglePaymentPrices not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RefreshGooglePaymentPrices not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetWalletOverview(context.Context, *GetWalletOverviewRequest) (*GetWalletOverviewResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetWalletOverview not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetWalletOverview not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetWalletValueSummary(context.Context, *GetWalletValueSummaryRequest) (*GetWalletValueSummaryResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetWalletValueSummary not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetWalletValueSummary not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetUserGiftWall(context.Context, *GetUserGiftWallRequest) (*GetUserGiftWallResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetUserGiftWall not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetUserGiftWall not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListRechargeProducts(context.Context, *ListRechargeProductsRequest) (*ListRechargeProductsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRechargeProducts not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRechargeProducts not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ConfirmGooglePayment(context.Context, *ConfirmGooglePaymentRequest) (*ConfirmGooglePaymentResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ConfirmGooglePayment not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ConfirmGooglePayment not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListAdminRechargeProducts(context.Context, *ListAdminRechargeProductsRequest) (*ListAdminRechargeProductsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListAdminRechargeProducts not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListAdminRechargeProducts not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) CreateRechargeProduct(context.Context, *CreateRechargeProductRequest) (*RechargeProductResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateRechargeProduct not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateRechargeProduct not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) UpdateRechargeProduct(context.Context, *UpdateRechargeProductRequest) (*RechargeProductResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateRechargeProduct not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateRechargeProduct not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) DeleteRechargeProduct(context.Context, *DeleteRechargeProductRequest) (*DeleteRechargeProductResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteRechargeProduct not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteRechargeProduct not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListThirdPartyPaymentChannels(context.Context, *ListThirdPartyPaymentChannelsRequest) (*ListThirdPartyPaymentChannelsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListThirdPartyPaymentChannels not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListThirdPartyPaymentChannels not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) SetThirdPartyPaymentMethodStatus(context.Context, *SetThirdPartyPaymentMethodStatusRequest) (*ThirdPartyPaymentMethodResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetThirdPartyPaymentMethodStatus not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetThirdPartyPaymentMethodStatus not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) UpdateThirdPartyPaymentRate(context.Context, *UpdateThirdPartyPaymentRateRequest) (*ThirdPartyPaymentMethodResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateThirdPartyPaymentRate not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateThirdPartyPaymentRate not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) SyncThirdPartyPaymentMethods(context.Context, *SyncThirdPartyPaymentMethodsRequest) (*SyncThirdPartyPaymentMethodsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SyncThirdPartyPaymentMethods not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SyncThirdPartyPaymentMethods not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListH5RechargeOptions(context.Context, *H5RechargeOptionsRequest) (*H5RechargeOptionsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListH5RechargeOptions not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListH5RechargeOptions not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) CreateH5RechargeOrder(context.Context, *CreateH5RechargeOrderRequest) (*H5RechargeOrderResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateH5RechargeOrder not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateH5RechargeOrder not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) CreateTemporaryRechargeOrder(context.Context, *CreateTemporaryRechargeOrderRequest) (*H5RechargeOrderResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateTemporaryRechargeOrder not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateTemporaryRechargeOrder not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetTemporaryRechargeOrder(context.Context, *GetTemporaryRechargeOrderRequest) (*H5RechargeOrderResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetTemporaryRechargeOrder not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetTemporaryRechargeOrder not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListTemporaryRechargeOrders(context.Context, *ListTemporaryRechargeOrdersRequest) (*ListTemporaryRechargeOrdersResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListTemporaryRechargeOrders not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListTemporaryRechargeOrders not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) SubmitH5RechargeTx(context.Context, *SubmitH5RechargeTxRequest) (*H5RechargeOrderResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SubmitH5RechargeTx not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SubmitH5RechargeTx not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetH5RechargeOrder(context.Context, *GetH5RechargeOrderRequest) (*H5RechargeOrderResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetH5RechargeOrder not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetH5RechargeOrder not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) VerifyCoinSellerRechargeReceipt(context.Context, *VerifyCoinSellerRechargeReceiptRequest) (*VerifyCoinSellerRechargeReceiptResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VerifyCoinSellerRechargeReceipt not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VerifyCoinSellerRechargeReceipt not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) HandleMifapayNotify(context.Context, *HandleMifapayNotifyRequest) (*HandleMifapayNotifyResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method HandleMifapayNotify not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method HandleMifapayNotify not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) HandleV5PayNotify(context.Context, *HandleV5PayNotifyRequest) (*HandleV5PayNotifyResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method HandleV5PayNotify not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method HandleV5PayNotify not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetDiamondExchangeConfig(context.Context, *GetDiamondExchangeConfigRequest) (*GetDiamondExchangeConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetDiamondExchangeConfig not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetDiamondExchangeConfig not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListWalletTransactions(context.Context, *ListWalletTransactionsRequest) (*ListWalletTransactionsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListWalletTransactions not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListWalletTransactions not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListVipPackages(context.Context, *ListVipPackagesRequest) (*ListVipPackagesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListVipPackages not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListVipPackages not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetVipProgramConfig(context.Context, *GetVipProgramConfigRequest) (*GetVipProgramConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetVipProgramConfig not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetVipProgramConfig not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetMyVip(context.Context, *GetMyVipRequest) (*GetMyVipResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetMyVip not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetMyVip not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) CheckVipBenefit(context.Context, *CheckVipBenefitRequest) (*CheckVipBenefitResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CheckVipBenefit not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CheckVipBenefit not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) PurchaseVip(context.Context, *PurchaseVipRequest) (*PurchaseVipResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method PurchaseVip not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method PurchaseVip not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) DebitCPBreakupFee(context.Context, *DebitCPBreakupFeeRequest) (*DebitCPBreakupFeeResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DebitCPBreakupFee not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DebitCPBreakupFee not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) DebitWheelDraw(context.Context, *DebitWheelDrawRequest) (*DebitWheelDrawResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DebitWheelDraw not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DebitWheelDraw not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GrantVip(context.Context, *GrantVipRequest) (*GrantVipResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GrantVip not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GrantVip not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GrantVipTrialCard(context.Context, *GrantVipTrialCardRequest) (*GrantVipTrialCardResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GrantVipTrialCard not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GrantVipTrialCard not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) EquipVipTrialCard(context.Context, *EquipVipTrialCardRequest) (*EquipVipTrialCardResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method EquipVipTrialCard not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method EquipVipTrialCard not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) UnequipVipTrialCard(context.Context, *UnequipVipTrialCardRequest) (*UnequipVipTrialCardResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UnequipVipTrialCard not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UnequipVipTrialCard not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListAdminVipLevels(context.Context, *ListAdminVipLevelsRequest) (*ListAdminVipLevelsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListAdminVipLevels not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListAdminVipLevels not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) UpdateAdminVipLevels(context.Context, *UpdateAdminVipLevelsRequest) (*UpdateAdminVipLevelsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateAdminVipLevels not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateAdminVipLevels not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) UpdateAdminVipProgramConfig(context.Context, *UpdateAdminVipProgramConfigRequest) (*UpdateAdminVipProgramConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateAdminVipProgramConfig not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateAdminVipProgramConfig not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) UpdateMyVipSettings(context.Context, *UpdateMyVipSettingsRequest) (*UpdateMyVipSettingsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateMyVipSettings not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateMyVipSettings not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetMyCurrentVipDailyCoinRebate(context.Context, *GetMyCurrentVipDailyCoinRebateRequest) (*GetMyCurrentVipDailyCoinRebateResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetMyCurrentVipDailyCoinRebate not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetMyCurrentVipDailyCoinRebate not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListMyVipDailyCoinRebateStatuses(context.Context, *ListMyVipDailyCoinRebateStatusesRequest) (*ListMyVipDailyCoinRebateStatusesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListMyVipDailyCoinRebateStatuses not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListMyVipDailyCoinRebateStatuses not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ClaimVipDailyCoinRebate(context.Context, *ClaimVipDailyCoinRebateRequest) (*ClaimVipDailyCoinRebateResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ClaimVipDailyCoinRebate not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ClaimVipDailyCoinRebate not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) CreditTaskReward(context.Context, *CreditTaskRewardRequest) (*CreditTaskRewardResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreditTaskReward not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreditTaskReward not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) CreditLuckyGiftReward(context.Context, *CreditLuckyGiftRewardRequest) (*CreditLuckyGiftRewardResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreditLuckyGiftReward not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreditLuckyGiftReward not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) CreditWheelReward(context.Context, *CreditWheelRewardRequest) (*CreditWheelRewardResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreditWheelReward not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreditWheelReward not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) CreditRoomTurnoverReward(context.Context, *CreditRoomTurnoverRewardRequest) (*CreditRoomTurnoverRewardResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreditRoomTurnoverReward not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreditRoomTurnoverReward not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) CreditInviteActivityReward(context.Context, *CreditInviteActivityRewardRequest) (*CreditInviteActivityRewardResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreditInviteActivityReward not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreditInviteActivityReward not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) CreditAgencyOpeningReward(context.Context, *CreditAgencyOpeningRewardRequest) (*CreditAgencyOpeningRewardResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreditAgencyOpeningReward not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreditAgencyOpeningReward not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ApplyGameCoinChange(context.Context, *ApplyGameCoinChangeRequest) (*ApplyGameCoinChangeResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ApplyGameCoinChange not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ApplyGameCoinChange not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetRedPacketConfig(context.Context, *GetRedPacketConfigRequest) (*GetRedPacketConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetRedPacketConfig not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetRedPacketConfig not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) UpdateRedPacketConfig(context.Context, *UpdateRedPacketConfigRequest) (*UpdateRedPacketConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateRedPacketConfig not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateRedPacketConfig not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) CreateRedPacket(context.Context, *CreateRedPacketRequest) (*CreateRedPacketResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateRedPacket not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateRedPacket not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ClaimRedPacket(context.Context, *ClaimRedPacketRequest) (*ClaimRedPacketResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ClaimRedPacket not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ClaimRedPacket not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListRedPackets(context.Context, *ListRedPacketsRequest) (*ListRedPacketsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRedPackets not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRedPackets not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetRedPacket(context.Context, *GetRedPacketRequest) (*GetRedPacketResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetRedPacket not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetRedPacket not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ExpireRedPackets(context.Context, *ExpireRedPacketsRequest) (*ExpireRedPacketsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ExpireRedPackets not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ExpireRedPackets not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) RetryRedPacketRefund(context.Context, *RetryRedPacketRefundRequest) (*RetryRedPacketRefundResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RetryRedPacketRefund not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RetryRedPacketRefund not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) mustEmbedUnimplementedWalletServiceServer() {}
|
||||
func (UnimplementedWalletServiceServer) testEmbeddedByValue() {}
|
||||
@ -2270,7 +2302,7 @@ type UnsafeWalletServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterWalletServiceServer(s grpc.ServiceRegistrar, srv WalletServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedWalletServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedWalletServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -2982,6 +3014,24 @@ func _WalletService_SetResourceGroupStatus_Handler(srv interface{}, ctx context.
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_PinResourceGroupSnapshot_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(PinResourceGroupSnapshotRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(WalletServiceServer).PinResourceGroupSnapshot(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: WalletService_PinResourceGroupSnapshot_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(WalletServiceServer).PinResourceGroupSnapshot(ctx, req.(*PinResourceGroupSnapshotRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_ListGiftConfigs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListGiftConfigsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -3180,6 +3230,24 @@ func _WalletService_GrantResourceGroup_Handler(srv interface{}, ctx context.Cont
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_GrantPinnedResourceGroup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GrantPinnedResourceGroupRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(WalletServiceServer).GrantPinnedResourceGroup(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: WalletService_GrantPinnedResourceGroup_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(WalletServiceServer).GrantPinnedResourceGroup(ctx, req.(*GrantPinnedResourceGroupRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_RevokeResourceGrant_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RevokeResourceGrantRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -4675,6 +4743,10 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "SetResourceGroupStatus",
|
||||
Handler: _WalletService_SetResourceGroupStatus_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "PinResourceGroupSnapshot",
|
||||
Handler: _WalletService_PinResourceGroupSnapshot_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListGiftConfigs",
|
||||
Handler: _WalletService_ListGiftConfigs_Handler,
|
||||
@ -4719,6 +4791,10 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "GrantResourceGroup",
|
||||
Handler: _WalletService_GrantResourceGroup_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GrantPinnedResourceGroup",
|
||||
Handler: _WalletService_GrantPinnedResourceGroup_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "RevokeResourceGrant",
|
||||
Handler: _WalletService_RevokeResourceGrant_Handler,
|
||||
|
||||
@ -99,8 +99,6 @@ services:
|
||||
container_name: wallet-service
|
||||
environment:
|
||||
TZ: UTC
|
||||
volumes:
|
||||
- ./.env:/app/.env:ro
|
||||
ports:
|
||||
- "13004:13004"
|
||||
- "13104:13104"
|
||||
|
||||
181
pkg/activitymq/messages.go
Normal file
181
pkg/activitymq/messages.go
Normal file
@ -0,0 +1,181 @@
|
||||
// Package activitymq defines the RocketMQ fact contract owned by activity-service.
|
||||
package activitymq
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
MessageTypeActivityTemplateFact = "activity_template_fact"
|
||||
TagActivityTemplateFact = MessageTypeActivityTemplateFact
|
||||
|
||||
EventTypeGiftScored = "GiftScored"
|
||||
EventTypeTaskProgressed = "TaskProgressed"
|
||||
EventTypeTaskClaimed = "TaskClaimed"
|
||||
EventTypeActivityVisited = "ActivityVisited"
|
||||
)
|
||||
|
||||
// ActivityTemplateFactMessage is a self-contained immutable activity fact. Every
|
||||
// dimension needed by statistics is carried in the message so consumers never
|
||||
// need to read activity-service owner tables.
|
||||
type ActivityTemplateFactMessage struct {
|
||||
MessageType string `json:"message_type"`
|
||||
AppCode string `json:"app_code"`
|
||||
EventID string `json:"event_id"`
|
||||
EventType string `json:"event_type"`
|
||||
TemplateID string `json:"template_id"`
|
||||
TemplateCode string `json:"template_code"`
|
||||
PublishedVersion int64 `json:"published_version"`
|
||||
StatDay string `json:"stat_day"`
|
||||
RegionID int64 `json:"region_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
GiftID string `json:"gift_id"`
|
||||
TaskKey string `json:"task_key"`
|
||||
TaskType string `json:"task_type"`
|
||||
GiftCountDelta int64 `json:"gift_count_delta"`
|
||||
CoinAmountDelta int64 `json:"coin_amount_delta"`
|
||||
ProgressDelta int64 `json:"progress_delta"`
|
||||
CompletedDelta int64 `json:"completed_delta"`
|
||||
ClaimedDelta int64 `json:"claimed_delta"`
|
||||
VisitCountDelta int64 `json:"visit_count_delta"`
|
||||
OccurredAtMS int64 `json:"occurred_at_ms"`
|
||||
AttributionAtMS int64 `json:"attribution_at_ms"`
|
||||
}
|
||||
|
||||
// EventTypeTag accepts only committed owner facts. Keeping the allow-list here
|
||||
// prevents a malformed database value from becoming a broker routing expression.
|
||||
func EventTypeTag(eventType string) (string, error) {
|
||||
switch tag := strings.TrimSpace(eventType); tag {
|
||||
case EventTypeGiftScored, EventTypeTaskProgressed, EventTypeTaskClaimed, EventTypeActivityVisited:
|
||||
return tag, nil
|
||||
case "":
|
||||
return "", errors.New("activity event_type tag is required")
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported activity event_type tag %q", eventType)
|
||||
}
|
||||
}
|
||||
|
||||
// LegacyCompatibleTagExpression keeps messages published before typed tags were
|
||||
// enabled consumable while making all new deliveries broker-filtered and explicit.
|
||||
func LegacyCompatibleTagExpression(eventTypes ...string) (string, error) {
|
||||
tags := make([]string, 0, len(eventTypes))
|
||||
seen := make(map[string]struct{}, len(eventTypes))
|
||||
for _, eventType := range eventTypes {
|
||||
tag, err := EventTypeTag(eventType)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, exists := seen[tag]; exists {
|
||||
continue
|
||||
}
|
||||
seen[tag] = struct{}{}
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
if len(tags) == 0 {
|
||||
return "", errors.New("at least one typed activity event tag is required")
|
||||
}
|
||||
sort.Strings(tags)
|
||||
return TagActivityTemplateFact + " || " + strings.Join(tags, " || "), nil
|
||||
}
|
||||
|
||||
// EncodeActivityTemplateFactMessage normalizes the wrapper discriminator and
|
||||
// rejects poison facts before they can leave the transactional outbox relay.
|
||||
func EncodeActivityTemplateFactMessage(message ActivityTemplateFactMessage) ([]byte, error) {
|
||||
message.MessageType = MessageTypeActivityTemplateFact
|
||||
message = normalizeActivityTemplateFact(message)
|
||||
if err := validateActivityTemplateFact(message); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(message)
|
||||
}
|
||||
|
||||
// DecodeActivityTemplateFactMessage applies the same validation at every ingest
|
||||
// boundary, including replay tooling that may bypass the original producer.
|
||||
func DecodeActivityTemplateFactMessage(body []byte) (ActivityTemplateFactMessage, error) {
|
||||
var message ActivityTemplateFactMessage
|
||||
if err := json.Unmarshal(body, &message); err != nil {
|
||||
return ActivityTemplateFactMessage{}, err
|
||||
}
|
||||
if message.MessageType != MessageTypeActivityTemplateFact {
|
||||
return ActivityTemplateFactMessage{}, errors.New("unexpected activity template fact message_type")
|
||||
}
|
||||
message = normalizeActivityTemplateFact(message)
|
||||
if err := validateActivityTemplateFact(message); err != nil {
|
||||
return ActivityTemplateFactMessage{}, err
|
||||
}
|
||||
return message, nil
|
||||
}
|
||||
|
||||
func normalizeActivityTemplateFact(message ActivityTemplateFactMessage) ActivityTemplateFactMessage {
|
||||
message.AppCode = strings.TrimSpace(message.AppCode)
|
||||
message.EventID = strings.TrimSpace(message.EventID)
|
||||
message.EventType = strings.TrimSpace(message.EventType)
|
||||
message.TemplateID = strings.TrimSpace(message.TemplateID)
|
||||
message.TemplateCode = strings.TrimSpace(message.TemplateCode)
|
||||
message.StatDay = strings.TrimSpace(message.StatDay)
|
||||
message.GiftID = strings.TrimSpace(message.GiftID)
|
||||
message.TaskKey = strings.TrimSpace(message.TaskKey)
|
||||
message.TaskType = strings.TrimSpace(message.TaskType)
|
||||
if message.AttributionAtMS <= 0 {
|
||||
message.AttributionAtMS = message.OccurredAtMS
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
func validateActivityTemplateFact(message ActivityTemplateFactMessage) error {
|
||||
if message.AppCode == "" ||
|
||||
message.EventID == "" ||
|
||||
message.TemplateID == "" ||
|
||||
message.TemplateCode == "" ||
|
||||
message.PublishedVersion <= 0 ||
|
||||
message.RegionID < 0 ||
|
||||
message.UserID <= 0 ||
|
||||
message.OccurredAtMS <= 0 {
|
||||
return errors.New("activity template fact is incomplete")
|
||||
}
|
||||
if _, err := EventTypeTag(message.EventType); err != nil {
|
||||
return err
|
||||
}
|
||||
if message.GiftCountDelta < 0 || message.CoinAmountDelta < 0 ||
|
||||
message.ProgressDelta < 0 || message.CompletedDelta < 0 || message.ClaimedDelta < 0 || message.VisitCountDelta < 0 {
|
||||
return errors.New("activity template fact deltas cannot be negative")
|
||||
}
|
||||
parsedDay, err := time.Parse("2006-01-02", message.StatDay)
|
||||
if err != nil || parsedDay.Format("2006-01-02") != message.StatDay {
|
||||
return errors.New("activity template fact stat_day must be a UTC YYYY-MM-DD date")
|
||||
}
|
||||
// Task claims can be executed days after completion. stat_day remains the task earning day while
|
||||
// occurred_at_ms is the truthful wallet-grant time; attribution_at_ms proves the owner-selected bucket.
|
||||
daySourceMS := message.OccurredAtMS
|
||||
if message.EventType == EventTypeTaskClaimed {
|
||||
daySourceMS = message.AttributionAtMS
|
||||
}
|
||||
if time.UnixMilli(daySourceMS).UTC().Format("2006-01-02") != message.StatDay {
|
||||
return errors.New("activity template fact stat_day does not match attribution time")
|
||||
}
|
||||
switch message.EventType {
|
||||
case EventTypeGiftScored:
|
||||
if message.GiftID == "" || message.GiftCountDelta <= 0 {
|
||||
return errors.New("GiftScored requires gift_id and positive gift_count_delta")
|
||||
}
|
||||
case EventTypeTaskProgressed:
|
||||
if message.TaskKey == "" || message.TaskType == "" ||
|
||||
(message.ProgressDelta == 0 && message.CompletedDelta == 0) {
|
||||
return errors.New("TaskProgressed requires task identity and a progress or completed delta")
|
||||
}
|
||||
case EventTypeTaskClaimed:
|
||||
if message.TaskKey == "" || message.TaskType == "" || message.ClaimedDelta <= 0 {
|
||||
return errors.New("TaskClaimed requires task identity and positive claimed_delta")
|
||||
}
|
||||
case EventTypeActivityVisited:
|
||||
if message.VisitCountDelta <= 0 {
|
||||
return errors.New("ActivityVisited requires positive visit_count_delta")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
104
pkg/activitymq/messages_test.go
Normal file
104
pkg/activitymq/messages_test.go
Normal file
@ -0,0 +1,104 @@
|
||||
package activitymq
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestActivityTemplateFactRoundTrip(t *testing.T) {
|
||||
occurredAt := time.Date(2026, time.July, 14, 8, 30, 0, 0, time.UTC).UnixMilli()
|
||||
want := ActivityTemplateFactMessage{
|
||||
AppCode: "lalu", EventID: "activity:g1", EventType: EventTypeGiftScored,
|
||||
TemplateID: "template-1", TemplateCode: "summer_rank", PublishedVersion: 3,
|
||||
StatDay: "2026-07-14", RegionID: 9, UserID: 1001, GiftID: "rose",
|
||||
GiftCountDelta: 2, CoinAmountDelta: 200, OccurredAtMS: occurredAt,
|
||||
}
|
||||
body, err := EncodeActivityTemplateFactMessage(want)
|
||||
if err != nil {
|
||||
t.Fatalf("encode: %v", err)
|
||||
}
|
||||
got, err := DecodeActivityTemplateFactMessage(body)
|
||||
if err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if got.MessageType != MessageTypeActivityTemplateFact || got.EventID != want.EventID || got.CoinAmountDelta != 200 {
|
||||
t.Fatalf("unexpected round trip: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityTemplateFactRejectsMismatchedDayAndIncompleteDimensions(t *testing.T) {
|
||||
base := ActivityTemplateFactMessage{
|
||||
AppCode: "lalu", EventID: "activity:t1", EventType: EventTypeTaskProgressed,
|
||||
TemplateID: "template-1", TemplateCode: "summer_rank", PublishedVersion: 1,
|
||||
StatDay: "2026-07-13", UserID: 1001, TaskKey: "send_gift", TaskType: "gift",
|
||||
ProgressDelta: 1, OccurredAtMS: time.Date(2026, time.July, 14, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
}
|
||||
if _, err := EncodeActivityTemplateFactMessage(base); err == nil || !strings.Contains(err.Error(), "does not match") {
|
||||
t.Fatalf("expected stat day mismatch, got %v", err)
|
||||
}
|
||||
base.StatDay = "2026-07-14"
|
||||
base.TaskKey = ""
|
||||
if _, err := EncodeActivityTemplateFactMessage(base); err == nil {
|
||||
t.Fatal("expected incomplete task identity rejection")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskClaimedUsesImmutableEarningAttributionDay(t *testing.T) {
|
||||
earningAt := time.Date(2026, time.July, 14, 23, 50, 0, 0, time.UTC).UnixMilli()
|
||||
claimedAt := time.Date(2026, time.July, 17, 9, 15, 0, 0, time.UTC).UnixMilli()
|
||||
want := ActivityTemplateFactMessage{
|
||||
AppCode: "lalu", EventID: "activity:claim:delayed", EventType: EventTypeTaskClaimed,
|
||||
TemplateID: "template-1", TemplateCode: "summer_rank", PublishedVersion: 3,
|
||||
StatDay: "2026-07-14", RegionID: 9, UserID: 1001, TaskKey: "send_gift", TaskType: "gift",
|
||||
ClaimedDelta: 1, OccurredAtMS: claimedAt, AttributionAtMS: earningAt,
|
||||
}
|
||||
body, err := EncodeActivityTemplateFactMessage(want)
|
||||
if err != nil {
|
||||
t.Fatalf("encode delayed task claim: %v", err)
|
||||
}
|
||||
got, err := DecodeActivityTemplateFactMessage(body)
|
||||
if err != nil {
|
||||
t.Fatalf("decode delayed task claim: %v", err)
|
||||
}
|
||||
if got.StatDay != "2026-07-14" || got.OccurredAtMS != claimedAt || got.AttributionAtMS != earningAt {
|
||||
t.Fatalf("task claim attribution changed: %+v", got)
|
||||
}
|
||||
|
||||
// A claim cannot relabel its earning day independently from the persisted
|
||||
// attribution timestamp, otherwise retry time or a moved user region could
|
||||
// rewrite already-earned statistics.
|
||||
want.AttributionAtMS = time.Date(2026, time.July, 15, 0, 0, 0, 0, time.UTC).UnixMilli()
|
||||
if _, err := EncodeActivityTemplateFactMessage(want); err == nil || !strings.Contains(err.Error(), "attribution") {
|
||||
t.Fatalf("expected attribution mismatch rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityVisitedRequiresPositiveVisitDelta(t *testing.T) {
|
||||
visitedAt := time.Date(2026, time.July, 14, 8, 30, 0, 0, time.UTC).UnixMilli()
|
||||
fact := ActivityTemplateFactMessage{
|
||||
AppCode: "lalu", EventID: "activity:visit:1", EventType: EventTypeActivityVisited,
|
||||
TemplateID: "template-1", TemplateCode: "summer_rank", PublishedVersion: 3,
|
||||
StatDay: "2026-07-14", RegionID: 9, UserID: 1001, OccurredAtMS: visitedAt,
|
||||
}
|
||||
if _, err := EncodeActivityTemplateFactMessage(fact); err == nil {
|
||||
t.Fatal("expected zero visit delta rejection")
|
||||
}
|
||||
fact.VisitCountDelta = 1
|
||||
if _, err := EncodeActivityTemplateFactMessage(fact); err != nil {
|
||||
t.Fatalf("encode activity visit: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityTemplateFactTagExpressionIsSafeAndDeterministic(t *testing.T) {
|
||||
expression, err := LegacyCompatibleTagExpression(EventTypeTaskClaimed, EventTypeGiftScored, EventTypeTaskClaimed)
|
||||
if err != nil {
|
||||
t.Fatalf("expression: %v", err)
|
||||
}
|
||||
if expression != "activity_template_fact || GiftScored || TaskClaimed" {
|
||||
t.Fatalf("unexpected expression: %s", expression)
|
||||
}
|
||||
if _, err := EventTypeTag("GiftScored || *"); err == nil {
|
||||
t.Fatal("expected unsafe tag rejection")
|
||||
}
|
||||
}
|
||||
@ -48,6 +48,7 @@ TOPICS=(
|
||||
"hyapp_room_rocket_launch"
|
||||
"hyapp_user_outbox"
|
||||
"hyapp_message_action_outbox"
|
||||
"hyapp_activity_outbox"
|
||||
"hyapp_game_outbox"
|
||||
)
|
||||
|
||||
|
||||
@ -20,6 +20,29 @@ OPTIONAL_SQL_FILES=(
|
||||
|
||||
mkdir -p "${LOCAL_INITDB_DIR}"
|
||||
|
||||
append_current_repo_grants() {
|
||||
local target_file="$1"
|
||||
cat >> "${target_file}" <<'EOF'
|
||||
|
||||
-- Local bootstrap follows the databases owned by this repository. The optional
|
||||
-- deploy-platform grants can lag a newly added service or be absent in a lightweight
|
||||
-- checkout; every current owner must still be able to start with the shared local user.
|
||||
CREATE USER IF NOT EXISTS 'hyapp'@'%' IDENTIFIED BY 'hyapp';
|
||||
GRANT ALL PRIVILEGES ON hyapp_room.* TO 'hyapp'@'%';
|
||||
GRANT ALL PRIVILEGES ON hyapp_user.* TO 'hyapp'@'%';
|
||||
GRANT ALL PRIVILEGES ON hyapp_wallet.* TO 'hyapp'@'%';
|
||||
GRANT ALL PRIVILEGES ON hyapp_activity.* TO 'hyapp'@'%';
|
||||
GRANT ALL PRIVILEGES ON hyapp_lucky_gift.* TO 'hyapp'@'%';
|
||||
GRANT ALL PRIVILEGES ON hyapp_cron.* TO 'hyapp'@'%';
|
||||
GRANT ALL PRIVILEGES ON hyapp_game.* TO 'hyapp'@'%';
|
||||
GRANT ALL PRIVILEGES ON hyapp_robot.* TO 'hyapp'@'%';
|
||||
GRANT ALL PRIVILEGES ON hyapp_notice.* TO 'hyapp'@'%';
|
||||
GRANT ALL PRIVILEGES ON hyapp_statistics.* TO 'hyapp'@'%';
|
||||
GRANT ALL PRIVILEGES ON hyapp_admin.* TO 'hyapp'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
EOF
|
||||
}
|
||||
|
||||
for sql_name in "${OPTIONAL_SQL_FILES[@]}"; do
|
||||
source_file="${DEPLOY_PLATFORM_ROOT}/deploy/mysql/initdb/${sql_name}"
|
||||
target_file="${LOCAL_INITDB_DIR}/${sql_name}"
|
||||
@ -27,24 +50,22 @@ for sql_name in "${OPTIONAL_SQL_FILES[@]}"; do
|
||||
if [[ -f "${source_file}" ]]; then
|
||||
cp "${source_file}" "${target_file}"
|
||||
if [[ "${sql_name}" == "999_local_grants.sql" ]]; then
|
||||
# hyapp-server 新增 owner 库时,deploy-platform 的本地授权文件可能还没同步。
|
||||
# 本地启动必须以当前仓库服务清单为准,否则新服务库已经创建但 hyapp 用户无法连接。
|
||||
cat >> "${target_file}" <<'EOF'
|
||||
GRANT ALL PRIVILEGES ON hyapp_lucky_gift.* TO 'hyapp'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
EOF
|
||||
append_current_repo_grants "${target_file}"
|
||||
fi
|
||||
printf 'prepared optional mysql init file: %s\n' "${source_file}"
|
||||
continue
|
||||
fi
|
||||
|
||||
printf -- '-- optional mysql init file is absent locally: %s\n' "${source_file}" > "${target_file}"
|
||||
if [[ "${sql_name}" == "999_local_grants.sql" ]]; then
|
||||
if [[ "${sql_name}" == "006_admin_database.sql" ]]; then
|
||||
# server/admin owns its migrations but requires the database itself before the
|
||||
# process can connect and run them. Keep the fallback minimal and idempotent.
|
||||
cat >> "${target_file}" <<'EOF'
|
||||
CREATE USER IF NOT EXISTS 'hyapp'@'%' IDENTIFIED BY 'hyapp';
|
||||
GRANT ALL PRIVILEGES ON hyapp_lucky_gift.* TO 'hyapp'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
CREATE DATABASE IF NOT EXISTS hyapp_admin DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
EOF
|
||||
fi
|
||||
if [[ "${sql_name}" == "999_local_grants.sql" ]]; then
|
||||
append_current_repo_grants "${target_file}"
|
||||
fi
|
||||
printf 'prepared empty optional mysql init file: %s\n' "${source_file}"
|
||||
done
|
||||
|
||||
@ -23,11 +23,13 @@ import (
|
||||
"hyapp-admin-server/internal/integration/luckygiftadmin"
|
||||
"hyapp-admin-server/internal/integration/robotclient"
|
||||
"hyapp-admin-server/internal/integration/roomclient"
|
||||
"hyapp-admin-server/internal/integration/statisticsclient"
|
||||
"hyapp-admin-server/internal/integration/userclient"
|
||||
"hyapp-admin-server/internal/integration/walletclient"
|
||||
jobrunner "hyapp-admin-server/internal/job"
|
||||
"hyapp-admin-server/internal/migration"
|
||||
achievementconfigmodule "hyapp-admin-server/internal/modules/achievementconfig"
|
||||
activitytemplatemodule "hyapp-admin-server/internal/modules/activitytemplate"
|
||||
adminusermodule "hyapp-admin-server/internal/modules/adminuser"
|
||||
agencyopeningmodule "hyapp-admin-server/internal/modules/agencyopening"
|
||||
appconfigmodule "hyapp-admin-server/internal/modules/appconfig"
|
||||
@ -351,10 +353,14 @@ func main() {
|
||||
)
|
||||
luckyGiftHandler := luckygiftmodule.New(luckygiftadmin.NewGRPC(luckyGiftConn), cfg.LuckyGiftService.RequestTimeout, auditHandler)
|
||||
handlers := router.Handlers{
|
||||
Audit: auditHandler,
|
||||
Auth: authmodule.New(store, auth, auditHandler, cfg),
|
||||
AdminUser: adminusermodule.New(store, cfg, auditHandler),
|
||||
AgencyOpening: agencyopeningmodule.New(activityclient.NewGRPC(activityConn), auditHandler),
|
||||
Audit: auditHandler,
|
||||
Auth: authmodule.New(store, auth, auditHandler, cfg),
|
||||
AdminUser: adminusermodule.New(store, cfg, auditHandler),
|
||||
AgencyOpening: agencyopeningmodule.New(activityclient.NewGRPC(activityConn), auditHandler),
|
||||
ActivityTemplate: activitytemplatemodule.New(
|
||||
activityclient.NewGRPC(activityConn), auditHandler,
|
||||
statisticsclient.NewHTTP(cfg.StatisticsService.BaseURL, cfg.StatisticsService.RequestTimeout),
|
||||
).BindUserBatchGetter(userclient.NewGRPC(userConn)),
|
||||
AchievementConfig: achievementconfigmodule.New(activityclient.NewGRPC(activityConn), auditHandler),
|
||||
AppConfig: appconfigmodule.New(store, auditHandler),
|
||||
AppRegistry: appregistrymodule.New(userDB, store),
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// Client 是 admin-server 访问 activity-service 任务配置能力的最小 gRPC 依赖。
|
||||
// Client 是 admin-server 访问 activity-service 后台配置能力的 gRPC 边界。
|
||||
type Client interface {
|
||||
ListTaskDefinitions(ctx context.Context, req *activityv1.ListTaskDefinitionsRequest) (*activityv1.ListTaskDefinitionsResponse, error)
|
||||
UpsertTaskDefinition(ctx context.Context, req *activityv1.UpsertTaskDefinitionRequest) (*activityv1.UpsertTaskDefinitionResponse, error)
|
||||
@ -51,6 +51,20 @@ type Client interface {
|
||||
SetAgencyOpeningCycleStatus(ctx context.Context, req *activityv1.SetAgencyOpeningCycleStatusRequest) (*activityv1.SetAgencyOpeningCycleStatusResponse, error)
|
||||
ListAgencyOpeningApplications(ctx context.Context, req *activityv1.ListAgencyOpeningApplicationsRequest) (*activityv1.ListAgencyOpeningApplicationsResponse, error)
|
||||
ReviewAgencyOpeningApplication(ctx context.Context, req *activityv1.ReviewAgencyOpeningApplicationRequest) (*activityv1.ReviewAgencyOpeningApplicationResponse, error)
|
||||
ListActivityTemplates(ctx context.Context, req *activityv1.ListActivityTemplatesRequest) (*activityv1.ListActivityTemplatesResponse, error)
|
||||
CreateActivityTemplate(ctx context.Context, req *activityv1.CreateActivityTemplateRequest) (*activityv1.CreateActivityTemplateResponse, error)
|
||||
GetActivityTemplate(ctx context.Context, req *activityv1.GetActivityTemplateRequest) (*activityv1.GetActivityTemplateResponse, error)
|
||||
UpdateActivityTemplate(ctx context.Context, req *activityv1.UpdateActivityTemplateRequest) (*activityv1.UpdateActivityTemplateResponse, error)
|
||||
SetActivityTemplateStatus(ctx context.Context, req *activityv1.SetActivityTemplateStatusRequest) (*activityv1.SetActivityTemplateStatusResponse, error)
|
||||
DeleteActivityTemplate(ctx context.Context, req *activityv1.DeleteActivityTemplateRequest) (*activityv1.DeleteActivityTemplateResponse, error)
|
||||
ListActivityTemplateVersions(ctx context.Context, req *activityv1.ListActivityTemplateVersionsRequest) (*activityv1.ListActivityTemplateVersionsResponse, error)
|
||||
CloneActivityTemplate(ctx context.Context, req *activityv1.CloneActivityTemplateRequest) (*activityv1.CloneActivityTemplateResponse, error)
|
||||
ValidateActivityTemplate(ctx context.Context, req *activityv1.ValidateActivityTemplateRequest) (*activityv1.ValidateActivityTemplateResponse, error)
|
||||
ListActivityTemplateLeaderboard(ctx context.Context, req *activityv1.AdminListActivityTemplateLeaderboardRequest) (*activityv1.AdminListActivityTemplateLeaderboardResponse, error)
|
||||
ListActivityTemplateRewardJobs(ctx context.Context, req *activityv1.ListActivityTemplateRewardJobsRequest) (*activityv1.ListActivityTemplateRewardJobsResponse, error)
|
||||
RetryActivityTemplateRewardJob(ctx context.Context, req *activityv1.RetryActivityTemplateRewardJobRequest) (*activityv1.RetryActivityTemplateRewardJobResponse, error)
|
||||
ListActivityTemplateTaskClaims(ctx context.Context, req *activityv1.ListActivityTemplateTaskClaimsRequest) (*activityv1.ListActivityTemplateTaskClaimsResponse, error)
|
||||
RetryActivityTemplateTaskClaim(ctx context.Context, req *activityv1.RetryActivityTemplateTaskClaimRequest) (*activityv1.RetryActivityTemplateTaskClaimResponse, error)
|
||||
GetCPWeeklyRankConfig(ctx context.Context, req *activityv1.GetCPWeeklyRankConfigRequest) (*activityv1.GetCPWeeklyRankConfigResponse, error)
|
||||
UpdateCPWeeklyRankConfig(ctx context.Context, req *activityv1.UpdateCPWeeklyRankConfigRequest) (*activityv1.UpdateCPWeeklyRankConfigResponse, error)
|
||||
ListCPWeeklyRankSettlements(ctx context.Context, req *activityv1.ListCPWeeklyRankSettlementsRequest) (*activityv1.ListCPWeeklyRankSettlementsResponse, error)
|
||||
@ -80,6 +94,7 @@ type GRPCClient struct {
|
||||
roomTurnoverRewardClient activityv1.AdminRoomTurnoverRewardServiceClient
|
||||
weeklyStarClient activityv1.AdminWeeklyStarServiceClient
|
||||
agencyOpeningClient activityv1.AdminAgencyOpeningServiceClient
|
||||
activityTemplateClient activityv1.AdminActivityTemplateServiceClient
|
||||
cpWeeklyRankClient activityv1.AdminCPWeeklyRankServiceClient
|
||||
wheelClient activityv1.AdminWheelServiceClient
|
||||
broadcastClient activityv1.BroadcastServiceClient
|
||||
@ -100,6 +115,7 @@ func NewGRPC(conn grpc.ClientConnInterface) *GRPCClient {
|
||||
roomTurnoverRewardClient: activityv1.NewAdminRoomTurnoverRewardServiceClient(conn),
|
||||
weeklyStarClient: activityv1.NewAdminWeeklyStarServiceClient(conn),
|
||||
agencyOpeningClient: activityv1.NewAdminAgencyOpeningServiceClient(conn),
|
||||
activityTemplateClient: activityv1.NewAdminActivityTemplateServiceClient(conn),
|
||||
cpWeeklyRankClient: activityv1.NewAdminCPWeeklyRankServiceClient(conn),
|
||||
wheelClient: activityv1.NewAdminWheelServiceClient(conn),
|
||||
broadcastClient: activityv1.NewBroadcastServiceClient(conn),
|
||||
@ -273,6 +289,62 @@ func (c *GRPCClient) ReviewAgencyOpeningApplication(ctx context.Context, req *ac
|
||||
return c.agencyOpeningClient.ReviewAgencyOpeningApplication(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) ListActivityTemplates(ctx context.Context, req *activityv1.ListActivityTemplatesRequest) (*activityv1.ListActivityTemplatesResponse, error) {
|
||||
return c.activityTemplateClient.ListActivityTemplates(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) CreateActivityTemplate(ctx context.Context, req *activityv1.CreateActivityTemplateRequest) (*activityv1.CreateActivityTemplateResponse, error) {
|
||||
return c.activityTemplateClient.CreateActivityTemplate(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) GetActivityTemplate(ctx context.Context, req *activityv1.GetActivityTemplateRequest) (*activityv1.GetActivityTemplateResponse, error) {
|
||||
return c.activityTemplateClient.GetActivityTemplate(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) UpdateActivityTemplate(ctx context.Context, req *activityv1.UpdateActivityTemplateRequest) (*activityv1.UpdateActivityTemplateResponse, error) {
|
||||
return c.activityTemplateClient.UpdateActivityTemplate(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) SetActivityTemplateStatus(ctx context.Context, req *activityv1.SetActivityTemplateStatusRequest) (*activityv1.SetActivityTemplateStatusResponse, error) {
|
||||
return c.activityTemplateClient.SetActivityTemplateStatus(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) DeleteActivityTemplate(ctx context.Context, req *activityv1.DeleteActivityTemplateRequest) (*activityv1.DeleteActivityTemplateResponse, error) {
|
||||
return c.activityTemplateClient.DeleteActivityTemplate(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) ListActivityTemplateVersions(ctx context.Context, req *activityv1.ListActivityTemplateVersionsRequest) (*activityv1.ListActivityTemplateVersionsResponse, error) {
|
||||
return c.activityTemplateClient.ListActivityTemplateVersions(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) CloneActivityTemplate(ctx context.Context, req *activityv1.CloneActivityTemplateRequest) (*activityv1.CloneActivityTemplateResponse, error) {
|
||||
return c.activityTemplateClient.CloneActivityTemplate(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) ValidateActivityTemplate(ctx context.Context, req *activityv1.ValidateActivityTemplateRequest) (*activityv1.ValidateActivityTemplateResponse, error) {
|
||||
return c.activityTemplateClient.ValidateActivityTemplate(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) ListActivityTemplateLeaderboard(ctx context.Context, req *activityv1.AdminListActivityTemplateLeaderboardRequest) (*activityv1.AdminListActivityTemplateLeaderboardResponse, error) {
|
||||
return c.activityTemplateClient.ListActivityTemplateLeaderboard(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) ListActivityTemplateRewardJobs(ctx context.Context, req *activityv1.ListActivityTemplateRewardJobsRequest) (*activityv1.ListActivityTemplateRewardJobsResponse, error) {
|
||||
return c.activityTemplateClient.ListActivityTemplateRewardJobs(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) RetryActivityTemplateRewardJob(ctx context.Context, req *activityv1.RetryActivityTemplateRewardJobRequest) (*activityv1.RetryActivityTemplateRewardJobResponse, error) {
|
||||
return c.activityTemplateClient.RetryActivityTemplateRewardJob(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) ListActivityTemplateTaskClaims(ctx context.Context, req *activityv1.ListActivityTemplateTaskClaimsRequest) (*activityv1.ListActivityTemplateTaskClaimsResponse, error) {
|
||||
return c.activityTemplateClient.ListActivityTemplateTaskClaims(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) RetryActivityTemplateTaskClaim(ctx context.Context, req *activityv1.RetryActivityTemplateTaskClaimRequest) (*activityv1.RetryActivityTemplateTaskClaimResponse, error) {
|
||||
return c.activityTemplateClient.RetryActivityTemplateTaskClaim(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) GetCPWeeklyRankConfig(ctx context.Context, req *activityv1.GetCPWeeklyRankConfigRequest) (*activityv1.GetCPWeeklyRankConfigResponse, error) {
|
||||
return c.cpWeeklyRankClient.GetCPWeeklyRankConfig(ctx, req)
|
||||
}
|
||||
|
||||
@ -0,0 +1,148 @@
|
||||
package statisticsclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ActivityTemplateDataQuery struct {
|
||||
AppCode string
|
||||
TemplateID string
|
||||
PublishedVersion int64
|
||||
RegionID int64
|
||||
StartMS int64
|
||||
EndMS int64
|
||||
}
|
||||
|
||||
type ActivityTemplateOverview struct {
|
||||
TemplateID string `json:"template_id"`
|
||||
TemplateCode string `json:"template_code"`
|
||||
PublishedVersion int64 `json:"published_version"`
|
||||
Participants int64 `json:"participants"`
|
||||
VisitCount int64 `json:"visit_count"`
|
||||
GiftUsers int64 `json:"gift_users"`
|
||||
TaskUsers int64 `json:"task_users"`
|
||||
GiftEventCount int64 `json:"gift_event_count"`
|
||||
GiftCount int64 `json:"gift_count"`
|
||||
CoinAmount int64 `json:"coin_amount"`
|
||||
TaskProgress int64 `json:"task_progress"`
|
||||
TaskCompleted int64 `json:"task_completed"`
|
||||
TaskClaimed int64 `json:"task_claimed"`
|
||||
}
|
||||
|
||||
type ActivityTemplateTrendPoint struct {
|
||||
StatDay string `json:"stat_day"`
|
||||
Participants int64 `json:"participants"`
|
||||
VisitCount int64 `json:"visit_count"`
|
||||
GiftUsers int64 `json:"gift_users"`
|
||||
TaskUsers int64 `json:"task_users"`
|
||||
GiftEventCount int64 `json:"gift_event_count"`
|
||||
GiftCount int64 `json:"gift_count"`
|
||||
CoinAmount int64 `json:"coin_amount"`
|
||||
TaskProgress int64 `json:"task_progress"`
|
||||
TaskCompleted int64 `json:"task_completed"`
|
||||
TaskClaimed int64 `json:"task_claimed"`
|
||||
}
|
||||
|
||||
type ActivityTemplateGiftStat struct {
|
||||
GiftID string `json:"gift_id"`
|
||||
GiftUsers int64 `json:"gift_users"`
|
||||
GiftEventCount int64 `json:"gift_event_count"`
|
||||
GiftCount int64 `json:"gift_count"`
|
||||
CoinAmount int64 `json:"coin_amount"`
|
||||
}
|
||||
|
||||
type ActivityTemplateTaskStat struct {
|
||||
TaskKey string `json:"task_key"`
|
||||
TaskType string `json:"task_type"`
|
||||
TaskUsers int64 `json:"task_users"`
|
||||
TaskProgress int64 `json:"task_progress"`
|
||||
TaskCompleted int64 `json:"task_completed"`
|
||||
TaskClaimed int64 `json:"task_claimed"`
|
||||
}
|
||||
|
||||
// ActivityTemplateData mirrors statistics-service's aggregate-only read model.
|
||||
// Admin intentionally does not reconstruct any of these counters from activity owner tables.
|
||||
type ActivityTemplateData struct {
|
||||
Overview ActivityTemplateOverview `json:"overview"`
|
||||
Trend []ActivityTemplateTrendPoint `json:"trend"`
|
||||
Gifts []ActivityTemplateGiftStat `json:"gifts"`
|
||||
Tasks []ActivityTemplateTaskStat `json:"tasks"`
|
||||
Granularity string `json:"granularity"`
|
||||
Timezone string `json:"timezone"`
|
||||
StartMS int64 `json:"start_ms"`
|
||||
EndMS int64 `json:"end_ms"`
|
||||
RegionID int64 `json:"region_id"`
|
||||
DimensionLimit int `json:"dimension_limit"`
|
||||
GiftSort string `json:"gift_sort"`
|
||||
TaskSort string `json:"task_sort"`
|
||||
ServerTimeMS int64 `json:"server_time_ms"`
|
||||
}
|
||||
|
||||
type ActivityTemplateDataClient interface {
|
||||
ActivityTemplateData(context.Context, ActivityTemplateDataQuery) (ActivityTemplateData, error)
|
||||
}
|
||||
|
||||
type HTTPClient struct {
|
||||
baseURL string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewHTTP(baseURL string, timeout time.Duration) *HTTPClient {
|
||||
if timeout <= 0 {
|
||||
timeout = 3 * time.Second
|
||||
}
|
||||
return &HTTPClient{
|
||||
baseURL: strings.TrimRight(strings.TrimSpace(baseURL), "/"),
|
||||
client: &http.Client{Timeout: timeout},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HTTPClient) ActivityTemplateData(ctx context.Context, query ActivityTemplateDataQuery) (ActivityTemplateData, error) {
|
||||
values := url.Values{}
|
||||
values.Set("app_code", strings.TrimSpace(query.AppCode))
|
||||
values.Set("template_id", strings.TrimSpace(query.TemplateID))
|
||||
values.Set("published_version", strconv.FormatInt(query.PublishedVersion, 10))
|
||||
values.Set("start_ms", strconv.FormatInt(query.StartMS, 10))
|
||||
values.Set("end_ms", strconv.FormatInt(query.EndMS, 10))
|
||||
if query.RegionID > 0 {
|
||||
values.Set("region_id", strconv.FormatInt(query.RegionID, 10))
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/internal/v1/statistics/activity-templates/data?"+values.Encode(), nil)
|
||||
if err != nil {
|
||||
return ActivityTemplateData{}, err
|
||||
}
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return ActivityTemplateData{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
var body struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
_ = json.NewDecoder(resp.Body).Decode(&body)
|
||||
return ActivityTemplateData{}, fmt.Errorf("statistics service returned status %d: %s", resp.StatusCode, strings.TrimSpace(body.Error))
|
||||
}
|
||||
var out ActivityTemplateData
|
||||
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
||||
return ActivityTemplateData{}, err
|
||||
}
|
||||
// Arrays must remain arrays for dense table/chart clients; nil would serialize as null and force needless branches.
|
||||
if out.Trend == nil {
|
||||
out.Trend = []ActivityTemplateTrendPoint{}
|
||||
}
|
||||
if out.Gifts == nil {
|
||||
out.Gifts = []ActivityTemplateGiftStat{}
|
||||
}
|
||||
if out.Tasks == nil {
|
||||
out.Tasks = []ActivityTemplateTaskStat{}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
304
server/admin/internal/modules/activitytemplate/data.go
Normal file
304
server/admin/internal/modules/activitytemplate/data.go
Normal file
@ -0,0 +1,304 @@
|
||||
package activitytemplate
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hyapp-admin-server/internal/appctx"
|
||||
"hyapp-admin-server/internal/integration/statisticsclient"
|
||||
"hyapp-admin-server/internal/integration/userclient"
|
||||
"hyapp-admin-server/internal/middleware"
|
||||
"hyapp-admin-server/internal/modules/shared"
|
||||
"hyapp-admin-server/internal/response"
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const maxActivityTemplateDataRange = 366 * 24 * time.Hour
|
||||
|
||||
func (h *Handler) Leaderboard(c *gin.Context) {
|
||||
options := shared.ListOptions(c)
|
||||
versionNo, ok := positiveInt64Query(c, "version_no", true)
|
||||
if !ok {
|
||||
response.BadRequest(c, "version_no 参数不正确")
|
||||
return
|
||||
}
|
||||
boardType := strings.ToLower(strings.TrimSpace(c.Query("board_type")))
|
||||
periodKey := strings.TrimSpace(c.Query("period_key"))
|
||||
if boardType != "daily" && boardType != "total" {
|
||||
response.BadRequest(c, "board_type 参数不正确")
|
||||
return
|
||||
}
|
||||
if boardType == "daily" {
|
||||
if parsed, err := time.Parse("2006-01-02", periodKey); err != nil || parsed.Format("2006-01-02") != periodKey {
|
||||
response.BadRequest(c, "daily 榜单必须提供 YYYY-MM-DD 格式的 period_key")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
periodKey = "total"
|
||||
}
|
||||
resp, err := h.activity.ListActivityTemplateLeaderboard(c.Request.Context(), &activityv1.AdminListActivityTemplateLeaderboardRequest{
|
||||
Meta: h.meta(c), TemplateId: strings.TrimSpace(c.Param("template_id")), VersionNo: versionNo,
|
||||
BoardType: boardType, PeriodKey: periodKey, Page: int32(options.Page), PageSize: int32(options.PageSize),
|
||||
})
|
||||
if err != nil {
|
||||
h.writeGRPCError(c, err, "获取活动模版榜单失败")
|
||||
return
|
||||
}
|
||||
entries := make([]leaderboardEntryDTO, 0, len(resp.GetEntries()))
|
||||
users := h.activityTemplateUsers(c, leaderboardProtoUserIDs(resp.GetEntries()))
|
||||
for _, item := range resp.GetEntries() {
|
||||
entries = append(entries, leaderboardEntryFromProto(item, users))
|
||||
}
|
||||
response.OK(c, leaderboardPageDTO{
|
||||
Period: rankPeriodFromProto(resp.GetPeriod()), Items: entries,
|
||||
Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal(),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) RewardJobs(c *gin.Context) {
|
||||
options := shared.ListOptions(c)
|
||||
versionNo, ok := positiveInt64Query(c, "version_no", true)
|
||||
if !ok {
|
||||
response.BadRequest(c, "version_no 参数不正确")
|
||||
return
|
||||
}
|
||||
boardType := strings.ToLower(strings.TrimSpace(c.Query("board_type")))
|
||||
if boardType != "" && boardType != "daily" && boardType != "total" {
|
||||
response.BadRequest(c, "board_type 参数不正确")
|
||||
return
|
||||
}
|
||||
status := strings.ToLower(strings.TrimSpace(c.Query("status")))
|
||||
if status != "" && status != "pending" && status != "running" && status != "granted" && status != "failed" && status != "dead" {
|
||||
response.BadRequest(c, "status 参数不正确")
|
||||
return
|
||||
}
|
||||
periodKey := strings.TrimSpace(c.Query("period_key"))
|
||||
if boardType == "total" && periodKey != "" {
|
||||
periodKey = "total"
|
||||
}
|
||||
resp, err := h.activity.ListActivityTemplateRewardJobs(c.Request.Context(), &activityv1.ListActivityTemplateRewardJobsRequest{
|
||||
Meta: h.meta(c), TemplateId: strings.TrimSpace(c.Param("template_id")), VersionNo: versionNo,
|
||||
BoardType: boardType, PeriodKey: periodKey, Status: status, Page: int32(options.Page), PageSize: int32(options.PageSize),
|
||||
})
|
||||
if err != nil {
|
||||
h.writeGRPCError(c, err, "获取活动模版奖励发放记录失败")
|
||||
return
|
||||
}
|
||||
items := make([]rewardJobDTO, 0, len(resp.GetJobs()))
|
||||
users := h.activityTemplateUsers(c, rewardJobProtoUserIDs(resp.GetJobs()))
|
||||
for _, item := range resp.GetJobs() {
|
||||
items = append(items, rewardJobFromProto(item, users))
|
||||
}
|
||||
response.OK(c, pageDTO{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
|
||||
}
|
||||
|
||||
func (h *Handler) RetryRewardJob(c *gin.Context) {
|
||||
templateID := strings.TrimSpace(c.Param("template_id"))
|
||||
jobID := strings.TrimSpace(c.Param("reward_job_id"))
|
||||
if templateID == "" || jobID == "" {
|
||||
response.BadRequest(c, "活动模版或奖励任务 ID 不正确")
|
||||
return
|
||||
}
|
||||
resp, err := h.activity.RetryActivityTemplateRewardJob(c.Request.Context(), &activityv1.RetryActivityTemplateRewardJobRequest{
|
||||
Meta: h.meta(c), TemplateId: templateID, RewardJobId: jobID, OperatorAdminId: int64(middleware.CurrentUserID(c)),
|
||||
})
|
||||
if err != nil {
|
||||
h.writeGRPCError(c, err, "重试活动模版奖励发放失败")
|
||||
return
|
||||
}
|
||||
users := h.activityTemplateUsers(c, []int64{resp.GetJob().GetUserId()})
|
||||
item := rewardJobFromProto(resp.GetJob(), users)
|
||||
shared.OperationLogWithResourceID(c, h.audit, "retry-activity-template-reward", "activity_template_reward_jobs", jobID, "success", "")
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) TaskClaims(c *gin.Context) {
|
||||
options := shared.ListOptions(c)
|
||||
versionNo, versionOK := positiveInt64Query(c, "version_no", false)
|
||||
userID, userOK := positiveInt64Query(c, "user_id", false)
|
||||
if !versionOK || !userOK {
|
||||
response.BadRequest(c, "version_no 或 user_id 参数不正确")
|
||||
return
|
||||
}
|
||||
taskDay := strings.TrimSpace(c.Query("task_day"))
|
||||
if taskDay != "" {
|
||||
parsed, err := time.Parse("2006-01-02", taskDay)
|
||||
if err != nil || parsed.Format("2006-01-02") != taskDay {
|
||||
response.BadRequest(c, "task_day 必须为 YYYY-MM-DD")
|
||||
return
|
||||
}
|
||||
}
|
||||
status := strings.ToLower(strings.TrimSpace(c.Query("status")))
|
||||
if status != "" && status != "pending" && status != "running" && status != "granted" && status != "failed" {
|
||||
response.BadRequest(c, "status 参数不正确")
|
||||
return
|
||||
}
|
||||
resp, err := h.activity.ListActivityTemplateTaskClaims(c.Request.Context(), &activityv1.ListActivityTemplateTaskClaimsRequest{
|
||||
Meta: h.meta(c), TemplateId: strings.TrimSpace(c.Param("template_id")), VersionNo: versionNo,
|
||||
TaskDay: taskDay, TaskKey: strings.TrimSpace(c.Query("task_key")), UserId: userID, Status: status,
|
||||
Page: int32(options.Page), PageSize: int32(options.PageSize),
|
||||
})
|
||||
if err != nil {
|
||||
h.writeGRPCError(c, err, "获取活动模版任务领奖记录失败")
|
||||
return
|
||||
}
|
||||
items := make([]taskClaimDTO, 0, len(resp.GetClaims()))
|
||||
users := h.activityTemplateUsers(c, taskClaimProtoUserIDs(resp.GetClaims()))
|
||||
for _, item := range resp.GetClaims() {
|
||||
items = append(items, taskClaimFromProto(item, users))
|
||||
}
|
||||
response.OK(c, pageDTO{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
|
||||
}
|
||||
|
||||
func (h *Handler) RetryTaskClaim(c *gin.Context) {
|
||||
templateID := strings.TrimSpace(c.Param("template_id"))
|
||||
claimID := strings.TrimSpace(c.Param("claim_id"))
|
||||
if templateID == "" || claimID == "" {
|
||||
response.BadRequest(c, "活动模版或任务领奖记录 ID 不正确")
|
||||
return
|
||||
}
|
||||
resp, err := h.activity.RetryActivityTemplateTaskClaim(c.Request.Context(), &activityv1.RetryActivityTemplateTaskClaimRequest{
|
||||
Meta: h.meta(c), TemplateId: templateID, ClaimId: claimID, OperatorAdminId: int64(middleware.CurrentUserID(c)),
|
||||
})
|
||||
if err != nil {
|
||||
h.writeGRPCError(c, err, "重试活动模版任务奖励发放失败")
|
||||
return
|
||||
}
|
||||
users := h.activityTemplateUsers(c, []int64{resp.GetClaim().GetUserId()})
|
||||
item := taskClaimFromProto(resp.GetClaim(), users)
|
||||
shared.OperationLogWithResourceID(c, h.audit, "retry-activity-template-task-claim", "activity_template_task_claims", claimID, "success", "")
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) activityTemplateUsers(c *gin.Context, userIDs []int64) map[int64]*userclient.User {
|
||||
if h.users == nil || len(userIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
unique := make([]int64, 0, len(userIDs))
|
||||
seen := make(map[int64]struct{}, len(userIDs))
|
||||
for _, userID := range userIDs {
|
||||
if userID <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[userID]; exists {
|
||||
continue
|
||||
}
|
||||
seen[userID] = struct{}{}
|
||||
unique = append(unique, userID)
|
||||
}
|
||||
if len(unique) == 0 {
|
||||
return nil
|
||||
}
|
||||
users, err := h.users.BatchGetUsers(c.Request.Context(), userclient.BatchGetUsersRequest{
|
||||
RequestID: middleware.CurrentRequestID(c), Caller: "admin-server", UserIDs: unique,
|
||||
})
|
||||
if err != nil {
|
||||
// 用户资料是展示装饰,榜单和发奖事实仍必须返回;不能因 user-service 短暂不可用隐藏结算结果。
|
||||
return nil
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
func leaderboardProtoUserIDs(items []*activityv1.ActivityTemplateLeaderboardEntry) []int64 {
|
||||
userIDs := make([]int64, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item != nil {
|
||||
userIDs = append(userIDs, item.GetUserId())
|
||||
}
|
||||
}
|
||||
return userIDs
|
||||
}
|
||||
|
||||
func rewardJobProtoUserIDs(items []*activityv1.ActivityTemplateRewardJob) []int64 {
|
||||
userIDs := make([]int64, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item != nil {
|
||||
userIDs = append(userIDs, item.GetUserId())
|
||||
}
|
||||
}
|
||||
return userIDs
|
||||
}
|
||||
|
||||
func taskClaimProtoUserIDs(items []*activityv1.ActivityTemplateTaskClaim) []int64 {
|
||||
userIDs := make([]int64, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item != nil {
|
||||
userIDs = append(userIDs, item.GetUserId())
|
||||
}
|
||||
}
|
||||
return userIDs
|
||||
}
|
||||
|
||||
func (h *Handler) Data(c *gin.Context) {
|
||||
if h.stats == nil {
|
||||
response.ServerError(c, "活动模版统计服务未配置")
|
||||
return
|
||||
}
|
||||
versionNo, ok := positiveInt64Query(c, "version_no", true)
|
||||
if !ok {
|
||||
response.BadRequest(c, "version_no 参数不正确")
|
||||
return
|
||||
}
|
||||
startMS, startOK := requiredPositiveInt64Query(c, "start_ms")
|
||||
endMS, endOK := requiredPositiveInt64Query(c, "end_ms")
|
||||
if !startOK || !endOK || endMS <= startMS {
|
||||
response.BadRequest(c, "start_ms 或 end_ms 参数不正确")
|
||||
return
|
||||
}
|
||||
regionID := int64(0)
|
||||
if raw, exists := c.GetQuery("region_id"); exists {
|
||||
var err error
|
||||
regionID, err = strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
|
||||
if err != nil || regionID <= 0 {
|
||||
response.BadRequest(c, "region_id 参数不正确")
|
||||
return
|
||||
}
|
||||
}
|
||||
// statistics-service 按 UTC 日聚合。把任意 UI 时间窗归一为覆盖它的完整 [UTC 00:00, UTC 00:00),
|
||||
// 既避免半日过滤产生“看似精确但实际丢整日”的静默误差,也由响应 start_ms/end_ms 明确回显生效窗口。
|
||||
effectiveStartMS := utcDayFloor(startMS)
|
||||
effectiveEndMS := utcDayCeil(endMS)
|
||||
if effectiveEndMS-effectiveStartMS > maxActivityTemplateDataRange.Milliseconds() {
|
||||
response.BadRequest(c, "活动模版数据查询区间不能超过 366 天")
|
||||
return
|
||||
}
|
||||
data, err := h.stats.ActivityTemplateData(c.Request.Context(), statisticsclient.ActivityTemplateDataQuery{
|
||||
AppCode: appctx.FromContext(c.Request.Context()), TemplateID: strings.TrimSpace(c.Param("template_id")),
|
||||
PublishedVersion: versionNo, RegionID: regionID, StartMS: effectiveStartMS, EndMS: effectiveEndMS,
|
||||
})
|
||||
if err != nil {
|
||||
response.ServerError(c, "获取活动模版统计数据失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, data)
|
||||
}
|
||||
|
||||
func positiveInt64Query(c *gin.Context, key string, required bool) (int64, bool) {
|
||||
raw, exists := c.GetQuery(key)
|
||||
if !exists || strings.TrimSpace(raw) == "" {
|
||||
return 0, !required
|
||||
}
|
||||
value, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
|
||||
return value, err == nil && value > 0
|
||||
}
|
||||
|
||||
func requiredPositiveInt64Query(c *gin.Context, key string) (int64, bool) {
|
||||
value, ok := positiveInt64Query(c, key, true)
|
||||
return value, ok
|
||||
}
|
||||
|
||||
func utcDayFloor(ms int64) int64 {
|
||||
value := time.UnixMilli(ms).UTC()
|
||||
return time.Date(value.Year(), value.Month(), value.Day(), 0, 0, 0, 0, time.UTC).UnixMilli()
|
||||
}
|
||||
|
||||
func utcDayCeil(ms int64) int64 {
|
||||
floor := utcDayFloor(ms)
|
||||
if floor == ms {
|
||||
return floor
|
||||
}
|
||||
return floor + 24*time.Hour.Milliseconds()
|
||||
}
|
||||
275
server/admin/internal/modules/activitytemplate/handler.go
Normal file
275
server/admin/internal/modules/activitytemplate/handler.go
Normal file
@ -0,0 +1,275 @@
|
||||
package activitytemplate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hyapp-admin-server/internal/appctx"
|
||||
"hyapp-admin-server/internal/integration/activityclient"
|
||||
"hyapp-admin-server/internal/integration/statisticsclient"
|
||||
"hyapp-admin-server/internal/integration/userclient"
|
||||
"hyapp-admin-server/internal/middleware"
|
||||
"hyapp-admin-server/internal/modules/shared"
|
||||
"hyapp-admin-server/internal/response"
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"google.golang.org/grpc/codes"
|
||||
grpcstatus "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
activity activityclient.Client
|
||||
stats statisticsclient.ActivityTemplateDataClient
|
||||
users UserBatchGetter
|
||||
audit shared.OperationLogger
|
||||
}
|
||||
|
||||
type UserBatchGetter interface {
|
||||
BatchGetUsers(context.Context, userclient.BatchGetUsersRequest) (map[int64]*userclient.User, error)
|
||||
}
|
||||
|
||||
func New(activity activityclient.Client, audit shared.OperationLogger, dataClients ...statisticsclient.ActivityTemplateDataClient) *Handler {
|
||||
handler := &Handler{activity: activity, audit: audit}
|
||||
if len(dataClients) > 0 {
|
||||
handler.stats = dataClients[0]
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
func (h *Handler) BindUserBatchGetter(users UserBatchGetter) *Handler {
|
||||
if h != nil {
|
||||
h.users = users
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *Handler) List(c *gin.Context) {
|
||||
options := shared.ListOptions(c)
|
||||
startMS, startOK := parseOptionalNonNegativeInt64Query(c, "start_ms")
|
||||
endMS, endOK := parseOptionalNonNegativeInt64Query(c, "end_ms")
|
||||
if !startOK || !endOK {
|
||||
response.BadRequest(c, "start_ms 或 end_ms 参数不正确")
|
||||
return
|
||||
}
|
||||
req := &activityv1.ListActivityTemplatesRequest{
|
||||
Meta: h.meta(c),
|
||||
Keyword: strings.TrimSpace(options.Keyword),
|
||||
Status: strings.TrimSpace(options.Status),
|
||||
StartMs: startMS,
|
||||
EndMs: endMS,
|
||||
Page: int32(options.Page),
|
||||
PageSize: int32(options.PageSize),
|
||||
}
|
||||
if raw, exists := c.GetQuery("region_id"); exists {
|
||||
regionID, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
|
||||
if err != nil || regionID <= 0 {
|
||||
response.BadRequest(c, "region_id 参数不正确")
|
||||
return
|
||||
}
|
||||
req.RegionId = ®ionID
|
||||
}
|
||||
if raw, exists := c.GetQuery("all_regions"); exists {
|
||||
allRegions, err := strconv.ParseBool(strings.TrimSpace(raw))
|
||||
if err != nil {
|
||||
response.BadRequest(c, "all_regions 参数不正确")
|
||||
return
|
||||
}
|
||||
req.AllRegions = &allRegions
|
||||
}
|
||||
resp, err := h.activity.ListActivityTemplates(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
h.writeGRPCError(c, err, "获取活动模版失败")
|
||||
return
|
||||
}
|
||||
items := make([]templateSummaryDTO, 0, len(resp.GetTemplates()))
|
||||
for _, item := range resp.GetTemplates() {
|
||||
items = append(items, summaryFromProto(item))
|
||||
}
|
||||
response.OK(c, pageDTO{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal(), ServerTimeMS: resp.GetServerTimeMs()})
|
||||
}
|
||||
|
||||
func (h *Handler) Create(c *gin.Context) {
|
||||
var req templateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "活动模版参数不正确")
|
||||
return
|
||||
}
|
||||
resp, err := h.activity.CreateActivityTemplate(c.Request.Context(), &activityv1.CreateActivityTemplateRequest{
|
||||
Meta: h.meta(c), Template: req.toProto(""), OperatorAdminId: int64(middleware.CurrentUserID(c)),
|
||||
})
|
||||
if err != nil {
|
||||
h.writeGRPCError(c, err, "创建活动模版失败")
|
||||
return
|
||||
}
|
||||
item := templateFromProto(resp.GetTemplate())
|
||||
shared.OperationLogWithResourceID(c, h.audit, "create-activity-template", "activity_templates", item.TemplateID, "success", "")
|
||||
response.Created(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) Get(c *gin.Context) {
|
||||
templateID := strings.TrimSpace(c.Param("template_id"))
|
||||
resp, err := h.activity.GetActivityTemplate(c.Request.Context(), &activityv1.GetActivityTemplateRequest{Meta: h.meta(c), TemplateId: templateID})
|
||||
if err != nil {
|
||||
h.writeGRPCError(c, err, "获取活动模版详情失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, templateFromProto(resp.GetTemplate()))
|
||||
}
|
||||
|
||||
func (h *Handler) Update(c *gin.Context) {
|
||||
var req templateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "活动模版参数不正确")
|
||||
return
|
||||
}
|
||||
resp, err := h.activity.UpdateActivityTemplate(c.Request.Context(), &activityv1.UpdateActivityTemplateRequest{
|
||||
Meta: h.meta(c), Template: req.toProto(strings.TrimSpace(c.Param("template_id"))),
|
||||
ExpectedRevision: req.ExpectedRevision, OperatorAdminId: int64(middleware.CurrentUserID(c)),
|
||||
})
|
||||
if err != nil {
|
||||
h.writeGRPCError(c, err, "更新活动模版失败")
|
||||
return
|
||||
}
|
||||
item := templateFromProto(resp.GetTemplate())
|
||||
shared.OperationLogWithResourceID(c, h.audit, "update-activity-template", "activity_templates", item.TemplateID, "success", "")
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) SetStatus(c *gin.Context) {
|
||||
var req statusRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "活动模版状态参数不正确")
|
||||
return
|
||||
}
|
||||
templateID := strings.TrimSpace(c.Param("template_id"))
|
||||
resp, err := h.activity.SetActivityTemplateStatus(c.Request.Context(), &activityv1.SetActivityTemplateStatusRequest{
|
||||
Meta: h.meta(c), TemplateId: templateID, Status: strings.TrimSpace(req.Status),
|
||||
ExpectedRevision: req.ExpectedRevision, OperatorAdminId: int64(middleware.CurrentUserID(c)),
|
||||
})
|
||||
if err != nil {
|
||||
h.writeGRPCError(c, err, "更新活动模版状态失败")
|
||||
return
|
||||
}
|
||||
item := templateFromProto(resp.GetTemplate())
|
||||
shared.OperationLogWithResourceID(c, h.audit, "set-activity-template-status", "activity_templates", item.TemplateID, "success", "")
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) Delete(c *gin.Context) {
|
||||
expectedRevision := parseInt64Query(c, "revision")
|
||||
if expectedRevision <= 0 {
|
||||
response.BadRequest(c, "revision 参数不正确")
|
||||
return
|
||||
}
|
||||
templateID := strings.TrimSpace(c.Param("template_id"))
|
||||
resp, err := h.activity.DeleteActivityTemplate(c.Request.Context(), &activityv1.DeleteActivityTemplateRequest{
|
||||
Meta: h.meta(c), TemplateId: templateID, ExpectedRevision: expectedRevision,
|
||||
OperatorAdminId: int64(middleware.CurrentUserID(c)),
|
||||
})
|
||||
if err != nil {
|
||||
h.writeGRPCError(c, err, "删除活动模版失败")
|
||||
return
|
||||
}
|
||||
item := templateFromProto(resp.GetTemplate())
|
||||
shared.OperationLogWithResourceID(c, h.audit, "archive-activity-template", "activity_templates", item.TemplateID, "success", "")
|
||||
response.OK(c, gin.H{"template": item, "archived": resp.GetArchived()})
|
||||
}
|
||||
|
||||
func (h *Handler) Versions(c *gin.Context) {
|
||||
options := shared.ListOptions(c)
|
||||
resp, err := h.activity.ListActivityTemplateVersions(c.Request.Context(), &activityv1.ListActivityTemplateVersionsRequest{
|
||||
Meta: h.meta(c), TemplateId: strings.TrimSpace(c.Param("template_id")), Page: int32(options.Page), PageSize: int32(options.PageSize),
|
||||
})
|
||||
if err != nil {
|
||||
h.writeGRPCError(c, err, "获取活动模版版本失败")
|
||||
return
|
||||
}
|
||||
items := make([]versionDTO, 0, len(resp.GetVersions()))
|
||||
for _, version := range resp.GetVersions() {
|
||||
items = append(items, versionDTO{
|
||||
TemplateID: version.GetTemplateId(), VersionNo: version.GetVersionNo(), Snapshot: templateFromProto(version.GetSnapshot()),
|
||||
PublishedByAdminID: version.GetPublishedByAdminId(), PublishedAtMS: version.GetPublishedAtMs(),
|
||||
RuntimeFromMS: version.GetRuntimeFromMs(), RuntimeToMS: version.GetRuntimeToMs(),
|
||||
})
|
||||
}
|
||||
response.OK(c, pageDTO{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
|
||||
}
|
||||
|
||||
func (h *Handler) Clone(c *gin.Context) {
|
||||
var req cloneRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "复制活动模版参数不正确")
|
||||
return
|
||||
}
|
||||
resp, err := h.activity.CloneActivityTemplate(c.Request.Context(), &activityv1.CloneActivityTemplateRequest{
|
||||
Meta: h.meta(c), SourceTemplateId: strings.TrimSpace(c.Param("template_id")), SourceVersion: req.SourceVersion,
|
||||
TemplateCode: strings.TrimSpace(req.TemplateCode), Name: strings.TrimSpace(req.Name), OperatorAdminId: int64(middleware.CurrentUserID(c)),
|
||||
})
|
||||
if err != nil {
|
||||
h.writeGRPCError(c, err, "复制活动模版失败")
|
||||
return
|
||||
}
|
||||
item := templateFromProto(resp.GetTemplate())
|
||||
shared.OperationLogWithResourceID(c, h.audit, "clone-activity-template", "activity_templates", item.TemplateID, "success", "")
|
||||
response.Created(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) Validate(c *gin.Context) {
|
||||
var req validateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "活动模版校验参数不正确")
|
||||
return
|
||||
}
|
||||
resp, err := h.activity.ValidateActivityTemplate(c.Request.Context(), &activityv1.ValidateActivityTemplateRequest{
|
||||
Meta: h.meta(c), Template: req.Template.toProto(""), ForPublish: req.ForPublish,
|
||||
})
|
||||
if err != nil {
|
||||
h.writeGRPCError(c, err, "校验活动模版失败")
|
||||
return
|
||||
}
|
||||
issues := make([]validationIssueDTO, 0, len(resp.GetIssues()))
|
||||
for _, issue := range resp.GetIssues() {
|
||||
issues = append(issues, validationIssueDTO{Field: issue.GetField(), Code: issue.GetCode(), Message: issue.GetMessage()})
|
||||
}
|
||||
response.OK(c, gin.H{"valid": resp.GetValid(), "issues": issues})
|
||||
}
|
||||
|
||||
func (h *Handler) meta(c *gin.Context) *activityv1.RequestMeta {
|
||||
return &activityv1.RequestMeta{
|
||||
RequestId: middleware.CurrentRequestID(c), Caller: "admin-server", AppCode: appctx.FromContext(c.Request.Context()),
|
||||
SentAtMs: time.Now().UTC().UnixMilli(),
|
||||
}
|
||||
}
|
||||
|
||||
// writeGRPCError 把 owner service 的并发和状态冲突稳定映射为 HTTP 409;
|
||||
// 不能把 revision 冲突降级成 400,否则编辑器无法区分字段错误与“别人已经保存过”。
|
||||
func (h *Handler) writeGRPCError(c *gin.Context, err error, fallback string) {
|
||||
status := grpcstatus.Convert(err)
|
||||
switch status.Code() {
|
||||
case codes.InvalidArgument:
|
||||
response.BadRequest(c, status.Message())
|
||||
case codes.NotFound:
|
||||
response.NotFound(c, status.Message())
|
||||
case codes.AlreadyExists, codes.FailedPrecondition, codes.Aborted:
|
||||
response.Conflict(c, status.Message())
|
||||
default:
|
||||
response.ServerError(c, fallback)
|
||||
}
|
||||
}
|
||||
|
||||
func parseInt64Query(c *gin.Context, key string) int64 {
|
||||
value, _ := strconv.ParseInt(strings.TrimSpace(c.Query(key)), 10, 64)
|
||||
return value
|
||||
}
|
||||
|
||||
func parseOptionalNonNegativeInt64Query(c *gin.Context, key string) (int64, bool) {
|
||||
raw, exists := c.GetQuery(key)
|
||||
if !exists {
|
||||
return 0, true
|
||||
}
|
||||
value, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
|
||||
return value, err == nil && value >= 0
|
||||
}
|
||||
412
server/admin/internal/modules/activitytemplate/handler_test.go
Normal file
412
server/admin/internal/modules/activitytemplate/handler_test.go
Normal file
@ -0,0 +1,412 @@
|
||||
package activitytemplate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hyapp-admin-server/internal/integration/activityclient"
|
||||
"hyapp-admin-server/internal/integration/statisticsclient"
|
||||
"hyapp-admin-server/internal/integration/userclient"
|
||||
"hyapp-admin-server/internal/middleware"
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type revisionConflictClient struct {
|
||||
activityclient.Client
|
||||
}
|
||||
|
||||
type activityTemplateDataOwnerClient struct {
|
||||
activityclient.Client
|
||||
leaderboard *activityv1.AdminListActivityTemplateLeaderboardResponse
|
||||
rewards *activityv1.ListActivityTemplateRewardJobsResponse
|
||||
retryReward *activityv1.RetryActivityTemplateRewardJobResponse
|
||||
claims *activityv1.ListActivityTemplateTaskClaimsResponse
|
||||
retryClaim *activityv1.RetryActivityTemplateTaskClaimResponse
|
||||
lastClaims *activityv1.ListActivityTemplateTaskClaimsRequest
|
||||
lastRetry *activityv1.RetryActivityTemplateTaskClaimRequest
|
||||
lastRewards *activityv1.ListActivityTemplateRewardJobsRequest
|
||||
lastRewardRetry *activityv1.RetryActivityTemplateRewardJobRequest
|
||||
}
|
||||
|
||||
type activityTemplateVersionsOwnerClient struct {
|
||||
activityclient.Client
|
||||
response *activityv1.ListActivityTemplateVersionsResponse
|
||||
}
|
||||
|
||||
func (f activityTemplateVersionsOwnerClient) ListActivityTemplateVersions(context.Context, *activityv1.ListActivityTemplateVersionsRequest) (*activityv1.ListActivityTemplateVersionsResponse, error) {
|
||||
return f.response, nil
|
||||
}
|
||||
|
||||
func (f activityTemplateDataOwnerClient) ListActivityTemplateLeaderboard(context.Context, *activityv1.AdminListActivityTemplateLeaderboardRequest) (*activityv1.AdminListActivityTemplateLeaderboardResponse, error) {
|
||||
return f.leaderboard, nil
|
||||
}
|
||||
|
||||
func (f *activityTemplateDataOwnerClient) ListActivityTemplateRewardJobs(_ context.Context, req *activityv1.ListActivityTemplateRewardJobsRequest) (*activityv1.ListActivityTemplateRewardJobsResponse, error) {
|
||||
f.lastRewards = req
|
||||
return f.rewards, nil
|
||||
}
|
||||
|
||||
func (f *activityTemplateDataOwnerClient) RetryActivityTemplateRewardJob(_ context.Context, req *activityv1.RetryActivityTemplateRewardJobRequest) (*activityv1.RetryActivityTemplateRewardJobResponse, error) {
|
||||
f.lastRewardRetry = req
|
||||
return f.retryReward, nil
|
||||
}
|
||||
|
||||
func (f *activityTemplateDataOwnerClient) ListActivityTemplateTaskClaims(_ context.Context, req *activityv1.ListActivityTemplateTaskClaimsRequest) (*activityv1.ListActivityTemplateTaskClaimsResponse, error) {
|
||||
f.lastClaims = req
|
||||
return f.claims, nil
|
||||
}
|
||||
|
||||
func (f *activityTemplateDataOwnerClient) RetryActivityTemplateTaskClaim(_ context.Context, req *activityv1.RetryActivityTemplateTaskClaimRequest) (*activityv1.RetryActivityTemplateTaskClaimResponse, error) {
|
||||
f.lastRetry = req
|
||||
return f.retryClaim, nil
|
||||
}
|
||||
|
||||
type captureActivityTemplateUsers struct {
|
||||
calls int
|
||||
userIDs []int64
|
||||
users map[int64]*userclient.User
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *captureActivityTemplateUsers) BatchGetUsers(_ context.Context, req userclient.BatchGetUsersRequest) (map[int64]*userclient.User, error) {
|
||||
f.calls++
|
||||
f.userIDs = append([]int64(nil), req.UserIDs...)
|
||||
return f.users, f.err
|
||||
}
|
||||
|
||||
type captureActivityTemplateDataClient struct {
|
||||
query statisticsclient.ActivityTemplateDataQuery
|
||||
}
|
||||
|
||||
func (c *captureActivityTemplateDataClient) ActivityTemplateData(_ context.Context, query statisticsclient.ActivityTemplateDataQuery) (statisticsclient.ActivityTemplateData, error) {
|
||||
c.query = query
|
||||
return statisticsclient.ActivityTemplateData{
|
||||
Overview: statisticsclient.ActivityTemplateOverview{TemplateID: query.TemplateID, PublishedVersion: query.PublishedVersion, VisitCount: 13},
|
||||
Trend: []statisticsclient.ActivityTemplateTrendPoint{{StatDay: "2026-07-14", VisitCount: 7}}, Gifts: []statisticsclient.ActivityTemplateGiftStat{},
|
||||
Tasks: []statisticsclient.ActivityTemplateTaskStat{}, Granularity: "day", Timezone: "UTC",
|
||||
StartMS: query.StartMS, EndMS: query.EndMS, RegionID: query.RegionID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (revisionConflictClient) UpdateActivityTemplate(context.Context, *activityv1.UpdateActivityTemplateRequest) (*activityv1.UpdateActivityTemplateResponse, error) {
|
||||
return nil, status.Error(codes.FailedPrecondition, "activity template revision changed")
|
||||
}
|
||||
|
||||
func TestUpdateMapsRevisionConflictToHTTP409(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(recorder)
|
||||
ctx.Request = httptest.NewRequest(http.MethodPut, "/api/v1/admin/activities/templates/acttpl_1", strings.NewReader(`{
|
||||
"template_code":"gift_challenge_2026",
|
||||
"name":"Gift Challenge",
|
||||
"activity_type":"gift_challenge",
|
||||
"expected_revision":7
|
||||
}`))
|
||||
ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
ctx.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}
|
||||
|
||||
New(revisionConflictClient{}, nil).Update(ctx)
|
||||
|
||||
if recorder.Code != http.StatusConflict {
|
||||
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusConflict, recorder.Body.String())
|
||||
}
|
||||
var body struct {
|
||||
Code int `json:"code"`
|
||||
}
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if body.Code != 40900 {
|
||||
t.Fatalf("response code = %d, want 40900", body.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRejectsMalformedTimeFilterBeforeCallingOwnerService(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(recorder)
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/activities/templates?start_ms=not-a-time", nil)
|
||||
|
||||
New(nil, nil).List(ctx)
|
||||
|
||||
if recorder.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusBadRequest, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionsExposeActualRuntimeWindow(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(recorder)
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/activities/templates/acttpl_1/versions?page=1&page_size=20", nil)
|
||||
ctx.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}
|
||||
client := activityTemplateVersionsOwnerClient{response: &activityv1.ListActivityTemplateVersionsResponse{
|
||||
Total: 1,
|
||||
Versions: []*activityv1.ActivityTemplateVersion{{
|
||||
TemplateId: "acttpl_1", VersionNo: 1, Snapshot: &activityv1.ActivityTemplate{TemplateId: "acttpl_1", LifecycleStatus: "disabled"},
|
||||
PublishedByAdminId: 90001, PublishedAtMs: 100, RuntimeFromMs: 100, RuntimeToMs: 500,
|
||||
}},
|
||||
}}
|
||||
|
||||
New(client, nil).Versions(ctx)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d; body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
var body struct {
|
||||
Data struct {
|
||||
Items []struct {
|
||||
RuntimeFromMS int64 `json:"runtime_from_ms"`
|
||||
RuntimeToMS int64 `json:"runtime_to_ms"`
|
||||
Snapshot struct {
|
||||
LifecycleStatus string `json:"lifecycle_status"`
|
||||
} `json:"snapshot"`
|
||||
} `json:"items"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if len(body.Data.Items) != 1 || body.Data.Items[0].RuntimeFromMS != 100 || body.Data.Items[0].RuntimeToMS != 500 || body.Data.Items[0].Snapshot.LifecycleStatus != "disabled" {
|
||||
t.Fatalf("version runtime response = %+v", body.Data.Items)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataNormalizesRequestedWindowToUTCWholeDays(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
stats := &captureActivityTemplateDataClient{}
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(recorder)
|
||||
start := time.Date(2026, 7, 14, 12, 30, 0, 0, time.UTC).UnixMilli()
|
||||
end := time.Date(2026, 7, 15, 8, 45, 0, 0, time.UTC).UnixMilli()
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/activities/templates/acttpl_1/data?version_no=3®ion_id=7&start_ms="+strconv.FormatInt(start, 10)+"&end_ms="+strconv.FormatInt(end, 10), nil)
|
||||
ctx.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}
|
||||
|
||||
New(nil, nil, stats).Data(ctx)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d; body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
wantStart := time.Date(2026, 7, 14, 0, 0, 0, 0, time.UTC).UnixMilli()
|
||||
wantEnd := time.Date(2026, 7, 16, 0, 0, 0, 0, time.UTC).UnixMilli()
|
||||
if stats.query.TemplateID != "acttpl_1" || stats.query.PublishedVersion != 3 || stats.query.RegionID != 7 ||
|
||||
stats.query.StartMS != wantStart || stats.query.EndMS != wantEnd {
|
||||
t.Fatalf("normalized statistics query = %+v", stats.query)
|
||||
}
|
||||
var body struct {
|
||||
Data statisticsclient.ActivityTemplateData `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if body.Data.StartMS != wantStart || body.Data.EndMS != wantEnd || body.Data.Granularity != "day" || body.Data.Timezone != "UTC" ||
|
||||
body.Data.Overview.VisitCount != 13 || len(body.Data.Trend) != 1 || body.Data.Trend[0].VisitCount != 7 {
|
||||
t.Fatalf("effective range not echoed: %+v", body.Data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLeaderboardAndRewardsBatchDecorateUsersWithoutOwningProfileFacts(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
owner := activityTemplateDataOwnerClient{
|
||||
leaderboard: &activityv1.AdminListActivityTemplateLeaderboardResponse{
|
||||
Period: &activityv1.ActivityTemplateRankPeriod{TemplateId: "acttpl_1", BoardType: "daily", PeriodKey: "2026-07-14"},
|
||||
Entries: []*activityv1.ActivityTemplateLeaderboardEntry{
|
||||
{RankNo: 1, UserId: 501, Score: 900}, {RankNo: 2, UserId: 501, Score: 800}, {RankNo: 3, UserId: 502, Score: 700},
|
||||
}, Total: 3,
|
||||
},
|
||||
rewards: &activityv1.ListActivityTemplateRewardJobsResponse{
|
||||
Jobs: []*activityv1.ActivityTemplateRewardJob{{RewardJobId: "job-1", UserId: 502, Status: "granted"}}, Total: 1,
|
||||
},
|
||||
}
|
||||
users := &captureActivityTemplateUsers{users: map[int64]*userclient.User{
|
||||
501: {UserID: 501, DisplayUserID: "L501", Username: "first", Avatar: "first.png"},
|
||||
502: {UserID: 502, DisplayUserID: "L502", Username: "second", Avatar: "second.png"},
|
||||
}}
|
||||
handler := New(&owner, nil).BindUserBatchGetter(users)
|
||||
|
||||
leaderboardRecorder := httptest.NewRecorder()
|
||||
leaderboardContext, _ := gin.CreateTestContext(leaderboardRecorder)
|
||||
leaderboardContext.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/activities/templates/acttpl_1/leaderboard?version_no=1&board_type=daily&period_key=2026-07-14", nil)
|
||||
leaderboardContext.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}
|
||||
handler.Leaderboard(leaderboardContext)
|
||||
if leaderboardRecorder.Code != http.StatusOK || users.calls != 1 || len(users.userIDs) != 2 || users.userIDs[0] != 501 || users.userIDs[1] != 502 {
|
||||
t.Fatalf("leaderboard batch enrichment: status=%d calls=%d ids=%v body=%s", leaderboardRecorder.Code, users.calls, users.userIDs, leaderboardRecorder.Body.String())
|
||||
}
|
||||
var leaderboardBody struct {
|
||||
Data struct {
|
||||
Items []struct {
|
||||
User *activityTemplateUserDTO `json:"user"`
|
||||
} `json:"items"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(leaderboardRecorder.Body.Bytes(), &leaderboardBody); err != nil || len(leaderboardBody.Data.Items) != 3 || leaderboardBody.Data.Items[0].User == nil || leaderboardBody.Data.Items[0].User.DisplayUserID != "L501" {
|
||||
t.Fatalf("leaderboard user decoration mismatch: err=%v body=%s", err, leaderboardRecorder.Body.String())
|
||||
}
|
||||
|
||||
users.calls, users.userIDs = 0, nil
|
||||
rewardsRecorder := httptest.NewRecorder()
|
||||
rewardsContext, _ := gin.CreateTestContext(rewardsRecorder)
|
||||
rewardsContext.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/activities/templates/acttpl_1/rewards?version_no=1", nil)
|
||||
rewardsContext.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}
|
||||
handler.RewardJobs(rewardsContext)
|
||||
if rewardsRecorder.Code != http.StatusOK || users.calls != 1 || len(users.userIDs) != 1 || users.userIDs[0] != 502 || !strings.Contains(rewardsRecorder.Body.String(), `"display_user_id":"L502"`) {
|
||||
t.Fatalf("reward user decoration: status=%d calls=%d ids=%v body=%s", rewardsRecorder.Code, users.calls, users.userIDs, rewardsRecorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewardJobsRequiresPublishedVersion(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(recorder)
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/activities/templates/acttpl_1/rewards", nil)
|
||||
ctx.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}
|
||||
|
||||
New(&activityTemplateDataOwnerClient{}, nil).RewardJobs(ctx)
|
||||
|
||||
if recorder.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusBadRequest, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewardJobsExposeDeadBackoffAndAllowExplicitRevival(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
dead := &activityv1.ActivityTemplateRewardJob{
|
||||
RewardJobId: "job-dead", TemplateId: "acttpl_1", VersionNo: 2, UserId: 502,
|
||||
Status: "dead", FailureReason: "maximum delivery attempts exhausted", AttemptCount: 8,
|
||||
MaxAttempts: 8, NextRetryAtMs: 0,
|
||||
}
|
||||
running := proto.Clone(dead).(*activityv1.ActivityTemplateRewardJob)
|
||||
running.Status, running.AttemptCount, running.MaxAttempts = "running", 9, 9
|
||||
owner := &activityTemplateDataOwnerClient{
|
||||
rewards: &activityv1.ListActivityTemplateRewardJobsResponse{Jobs: []*activityv1.ActivityTemplateRewardJob{dead}, Total: 1},
|
||||
retryReward: &activityv1.RetryActivityTemplateRewardJobResponse{Job: running},
|
||||
}
|
||||
handler := New(owner, nil)
|
||||
|
||||
listRecorder := httptest.NewRecorder()
|
||||
listContext, _ := gin.CreateTestContext(listRecorder)
|
||||
listContext.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/activities/templates/acttpl_1/rewards?version_no=2&status=dead", nil)
|
||||
listContext.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}
|
||||
handler.RewardJobs(listContext)
|
||||
if listRecorder.Code != http.StatusOK || owner.lastRewards == nil || owner.lastRewards.GetStatus() != "dead" ||
|
||||
!strings.Contains(listRecorder.Body.String(), `"max_attempts":8`) || !strings.Contains(listRecorder.Body.String(), `"next_retry_at_ms":0`) {
|
||||
t.Fatalf("dead reward list mismatch: status=%d request=%+v body=%s", listRecorder.Code, owner.lastRewards, listRecorder.Body.String())
|
||||
}
|
||||
|
||||
retryRecorder := httptest.NewRecorder()
|
||||
retryContext, _ := gin.CreateTestContext(retryRecorder)
|
||||
retryContext.Request = httptest.NewRequest(http.MethodPost, "/api/v1/admin/activities/templates/acttpl_1/rewards/job-dead/retry", nil)
|
||||
retryContext.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}, {Key: "reward_job_id", Value: "job-dead"}}
|
||||
retryContext.Set(middleware.ContextUserID, uint(90001))
|
||||
handler.RetryRewardJob(retryContext)
|
||||
if retryRecorder.Code != http.StatusOK || owner.lastRewardRetry == nil || owner.lastRewardRetry.GetRewardJobId() != "job-dead" ||
|
||||
owner.lastRewardRetry.GetOperatorAdminId() != 90001 || !strings.Contains(retryRecorder.Body.String(), `"max_attempts":9`) {
|
||||
t.Fatalf("dead reward revival mismatch: status=%d request=%+v body=%s", retryRecorder.Code, owner.lastRewardRetry, retryRecorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskClaimsFilterEnrichAndRetryPreserveImmutableAttribution(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
rewardItem := &activityv1.ActivityTemplateRewardItem{ItemType: "resource", ResourceId: 91, ResourceType: "avatar_frame", Name: "Frame", Quantity: 1}
|
||||
claim := &activityv1.ActivityTemplateTaskClaim{
|
||||
ClaimId: "claim-1", CommandId: "device-command", TemplateId: "acttpl_1", TemplateCode: "summer",
|
||||
VersionNo: 3, TaskDay: "2026-07-14", TaskKey: "send_gift", UserId: 503,
|
||||
RewardResourceGroupId: 81, WalletCommandId: "wallet-command", WalletGrantId: "wallet-grant",
|
||||
Status: "failed", FailureReason: "timeout", AttemptCount: 2,
|
||||
RewardSnapshotId: "rgs-task", RewardSnapshotHash: strings.Repeat("a", 64), RewardName: "Task reward",
|
||||
RewardItems: []*activityv1.ActivityTemplateRewardItem{rewardItem}, EarningRegionId: 210, AttributionAtMs: 1784073000000,
|
||||
}
|
||||
owner := &activityTemplateDataOwnerClient{
|
||||
claims: &activityv1.ListActivityTemplateTaskClaimsResponse{Claims: []*activityv1.ActivityTemplateTaskClaim{claim}, Total: 1},
|
||||
retryClaim: &activityv1.RetryActivityTemplateTaskClaimResponse{Claim: claim},
|
||||
}
|
||||
users := &captureActivityTemplateUsers{users: map[int64]*userclient.User{
|
||||
503: {UserID: 503, DisplayUserID: "L503", Username: "winner", Avatar: "winner.png"},
|
||||
}}
|
||||
handler := New(owner, nil).BindUserBatchGetter(users)
|
||||
|
||||
listRecorder := httptest.NewRecorder()
|
||||
listContext, _ := gin.CreateTestContext(listRecorder)
|
||||
listContext.Request = httptest.NewRequest(http.MethodGet,
|
||||
"/api/v1/admin/activities/templates/acttpl_1/task-claims?version_no=3&task_day=2026-07-14&task_key=send_gift&user_id=503&status=failed&page=2&page_size=10", nil)
|
||||
listContext.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}
|
||||
handler.TaskClaims(listContext)
|
||||
|
||||
if listRecorder.Code != http.StatusOK || owner.lastClaims == nil || owner.lastClaims.GetVersionNo() != 3 ||
|
||||
owner.lastClaims.GetTaskDay() != "2026-07-14" || owner.lastClaims.GetTaskKey() != "send_gift" ||
|
||||
owner.lastClaims.GetUserId() != 503 || owner.lastClaims.GetStatus() != "failed" || owner.lastClaims.GetPage() != 2 || owner.lastClaims.GetPageSize() != 10 {
|
||||
t.Fatalf("task claim filters mismatch: status=%d request=%+v body=%s", listRecorder.Code, owner.lastClaims, listRecorder.Body.String())
|
||||
}
|
||||
var listBody struct {
|
||||
Data struct {
|
||||
Items []taskClaimDTO `json:"items"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(listRecorder.Body.Bytes(), &listBody); err != nil || len(listBody.Data.Items) != 1 {
|
||||
t.Fatalf("decode task claims: err=%v body=%s", err, listRecorder.Body.String())
|
||||
}
|
||||
item := listBody.Data.Items[0]
|
||||
if item.RewardSnapshotHash != strings.Repeat("a", 64) || item.RewardName != "Task reward" || len(item.RewardItems) != 1 ||
|
||||
item.EarningRegionID != 210 || item.AttributionAtMS != 1784073000000 || item.User == nil || item.User.DisplayUserID != "L503" {
|
||||
t.Fatalf("task claim immutable/admin view mismatch: %+v", item)
|
||||
}
|
||||
|
||||
retryRecorder := httptest.NewRecorder()
|
||||
retryContext, _ := gin.CreateTestContext(retryRecorder)
|
||||
retryContext.Request = httptest.NewRequest(http.MethodPost, "/api/v1/admin/activities/templates/acttpl_1/task-claims/claim-1/retry", nil)
|
||||
retryContext.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}, {Key: "claim_id", Value: "claim-1"}}
|
||||
retryContext.Set(middleware.ContextUserID, uint(90001))
|
||||
handler.RetryTaskClaim(retryContext)
|
||||
if retryRecorder.Code != http.StatusOK || owner.lastRetry == nil || owner.lastRetry.GetTemplateId() != "acttpl_1" ||
|
||||
owner.lastRetry.GetClaimId() != "claim-1" || owner.lastRetry.GetOperatorAdminId() != 90001 {
|
||||
t.Fatalf("task claim retry mismatch: status=%d request=%+v body=%s", retryRecorder.Code, owner.lastRetry, retryRecorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminRewardDTOsExposePublishedSnapshots(t *testing.T) {
|
||||
item := &activityv1.ActivityTemplateRewardItem{ItemType: "wallet_asset", WalletAssetType: "coin", WalletAssetAmount: 500, Name: "Coins"}
|
||||
template := templateFromProto(&activityv1.ActivityTemplate{
|
||||
Tasks: []*activityv1.ActivityTemplateTask{{TaskKey: "visit", RewardSnapshotId: "task-snapshot", RewardSnapshotHash: strings.Repeat("b", 64), RewardName: "Visit reward", RewardItems: []*activityv1.ActivityTemplateRewardItem{item}}},
|
||||
RankRewards: []*activityv1.ActivityTemplateRankReward{{BoardType: "total", RankFrom: 1, RankTo: 1, RewardSnapshotId: "rank-snapshot", RewardSnapshotHash: strings.Repeat("c", 64), RewardName: "Champion", RewardItems: []*activityv1.ActivityTemplateRewardItem{item}}},
|
||||
})
|
||||
job := rewardJobFromProto(&activityv1.ActivityTemplateRewardJob{
|
||||
RewardJobId: "job-1", RewardSnapshotId: "rank-snapshot", RewardSnapshotHash: strings.Repeat("c", 64), RewardName: "Champion", RewardItems: []*activityv1.ActivityTemplateRewardItem{item},
|
||||
}, nil)
|
||||
if len(template.Tasks) != 1 || template.Tasks[0].RewardSnapshotHash != strings.Repeat("b", 64) || len(template.Tasks[0].RewardItems) != 1 ||
|
||||
len(template.RankRewards) != 1 || template.RankRewards[0].RewardSnapshotHash != strings.Repeat("c", 64) || len(template.RankRewards[0].RewardItems) != 1 ||
|
||||
job.RewardSnapshotID != "rank-snapshot" || job.RewardSnapshotHash != strings.Repeat("c", 64) || len(job.RewardItems) != 1 {
|
||||
t.Fatalf("published reward snapshots were dropped: template=%+v job=%+v", template, job)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLeaderboardProfileFailureDoesNotHideRankFacts(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
owner := activityTemplateDataOwnerClient{leaderboard: &activityv1.AdminListActivityTemplateLeaderboardResponse{
|
||||
Period: &activityv1.ActivityTemplateRankPeriod{TemplateId: "acttpl_1", BoardType: "total", PeriodKey: "total"},
|
||||
Entries: []*activityv1.ActivityTemplateLeaderboardEntry{{RankNo: 1, UserId: 501, Score: 900}}, Total: 1,
|
||||
}}
|
||||
users := &captureActivityTemplateUsers{err: errors.New("user service unavailable")}
|
||||
handler := New(&owner, nil).BindUserBatchGetter(users)
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(recorder)
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/activities/templates/acttpl_1/leaderboard?version_no=1&board_type=total", nil)
|
||||
ctx.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}
|
||||
|
||||
handler.Leaderboard(ctx)
|
||||
|
||||
if recorder.Code != http.StatusOK || users.calls != 1 || !strings.Contains(recorder.Body.String(), `"score":900`) || strings.Contains(recorder.Body.String(), `"user":`) {
|
||||
t.Fatalf("profile degradation hid or changed rank facts: status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
130
server/admin/internal/modules/activitytemplate/request.go
Normal file
130
server/admin/internal/modules/activitytemplate/request.go
Normal file
@ -0,0 +1,130 @@
|
||||
package activitytemplate
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
)
|
||||
|
||||
type localeRequest struct {
|
||||
Locale string `json:"locale"`
|
||||
Title string `json:"title"`
|
||||
Rules string `json:"rules"`
|
||||
}
|
||||
|
||||
type giftRequest struct {
|
||||
GiftID string `json:"gift_id"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
Name string `json:"name"`
|
||||
IconURL string `json:"icon_url"`
|
||||
CoinPrice int64 `json:"coin_price"`
|
||||
PriceVersion string `json:"price_version"`
|
||||
}
|
||||
|
||||
type taskRequest struct {
|
||||
TaskKey string `json:"task_key"`
|
||||
TaskType string `json:"task_type"`
|
||||
TargetValue int64 `json:"target_value"`
|
||||
RewardResourceGroupID int64 `json:"reward_resource_group_id"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
}
|
||||
|
||||
type rankRewardRequest struct {
|
||||
BoardType string `json:"board_type"`
|
||||
RankFrom int32 `json:"rank_from"`
|
||||
RankTo int32 `json:"rank_to"`
|
||||
ResourceGroupID int64 `json:"resource_group_id"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
}
|
||||
|
||||
type assetRequest struct {
|
||||
AssetKey string `json:"asset_key"`
|
||||
Locale string `json:"locale"`
|
||||
URL string `json:"url"`
|
||||
MediaType string `json:"media_type"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
}
|
||||
|
||||
// templateRequest 与 HTTP JSON 一一对应;不在 admin 层重复业务校验,确保保存和发布都复用 activity-service 的同一规则。
|
||||
type templateRequest struct {
|
||||
TemplateCode string `json:"template_code"`
|
||||
Name string `json:"name"`
|
||||
ActivityType string `json:"activity_type"`
|
||||
StartMS int64 `json:"start_ms"`
|
||||
EndMS int64 `json:"end_ms"`
|
||||
AllRegions bool `json:"all_regions"`
|
||||
RegionIDs []int64 `json:"region_ids"`
|
||||
Locales []localeRequest `json:"locales"`
|
||||
Gifts []giftRequest `json:"gifts"`
|
||||
Tasks []taskRequest `json:"tasks"`
|
||||
DailyLeaderboardSize int32 `json:"daily_leaderboard_size"`
|
||||
TotalLeaderboardSize int32 `json:"total_leaderboard_size"`
|
||||
RankRewards []rankRewardRequest `json:"rank_rewards"`
|
||||
Assets []assetRequest `json:"assets"`
|
||||
DisplayConfigJSON string `json:"display_config_json"`
|
||||
ExpectedRevision int64 `json:"expected_revision,omitempty"`
|
||||
}
|
||||
|
||||
type statusRequest struct {
|
||||
Status string `json:"status"`
|
||||
ExpectedRevision int64 `json:"expected_revision"`
|
||||
}
|
||||
|
||||
type cloneRequest struct {
|
||||
SourceVersion int64 `json:"source_version"`
|
||||
TemplateCode string `json:"template_code"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type validateRequest struct {
|
||||
Template templateRequest `json:"template"`
|
||||
ForPublish bool `json:"for_publish"`
|
||||
}
|
||||
|
||||
func (r templateRequest) toProto(templateID string) *activityv1.ActivityTemplate {
|
||||
item := &activityv1.ActivityTemplate{
|
||||
TemplateId: strings.TrimSpace(templateID),
|
||||
TemplateCode: strings.TrimSpace(r.TemplateCode),
|
||||
Name: strings.TrimSpace(r.Name),
|
||||
ActivityType: strings.TrimSpace(r.ActivityType),
|
||||
StartMs: r.StartMS,
|
||||
EndMs: r.EndMS,
|
||||
AllRegions: r.AllRegions,
|
||||
RegionIds: append([]int64(nil), r.RegionIDs...),
|
||||
Locales: make([]*activityv1.ActivityTemplateLocale, 0, len(r.Locales)),
|
||||
Gifts: make([]*activityv1.ActivityTemplateGift, 0, len(r.Gifts)),
|
||||
Tasks: make([]*activityv1.ActivityTemplateTask, 0, len(r.Tasks)),
|
||||
DailyLeaderboardSize: r.DailyLeaderboardSize,
|
||||
TotalLeaderboardSize: r.TotalLeaderboardSize,
|
||||
RankRewards: make([]*activityv1.ActivityTemplateRankReward, 0, len(r.RankRewards)),
|
||||
Assets: make([]*activityv1.ActivityTemplateAsset, 0, len(r.Assets)),
|
||||
DisplayConfigJson: strings.TrimSpace(r.DisplayConfigJSON),
|
||||
}
|
||||
for _, locale := range r.Locales {
|
||||
item.Locales = append(item.Locales, &activityv1.ActivityTemplateLocale{Locale: locale.Locale, Title: locale.Title, Rules: locale.Rules})
|
||||
}
|
||||
for _, gift := range r.Gifts {
|
||||
item.Gifts = append(item.Gifts, &activityv1.ActivityTemplateGift{
|
||||
GiftId: gift.GiftID, SortOrder: gift.SortOrder, Name: gift.Name, IconUrl: gift.IconURL,
|
||||
CoinPrice: gift.CoinPrice, PriceVersion: gift.PriceVersion,
|
||||
})
|
||||
}
|
||||
for _, task := range r.Tasks {
|
||||
item.Tasks = append(item.Tasks, &activityv1.ActivityTemplateTask{
|
||||
TaskKey: task.TaskKey, TaskType: task.TaskType, TargetValue: task.TargetValue,
|
||||
RewardResourceGroupId: task.RewardResourceGroupID, SortOrder: task.SortOrder,
|
||||
})
|
||||
}
|
||||
for _, reward := range r.RankRewards {
|
||||
item.RankRewards = append(item.RankRewards, &activityv1.ActivityTemplateRankReward{
|
||||
BoardType: reward.BoardType, RankFrom: reward.RankFrom, RankTo: reward.RankTo,
|
||||
ResourceGroupId: reward.ResourceGroupID, SortOrder: reward.SortOrder,
|
||||
})
|
||||
}
|
||||
for _, asset := range r.Assets {
|
||||
item.Assets = append(item.Assets, &activityv1.ActivityTemplateAsset{
|
||||
AssetKey: asset.AssetKey, Locale: asset.Locale, Url: asset.URL, MediaType: asset.MediaType, SortOrder: asset.SortOrder,
|
||||
})
|
||||
}
|
||||
return item
|
||||
}
|
||||
382
server/admin/internal/modules/activitytemplate/response.go
Normal file
382
server/admin/internal/modules/activitytemplate/response.go
Normal file
@ -0,0 +1,382 @@
|
||||
package activitytemplate
|
||||
|
||||
import (
|
||||
"hyapp-admin-server/internal/integration/userclient"
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
)
|
||||
|
||||
type localeDTO struct {
|
||||
Locale string `json:"locale"`
|
||||
Title string `json:"title"`
|
||||
Rules string `json:"rules"`
|
||||
}
|
||||
|
||||
type giftDTO struct {
|
||||
GiftID string `json:"gift_id"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
Name string `json:"name"`
|
||||
IconURL string `json:"icon_url"`
|
||||
CoinPrice int64 `json:"coin_price"`
|
||||
PriceVersion string `json:"price_version"`
|
||||
}
|
||||
|
||||
// rewardItemDTO is the immutable, display-safe reward item captured at publish
|
||||
// time. Admin reads this snapshot instead of joining mutable wallet catalog rows.
|
||||
type rewardItemDTO struct {
|
||||
ItemType string `json:"item_type"`
|
||||
ResourceID int64 `json:"resource_id"`
|
||||
ResourceType string `json:"resource_type"`
|
||||
Name string `json:"name"`
|
||||
IconURL string `json:"icon_url"`
|
||||
Quantity int64 `json:"quantity"`
|
||||
DurationMS int64 `json:"duration_ms"`
|
||||
WalletAssetType string `json:"wallet_asset_type"`
|
||||
WalletAssetAmount int64 `json:"wallet_asset_amount"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
}
|
||||
|
||||
type taskDTO struct {
|
||||
TaskKey string `json:"task_key"`
|
||||
TaskType string `json:"task_type"`
|
||||
TargetValue int64 `json:"target_value"`
|
||||
RewardResourceGroupID int64 `json:"reward_resource_group_id"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
RewardSnapshotID string `json:"reward_snapshot_id"`
|
||||
RewardSnapshotHash string `json:"reward_snapshot_hash"`
|
||||
RewardName string `json:"reward_name"`
|
||||
RewardItems []rewardItemDTO `json:"reward_items"`
|
||||
}
|
||||
|
||||
type rankRewardDTO struct {
|
||||
BoardType string `json:"board_type"`
|
||||
RankFrom int32 `json:"rank_from"`
|
||||
RankTo int32 `json:"rank_to"`
|
||||
ResourceGroupID int64 `json:"resource_group_id"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
RewardSnapshotID string `json:"reward_snapshot_id"`
|
||||
RewardSnapshotHash string `json:"reward_snapshot_hash"`
|
||||
RewardName string `json:"reward_name"`
|
||||
RewardItems []rewardItemDTO `json:"reward_items"`
|
||||
}
|
||||
|
||||
type assetDTO struct {
|
||||
AssetKey string `json:"asset_key"`
|
||||
Locale string `json:"locale"`
|
||||
URL string `json:"url"`
|
||||
MediaType string `json:"media_type"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
}
|
||||
|
||||
type templateSummaryDTO struct {
|
||||
AppCode string `json:"app_code"`
|
||||
TemplateID string `json:"template_id"`
|
||||
TemplateCode string `json:"template_code"`
|
||||
Name string `json:"name"`
|
||||
ActivityType string `json:"activity_type"`
|
||||
Status string `json:"status"`
|
||||
LifecycleStatus string `json:"lifecycle_status"`
|
||||
StartMS int64 `json:"start_ms"`
|
||||
EndMS int64 `json:"end_ms"`
|
||||
AllRegions bool `json:"all_regions"`
|
||||
RegionIDs []int64 `json:"region_ids"`
|
||||
Revision int64 `json:"revision"`
|
||||
PublishedVersion int64 `json:"published_version"`
|
||||
CreatedByAdminID int64 `json:"created_by_admin_id"`
|
||||
UpdatedByAdminID int64 `json:"updated_by_admin_id"`
|
||||
CreatedAtMS int64 `json:"created_at_ms"`
|
||||
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||
}
|
||||
|
||||
type templateDTO struct {
|
||||
AppCode string `json:"app_code"`
|
||||
TemplateID string `json:"template_id"`
|
||||
TemplateCode string `json:"template_code"`
|
||||
Name string `json:"name"`
|
||||
ActivityType string `json:"activity_type"`
|
||||
Status string `json:"status"`
|
||||
LifecycleStatus string `json:"lifecycle_status"`
|
||||
StartMS int64 `json:"start_ms"`
|
||||
EndMS int64 `json:"end_ms"`
|
||||
AllRegions bool `json:"all_regions"`
|
||||
RegionIDs []int64 `json:"region_ids"`
|
||||
Locales []localeDTO `json:"locales"`
|
||||
Gifts []giftDTO `json:"gifts"`
|
||||
Tasks []taskDTO `json:"tasks"`
|
||||
DailyLeaderboardSize int32 `json:"daily_leaderboard_size"`
|
||||
TotalLeaderboardSize int32 `json:"total_leaderboard_size"`
|
||||
RankRewards []rankRewardDTO `json:"rank_rewards"`
|
||||
Assets []assetDTO `json:"assets"`
|
||||
DisplayConfigJSON string `json:"display_config_json"`
|
||||
Revision int64 `json:"revision"`
|
||||
PublishedVersion int64 `json:"published_version"`
|
||||
CreatedByAdminID int64 `json:"created_by_admin_id"`
|
||||
UpdatedByAdminID int64 `json:"updated_by_admin_id"`
|
||||
PublishedByAdminID int64 `json:"published_by_admin_id"`
|
||||
CreatedAtMS int64 `json:"created_at_ms"`
|
||||
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||
PublishedAtMS int64 `json:"published_at_ms"`
|
||||
ArchivedAtMS int64 `json:"archived_at_ms"`
|
||||
}
|
||||
|
||||
type versionDTO struct {
|
||||
TemplateID string `json:"template_id"`
|
||||
VersionNo int64 `json:"version_no"`
|
||||
Snapshot templateDTO `json:"snapshot"`
|
||||
PublishedByAdminID int64 `json:"published_by_admin_id"`
|
||||
PublishedAtMS int64 `json:"published_at_ms"`
|
||||
RuntimeFromMS int64 `json:"runtime_from_ms"`
|
||||
RuntimeToMS int64 `json:"runtime_to_ms"`
|
||||
}
|
||||
|
||||
type validationIssueDTO struct {
|
||||
Field string `json:"field"`
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type pageDTO struct {
|
||||
Items any `json:"items"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Total int64 `json:"total"`
|
||||
ServerTimeMS int64 `json:"server_time_ms,omitempty"`
|
||||
}
|
||||
|
||||
type rankPeriodDTO struct {
|
||||
TemplateID string `json:"template_id"`
|
||||
TemplateCode string `json:"template_code"`
|
||||
VersionNo int64 `json:"version_no"`
|
||||
BoardType string `json:"board_type"`
|
||||
PeriodKey string `json:"period_key"`
|
||||
PeriodStartMS int64 `json:"period_start_ms"`
|
||||
PeriodEndMS int64 `json:"period_end_ms"`
|
||||
SettleAfterMS int64 `json:"settle_after_ms"`
|
||||
Status string `json:"status"`
|
||||
SettledAtMS int64 `json:"settled_at_ms"`
|
||||
}
|
||||
|
||||
type leaderboardEntryDTO struct {
|
||||
RankNo int32 `json:"rank_no"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Score int64 `json:"score"`
|
||||
GiftCount int64 `json:"gift_count"`
|
||||
CoinAmount int64 `json:"coin_amount"`
|
||||
FirstScoredAtMS int64 `json:"first_scored_at_ms"`
|
||||
Snapshot bool `json:"snapshot"`
|
||||
User *activityTemplateUserDTO `json:"user,omitempty"`
|
||||
}
|
||||
|
||||
type leaderboardPageDTO struct {
|
||||
Period rankPeriodDTO `json:"period"`
|
||||
Items []leaderboardEntryDTO `json:"items"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
type rewardJobDTO struct {
|
||||
RewardJobID string `json:"reward_job_id"`
|
||||
TemplateID string `json:"template_id"`
|
||||
TemplateCode string `json:"template_code"`
|
||||
VersionNo int64 `json:"version_no"`
|
||||
BoardType string `json:"board_type"`
|
||||
PeriodKey string `json:"period_key"`
|
||||
RankNo int32 `json:"rank_no"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Score int64 `json:"score"`
|
||||
ResourceGroupID int64 `json:"resource_group_id"`
|
||||
WalletCommandID string `json:"wallet_command_id"`
|
||||
WalletGrantID string `json:"wallet_grant_id"`
|
||||
Status string `json:"status"`
|
||||
FailureReason string `json:"failure_reason"`
|
||||
AttemptCount int32 `json:"attempt_count"`
|
||||
MaxAttempts int32 `json:"max_attempts"`
|
||||
NextRetryAtMS int64 `json:"next_retry_at_ms"`
|
||||
CreatedAtMS int64 `json:"created_at_ms"`
|
||||
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||
GrantedAtMS int64 `json:"granted_at_ms"`
|
||||
RewardSnapshotID string `json:"reward_snapshot_id"`
|
||||
RewardSnapshotHash string `json:"reward_snapshot_hash"`
|
||||
RewardName string `json:"reward_name"`
|
||||
RewardItems []rewardItemDTO `json:"reward_items"`
|
||||
User *activityTemplateUserDTO `json:"user,omitempty"`
|
||||
}
|
||||
|
||||
type taskClaimDTO struct {
|
||||
ClaimID string `json:"claim_id"`
|
||||
CommandID string `json:"command_id"`
|
||||
TemplateID string `json:"template_id"`
|
||||
TemplateCode string `json:"template_code"`
|
||||
VersionNo int64 `json:"version_no"`
|
||||
TaskDay string `json:"task_day"`
|
||||
TaskKey string `json:"task_key"`
|
||||
UserID int64 `json:"user_id"`
|
||||
RewardResourceGroupID int64 `json:"reward_resource_group_id"`
|
||||
WalletCommandID string `json:"wallet_command_id"`
|
||||
WalletGrantID string `json:"wallet_grant_id"`
|
||||
Status string `json:"status"`
|
||||
FailureReason string `json:"failure_reason"`
|
||||
AttemptCount int32 `json:"attempt_count"`
|
||||
CreatedAtMS int64 `json:"created_at_ms"`
|
||||
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||
GrantedAtMS int64 `json:"granted_at_ms"`
|
||||
RewardSnapshotID string `json:"reward_snapshot_id"`
|
||||
RewardSnapshotHash string `json:"reward_snapshot_hash"`
|
||||
RewardName string `json:"reward_name"`
|
||||
RewardItems []rewardItemDTO `json:"reward_items"`
|
||||
EarningRegionID int64 `json:"earning_region_id"`
|
||||
AttributionAtMS int64 `json:"attribution_at_ms"`
|
||||
User *activityTemplateUserDTO `json:"user,omitempty"`
|
||||
}
|
||||
|
||||
type activityTemplateUserDTO struct {
|
||||
DisplayUserID string `json:"display_user_id"`
|
||||
Username string `json:"username"`
|
||||
Avatar string `json:"avatar"`
|
||||
}
|
||||
|
||||
func rankPeriodFromProto(item *activityv1.ActivityTemplateRankPeriod) rankPeriodDTO {
|
||||
if item == nil {
|
||||
return rankPeriodDTO{}
|
||||
}
|
||||
return rankPeriodDTO{
|
||||
TemplateID: item.GetTemplateId(), TemplateCode: item.GetTemplateCode(), VersionNo: item.GetVersionNo(),
|
||||
BoardType: item.GetBoardType(), PeriodKey: item.GetPeriodKey(), PeriodStartMS: item.GetPeriodStartMs(),
|
||||
PeriodEndMS: item.GetPeriodEndMs(), SettleAfterMS: item.GetSettleAfterMs(),
|
||||
Status: item.GetStatus(), SettledAtMS: item.GetSettledAtMs(),
|
||||
}
|
||||
}
|
||||
|
||||
func leaderboardEntryFromProto(item *activityv1.ActivityTemplateLeaderboardEntry, users map[int64]*userclient.User) leaderboardEntryDTO {
|
||||
if item == nil {
|
||||
return leaderboardEntryDTO{}
|
||||
}
|
||||
out := leaderboardEntryDTO{
|
||||
RankNo: item.GetRankNo(), UserID: item.GetUserId(), Score: item.GetScore(), GiftCount: item.GetGiftCount(),
|
||||
CoinAmount: item.GetCoinAmount(), FirstScoredAtMS: item.GetFirstScoredAtMs(), Snapshot: item.GetSnapshot(),
|
||||
}
|
||||
out.User = activityTemplateUserFromClient(users[item.GetUserId()])
|
||||
return out
|
||||
}
|
||||
|
||||
func rewardJobFromProto(item *activityv1.ActivityTemplateRewardJob, users map[int64]*userclient.User) rewardJobDTO {
|
||||
if item == nil {
|
||||
return rewardJobDTO{}
|
||||
}
|
||||
out := rewardJobDTO{
|
||||
RewardJobID: item.GetRewardJobId(), TemplateID: item.GetTemplateId(), TemplateCode: item.GetTemplateCode(),
|
||||
VersionNo: item.GetVersionNo(), BoardType: item.GetBoardType(), PeriodKey: item.GetPeriodKey(), RankNo: item.GetRankNo(),
|
||||
UserID: item.GetUserId(), Score: item.GetScore(), ResourceGroupID: item.GetResourceGroupId(),
|
||||
WalletCommandID: item.GetWalletCommandId(), WalletGrantID: item.GetWalletGrantId(), Status: item.GetStatus(),
|
||||
FailureReason: item.GetFailureReason(), AttemptCount: item.GetAttemptCount(), MaxAttempts: item.GetMaxAttempts(),
|
||||
NextRetryAtMS: item.GetNextRetryAtMs(), CreatedAtMS: item.GetCreatedAtMs(),
|
||||
UpdatedAtMS: item.GetUpdatedAtMs(), GrantedAtMS: item.GetGrantedAtMs(),
|
||||
RewardSnapshotID: item.GetRewardSnapshotId(), RewardSnapshotHash: item.GetRewardSnapshotHash(),
|
||||
RewardName: item.GetRewardName(), RewardItems: rewardItemsFromProto(item.GetRewardItems()),
|
||||
}
|
||||
out.User = activityTemplateUserFromClient(users[item.GetUserId()])
|
||||
return out
|
||||
}
|
||||
|
||||
func taskClaimFromProto(item *activityv1.ActivityTemplateTaskClaim, users map[int64]*userclient.User) taskClaimDTO {
|
||||
if item == nil {
|
||||
return taskClaimDTO{RewardItems: []rewardItemDTO{}}
|
||||
}
|
||||
out := taskClaimDTO{
|
||||
ClaimID: item.GetClaimId(), CommandID: item.GetCommandId(), TemplateID: item.GetTemplateId(), TemplateCode: item.GetTemplateCode(),
|
||||
VersionNo: item.GetVersionNo(), TaskDay: item.GetTaskDay(), TaskKey: item.GetTaskKey(), UserID: item.GetUserId(),
|
||||
RewardResourceGroupID: item.GetRewardResourceGroupId(), WalletCommandID: item.GetWalletCommandId(), WalletGrantID: item.GetWalletGrantId(),
|
||||
Status: item.GetStatus(), FailureReason: item.GetFailureReason(), AttemptCount: item.GetAttemptCount(),
|
||||
CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), GrantedAtMS: item.GetGrantedAtMs(),
|
||||
RewardSnapshotID: item.GetRewardSnapshotId(), RewardSnapshotHash: item.GetRewardSnapshotHash(), RewardName: item.GetRewardName(),
|
||||
RewardItems: rewardItemsFromProto(item.GetRewardItems()), EarningRegionID: item.GetEarningRegionId(), AttributionAtMS: item.GetAttributionAtMs(),
|
||||
}
|
||||
out.User = activityTemplateUserFromClient(users[item.GetUserId()])
|
||||
return out
|
||||
}
|
||||
|
||||
func rewardItemsFromProto(items []*activityv1.ActivityTemplateRewardItem) []rewardItemDTO {
|
||||
out := make([]rewardItemDTO, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, rewardItemDTO{
|
||||
ItemType: item.GetItemType(), ResourceID: item.GetResourceId(), ResourceType: item.GetResourceType(),
|
||||
Name: item.GetName(), IconURL: item.GetIconUrl(), Quantity: item.GetQuantity(), DurationMS: item.GetDurationMs(),
|
||||
WalletAssetType: item.GetWalletAssetType(), WalletAssetAmount: item.GetWalletAssetAmount(), SortOrder: item.GetSortOrder(),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func activityTemplateUserFromClient(item *userclient.User) *activityTemplateUserDTO {
|
||||
if item == nil {
|
||||
return nil
|
||||
}
|
||||
return &activityTemplateUserDTO{DisplayUserID: item.DisplayUserID, Username: item.Username, Avatar: item.Avatar}
|
||||
}
|
||||
|
||||
func summaryFromProto(item *activityv1.ActivityTemplateSummary) templateSummaryDTO {
|
||||
if item == nil {
|
||||
return templateSummaryDTO{RegionIDs: []int64{}}
|
||||
}
|
||||
return templateSummaryDTO{
|
||||
AppCode: item.GetAppCode(), TemplateID: item.GetTemplateId(), TemplateCode: item.GetTemplateCode(),
|
||||
Name: item.GetName(), ActivityType: item.GetActivityType(), Status: item.GetStatus(), LifecycleStatus: item.GetLifecycleStatus(),
|
||||
StartMS: item.GetStartMs(), EndMS: item.GetEndMs(), AllRegions: item.GetAllRegions(), RegionIDs: cloneInt64Slice(item.GetRegionIds()),
|
||||
Revision: item.GetRevision(), PublishedVersion: item.GetPublishedVersion(),
|
||||
CreatedByAdminID: item.GetCreatedByAdminId(), UpdatedByAdminID: item.GetUpdatedByAdminId(),
|
||||
CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(),
|
||||
}
|
||||
}
|
||||
|
||||
func templateFromProto(item *activityv1.ActivityTemplate) templateDTO {
|
||||
if item == nil {
|
||||
return templateDTO{RegionIDs: []int64{}, Locales: []localeDTO{}, Gifts: []giftDTO{}, Tasks: []taskDTO{}, RankRewards: []rankRewardDTO{}, Assets: []assetDTO{}}
|
||||
}
|
||||
out := templateDTO{
|
||||
AppCode: item.GetAppCode(), TemplateID: item.GetTemplateId(), TemplateCode: item.GetTemplateCode(), Name: item.GetName(),
|
||||
ActivityType: item.GetActivityType(), Status: item.GetStatus(), LifecycleStatus: item.GetLifecycleStatus(),
|
||||
StartMS: item.GetStartMs(), EndMS: item.GetEndMs(), AllRegions: item.GetAllRegions(), RegionIDs: cloneInt64Slice(item.GetRegionIds()),
|
||||
Locales: make([]localeDTO, 0, len(item.GetLocales())), Gifts: make([]giftDTO, 0, len(item.GetGifts())),
|
||||
Tasks: make([]taskDTO, 0, len(item.GetTasks())), RankRewards: make([]rankRewardDTO, 0, len(item.GetRankRewards())),
|
||||
Assets: make([]assetDTO, 0, len(item.GetAssets())), DailyLeaderboardSize: item.GetDailyLeaderboardSize(),
|
||||
TotalLeaderboardSize: item.GetTotalLeaderboardSize(), DisplayConfigJSON: item.GetDisplayConfigJson(),
|
||||
Revision: item.GetRevision(), PublishedVersion: item.GetPublishedVersion(),
|
||||
CreatedByAdminID: item.GetCreatedByAdminId(), UpdatedByAdminID: item.GetUpdatedByAdminId(), PublishedByAdminID: item.GetPublishedByAdminId(),
|
||||
CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), PublishedAtMS: item.GetPublishedAtMs(), ArchivedAtMS: item.GetArchivedAtMs(),
|
||||
}
|
||||
for _, locale := range item.GetLocales() {
|
||||
out.Locales = append(out.Locales, localeDTO{Locale: locale.GetLocale(), Title: locale.GetTitle(), Rules: locale.GetRules()})
|
||||
}
|
||||
for _, gift := range item.GetGifts() {
|
||||
out.Gifts = append(out.Gifts, giftDTO{GiftID: gift.GetGiftId(), SortOrder: gift.GetSortOrder(), Name: gift.GetName(), IconURL: gift.GetIconUrl(), CoinPrice: gift.GetCoinPrice(), PriceVersion: gift.GetPriceVersion()})
|
||||
}
|
||||
for _, task := range item.GetTasks() {
|
||||
out.Tasks = append(out.Tasks, taskDTO{
|
||||
TaskKey: task.GetTaskKey(), TaskType: task.GetTaskType(), TargetValue: task.GetTargetValue(),
|
||||
RewardResourceGroupID: task.GetRewardResourceGroupId(), SortOrder: task.GetSortOrder(),
|
||||
RewardSnapshotID: task.GetRewardSnapshotId(), RewardSnapshotHash: task.GetRewardSnapshotHash(),
|
||||
RewardName: task.GetRewardName(), RewardItems: rewardItemsFromProto(task.GetRewardItems()),
|
||||
})
|
||||
}
|
||||
for _, reward := range item.GetRankRewards() {
|
||||
out.RankRewards = append(out.RankRewards, rankRewardDTO{
|
||||
BoardType: reward.GetBoardType(), RankFrom: reward.GetRankFrom(), RankTo: reward.GetRankTo(),
|
||||
ResourceGroupID: reward.GetResourceGroupId(), SortOrder: reward.GetSortOrder(),
|
||||
RewardSnapshotID: reward.GetRewardSnapshotId(), RewardSnapshotHash: reward.GetRewardSnapshotHash(),
|
||||
RewardName: reward.GetRewardName(), RewardItems: rewardItemsFromProto(reward.GetRewardItems()),
|
||||
})
|
||||
}
|
||||
for _, asset := range item.GetAssets() {
|
||||
out.Assets = append(out.Assets, assetDTO{AssetKey: asset.GetAssetKey(), Locale: asset.GetLocale(), URL: asset.GetUrl(), MediaType: asset.GetMediaType(), SortOrder: asset.GetSortOrder()})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneInt64Slice(items []int64) []int64 {
|
||||
out := make([]int64, len(items))
|
||||
copy(out, items)
|
||||
return out
|
||||
}
|
||||
30
server/admin/internal/modules/activitytemplate/routes.go
Normal file
30
server/admin/internal/modules/activitytemplate/routes.go
Normal file
@ -0,0 +1,30 @@
|
||||
package activitytemplate
|
||||
|
||||
import (
|
||||
"hyapp-admin-server/internal/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
|
||||
if h == nil {
|
||||
return
|
||||
}
|
||||
protected.GET("/admin/activities/templates", middleware.RequirePermission("activity-template:view"), h.List)
|
||||
protected.POST("/admin/activities/templates", middleware.RequirePermission("activity-template:create"), h.Create)
|
||||
protected.POST("/admin/activities/templates/validate", middleware.RequireAnyPermission("activity-template:create", "activity-template:update", "activity-template:publish"), h.Validate)
|
||||
protected.GET("/admin/activities/templates/:template_id", middleware.RequirePermission("activity-template:view"), h.Get)
|
||||
protected.PUT("/admin/activities/templates/:template_id", middleware.RequirePermission("activity-template:update"), h.Update)
|
||||
protected.PATCH("/admin/activities/templates/:template_id/status", middleware.RequirePermission("activity-template:publish"), h.SetStatus)
|
||||
protected.DELETE("/admin/activities/templates/:template_id", middleware.RequirePermission("activity-template:delete"), h.Delete)
|
||||
protected.GET("/admin/activities/templates/:template_id/versions", middleware.RequirePermission("activity-template:view"), h.Versions)
|
||||
protected.POST("/admin/activities/templates/:template_id/clone", middleware.RequirePermission("activity-template:create"), h.Clone)
|
||||
protected.GET("/admin/activities/templates/:template_id/leaderboard", middleware.RequirePermission("activity-template:data"), h.Leaderboard)
|
||||
protected.GET("/admin/activities/templates/:template_id/data", middleware.RequirePermission("activity-template:data"), h.Data)
|
||||
// 导出复用同一受限聚合契约,前端按 overview/trend/gifts/tasks 分区生成 CSV;避免另开一条可能绕过 366 天和 Top100 限制的数据链路。
|
||||
protected.GET("/admin/activities/templates/:template_id/data/export", middleware.RequirePermission("activity-template:export"), h.Data)
|
||||
protected.GET("/admin/activities/templates/:template_id/rewards", middleware.RequirePermission("activity-template:data"), h.RewardJobs)
|
||||
protected.POST("/admin/activities/templates/:template_id/rewards/:reward_job_id/retry", middleware.RequirePermission("activity-template:retry"), h.RetryRewardJob)
|
||||
protected.GET("/admin/activities/templates/:template_id/task-claims", middleware.RequirePermission("activity-template:data"), h.TaskClaims)
|
||||
protected.POST("/admin/activities/templates/:template_id/task-claims/:claim_id/retry", middleware.RequirePermission("activity-template:retry"), h.RetryTaskClaim)
|
||||
}
|
||||
@ -0,0 +1,92 @@
|
||||
package activitytemplate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"hyapp-admin-server/internal/integration/activityclient"
|
||||
"hyapp-admin-server/internal/middleware"
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type validatePermissionClient struct {
|
||||
activityclient.Client
|
||||
}
|
||||
|
||||
func (validatePermissionClient) ValidateActivityTemplate(context.Context, *activityv1.ValidateActivityTemplateRequest) (*activityv1.ValidateActivityTemplateResponse, error) {
|
||||
return &activityv1.ValidateActivityTemplateResponse{Valid: true}, nil
|
||||
}
|
||||
|
||||
func TestRegisterRoutesExposesActivityTemplateContract(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
engine := gin.New()
|
||||
RegisterRoutes(engine.Group("/api/v1"), &Handler{})
|
||||
|
||||
actual := make(map[string]struct{})
|
||||
for _, route := range engine.Routes() {
|
||||
actual[route.Method+" "+route.Path] = struct{}{}
|
||||
}
|
||||
expected := []string{
|
||||
"GET /api/v1/admin/activities/templates",
|
||||
"POST /api/v1/admin/activities/templates",
|
||||
"POST /api/v1/admin/activities/templates/validate",
|
||||
"GET /api/v1/admin/activities/templates/:template_id",
|
||||
"PUT /api/v1/admin/activities/templates/:template_id",
|
||||
"PATCH /api/v1/admin/activities/templates/:template_id/status",
|
||||
"DELETE /api/v1/admin/activities/templates/:template_id",
|
||||
"GET /api/v1/admin/activities/templates/:template_id/versions",
|
||||
"POST /api/v1/admin/activities/templates/:template_id/clone",
|
||||
"GET /api/v1/admin/activities/templates/:template_id/leaderboard",
|
||||
"GET /api/v1/admin/activities/templates/:template_id/data",
|
||||
"GET /api/v1/admin/activities/templates/:template_id/data/export",
|
||||
"GET /api/v1/admin/activities/templates/:template_id/rewards",
|
||||
"POST /api/v1/admin/activities/templates/:template_id/rewards/:reward_job_id/retry",
|
||||
"GET /api/v1/admin/activities/templates/:template_id/task-claims",
|
||||
"POST /api/v1/admin/activities/templates/:template_id/task-claims/:claim_id/retry",
|
||||
}
|
||||
for _, route := range expected {
|
||||
if _, ok := actual[route]; !ok {
|
||||
t.Errorf("missing route %s; actual=%v", route, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRouteAcceptsCreateUpdateOrPublishPermission(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
tests := []struct {
|
||||
permission string
|
||||
wantStatus int
|
||||
}{
|
||||
{permission: "activity-template:create", wantStatus: http.StatusOK},
|
||||
{permission: "activity-template:update", wantStatus: http.StatusOK},
|
||||
{permission: "activity-template:publish", wantStatus: http.StatusOK},
|
||||
{permission: "activity-template:view", wantStatus: http.StatusForbidden},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.permission, func(t *testing.T) {
|
||||
engine := gin.New()
|
||||
engine.Use(func(c *gin.Context) {
|
||||
c.Set(middleware.ContextPermissions, []string{tt.permission})
|
||||
c.Next()
|
||||
})
|
||||
RegisterRoutes(engine.Group("/api/v1"), New(validatePermissionClient{}, nil))
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/admin/activities/templates/validate", strings.NewReader(`{
|
||||
"template":{"template_code":"gift_challenge_2026","name":"Gift Challenge"},
|
||||
"for_publish":false
|
||||
}`))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
engine.ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != tt.wantStatus {
|
||||
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, tt.wantStatus, recorder.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -18,7 +18,8 @@ func TestH5AppScopeSchemaMigrationCreatesScopedKeys(t *testing.T) {
|
||||
for _, snippet := range []string{
|
||||
"app_code VARCHAR(32) NOT NULL DEFAULT ''",
|
||||
"is_deleted BOOLEAN NOT NULL DEFAULT FALSE",
|
||||
"DROP INDEX uk_admin_app_configs_group_key",
|
||||
"INDEX_NAME = 'uk_admin_app_configs_group_key'",
|
||||
"INDEX_NAME = 'idx_admin_app_config_group_key'",
|
||||
"uk_admin_app_config_app_group_key (app_code, `group`, `key`)",
|
||||
} {
|
||||
if !strings.Contains(sqlText, snippet) {
|
||||
|
||||
@ -124,12 +124,15 @@ var (
|
||||
}
|
||||
|
||||
activityRead = []string{
|
||||
"activity-template:view", "activity-template:data",
|
||||
"daily-task:view", "first-recharge-reward:view", "cumulative-recharge-reward:view",
|
||||
"invite-activity-reward:view", "seven-day-checkin:view", "room-rocket:view", "room-turnover-reward:view",
|
||||
"wheel:view", "weekly-star:view", "agency-opening:view", "user-leaderboard:view", "red-packet:view",
|
||||
"cp-config:view", "cp-weekly-rank:view", "vip-config:view", "achievement:view",
|
||||
}
|
||||
activityManage = []string{
|
||||
"activity-template:create", "activity-template:update", "activity-template:publish", "activity-template:delete",
|
||||
"activity-template:export", "activity-template:retry",
|
||||
"daily-task:create", "daily-task:update", "daily-task:status",
|
||||
"first-recharge-reward:update", "cumulative-recharge-reward:update", "invite-activity-reward:update",
|
||||
"seven-day-checkin:update", "room-rocket:update", "room-turnover-reward:update", "room-turnover-reward:retry",
|
||||
|
||||
@ -173,6 +173,14 @@ var defaultPermissions = []model.Permission{
|
||||
{Name: "每日任务创建", Code: "daily-task:create", Kind: "button"},
|
||||
{Name: "每日任务更新", Code: "daily-task:update", Kind: "button"},
|
||||
{Name: "每日任务状态", Code: "daily-task:status", Kind: "button"},
|
||||
{Name: "活动模版查看", Code: "activity-template:view", Kind: "menu"},
|
||||
{Name: "活动模版创建", Code: "activity-template:create", Kind: "button"},
|
||||
{Name: "活动模版更新", Code: "activity-template:update", Kind: "button"},
|
||||
{Name: "活动模版发布", Code: "activity-template:publish", Kind: "button"},
|
||||
{Name: "活动模版删除", Code: "activity-template:delete", Kind: "button"},
|
||||
{Name: "活动模版数据", Code: "activity-template:data", Kind: "button"},
|
||||
{Name: "活动模版导出", Code: "activity-template:export", Kind: "button"},
|
||||
{Name: "活动模版重试", Code: "activity-template:retry", Kind: "button"},
|
||||
{Name: "成就配置查看", Code: "achievement:view", Kind: "menu"},
|
||||
{Name: "成就配置创建", Code: "achievement:create", Kind: "button"},
|
||||
{Name: "成就配置更新", Code: "achievement:update", Kind: "button"},
|
||||
@ -270,7 +278,7 @@ func (s *Store) seedMenus() error {
|
||||
{Title: "资源管理", Code: "resources", Path: "", Icon: "inventory", PermissionCode: "", Sort: 60, Visible: true},
|
||||
{Title: "运营管理", Code: "operations", Path: "", Icon: "operations", PermissionCode: "", Sort: 70, Visible: true},
|
||||
{Title: "支付管理", Code: "payment", Path: "", Icon: "wallet", PermissionCode: "", Sort: 80, Visible: true},
|
||||
{Title: "活动管理", Code: "activities", Path: "", Icon: "campaign", PermissionCode: "", Sort: 90, Visible: true},
|
||||
{Title: "运营活动", Code: "activities", Path: "", Icon: "campaign", PermissionCode: "", Sort: 90, Visible: true},
|
||||
{Title: "游戏管理", Code: "games", Path: "", Icon: "sports_esports", PermissionCode: "", Sort: 100, Visible: true},
|
||||
{Title: "地区管理", Code: "geo", Path: "", Icon: "map", PermissionCode: "", Sort: 110, Visible: true},
|
||||
{Title: "后台设置", Code: "system", Path: "", Icon: "settings", PermissionCode: "", Sort: 120, Visible: true},
|
||||
@ -361,6 +369,7 @@ func (s *Store) seedMenus() error {
|
||||
{ParentID: &paymentID, Title: "三方临时支付链接", Code: "payment-temporary-links", Path: "/payment/temporary-links", Icon: "receipt", PermissionCode: "payment-temporary-link:view", Sort: 70, Visible: true},
|
||||
{ParentID: &paymentID, Title: "支付内购商品", Code: "payment-recharge-products", Path: "/payment/recharge-products", Icon: "wallet", PermissionCode: "payment-product:view", Sort: 71, Visible: true},
|
||||
{ParentID: &paymentID, Title: "币商充值订单", Code: "finance-coin-seller-recharge-orders", Path: "/finance/orders/coin-seller-recharges", Icon: "receipt", PermissionCode: "finance-order:coin-seller-recharge:view", Sort: 72, Visible: true},
|
||||
{ParentID: &activityID, Title: "活动模版", Code: "activity-template", Path: "/activities/templates", Icon: "campaign", PermissionCode: "activity-template:view", Sort: 68, Visible: true},
|
||||
{ParentID: &activityID, Title: "每日任务", Code: "daily-task-list", Path: "/activities/daily-tasks", Icon: "task", PermissionCode: "daily-task:view", Sort: 69, Visible: true},
|
||||
{ParentID: &activityID, Title: "注册奖励", Code: "registration-reward", Path: "/activities/registration-reward", Icon: "gift", PermissionCode: "registration-reward:view", Sort: 70, Visible: true},
|
||||
{ParentID: &activityID, Title: "成就配置", Code: "achievement-config", Path: "/activities/achievements", Icon: "military_tech", PermissionCode: "achievement:view", Sort: 71, Visible: true},
|
||||
|
||||
@ -201,6 +201,26 @@ func TestGiftRecordPermissionMigrationExtendsOperationsReadRoles(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityTemplatePermissionMigrationExtendsManagedRoles(t *testing.T) {
|
||||
content, err := os.ReadFile("../../migrations/096_activity_template_navigation.sql")
|
||||
if err != nil {
|
||||
t.Fatalf("read activity template permission migration: %v", err)
|
||||
}
|
||||
sqlText := string(content)
|
||||
for _, token := range []string{
|
||||
"'活动模版'", "'activity-template'", "'/activities/templates'",
|
||||
"'platform-admin', 'ops-admin', 'product-lead'",
|
||||
"'operations-specialist', 'product-specialist', 'auditor', 'readonly'",
|
||||
"'activity-template:view'", "'activity-template:create'", "'activity-template:update'",
|
||||
"'activity-template:publish'", "'activity-template:delete'", "'activity-template:data'",
|
||||
"'activity-template:export'", "'activity-template:retry'",
|
||||
} {
|
||||
if !strings.Contains(sqlText, token) {
|
||||
t.Fatalf("activity template migration missing %s", token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertMigrationPermissionCodes(t *testing.T, roleCode string, sqlList string) {
|
||||
t.Helper()
|
||||
quotedCode := regexp.MustCompile(`'([a-z0-9:-]+)'`)
|
||||
@ -210,9 +230,13 @@ func assertMigrationPermissionCodes(t *testing.T, roleCode string, sqlList strin
|
||||
actual = append(actual, match[1])
|
||||
}
|
||||
expected := append([]string(nil), defaultRolePermissionCodes(roleCode)...)
|
||||
// 093 已在生产执行,不能为了新增页面回写旧迁移;095 以增量方式给同一批运营只读岗位补 gift-record:view。
|
||||
// 这里仍校验 093 的原始精确矩阵,新权限由上面的 095 专项测试锁定。
|
||||
expected = stringSetDifference(expected, []string{"gift-record:view"})
|
||||
// 093 已在生产执行,不能为了新增页面回写旧迁移;095/096 以增量方式补充页面权限。
|
||||
// 这里仍校验 093 的原始精确矩阵,新权限由上面的专项迁移测试锁定。
|
||||
expected = stringSetDifference(expected, []string{
|
||||
"gift-record:view",
|
||||
"activity-template:view", "activity-template:create", "activity-template:update", "activity-template:publish",
|
||||
"activity-template:delete", "activity-template:data", "activity-template:export", "activity-template:retry",
|
||||
})
|
||||
sort.Strings(actual)
|
||||
sort.Strings(expected)
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
|
||||
@ -6,5 +6,6 @@ const (
|
||||
CodeUnauthorized = 40100
|
||||
CodeForbidden = 40300
|
||||
CodeNotFound = 40400
|
||||
CodeConflict = 40900
|
||||
CodeServerError = 50000
|
||||
)
|
||||
|
||||
@ -26,6 +26,10 @@ func NotFound(c *gin.Context, message string) {
|
||||
Fail(c, http.StatusNotFound, CodeNotFound, message)
|
||||
}
|
||||
|
||||
func Conflict(c *gin.Context, message string) {
|
||||
Fail(c, http.StatusConflict, CodeConflict, message)
|
||||
}
|
||||
|
||||
func ServerError(c *gin.Context, message string) {
|
||||
Fail(c, http.StatusInternalServerError, CodeServerError, message)
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"hyapp-admin-server/internal/config"
|
||||
"hyapp-admin-server/internal/middleware"
|
||||
"hyapp-admin-server/internal/modules/achievementconfig"
|
||||
"hyapp-admin-server/internal/modules/activitytemplate"
|
||||
"hyapp-admin-server/internal/modules/adminuser"
|
||||
"hyapp-admin-server/internal/modules/agencyopening"
|
||||
"hyapp-admin-server/internal/modules/appconfig"
|
||||
@ -70,6 +71,7 @@ import (
|
||||
type Handlers struct {
|
||||
AdminUser *adminuser.Handler
|
||||
AgencyOpening *agencyopening.Handler
|
||||
ActivityTemplate *activitytemplate.Handler
|
||||
AchievementConfig *achievementconfig.Handler
|
||||
AppConfig *appconfig.Handler
|
||||
AppRegistry *appregistry.Handler
|
||||
@ -142,6 +144,7 @@ func New(cfg config.Config, auth *service.AuthService, store *repository.Store,
|
||||
|
||||
authroutes.RegisterRoutes(api, protected, h.Auth)
|
||||
agencyopening.RegisterRoutes(appProtected, h.AgencyOpening)
|
||||
activitytemplate.RegisterRoutes(appProtected, h.ActivityTemplate)
|
||||
achievementconfig.RegisterRoutes(appProtected, h.AchievementConfig)
|
||||
coinledger.RegisterRoutes(appProtected, h.CoinLedger)
|
||||
menu.RegisterRoutes(protected, h.Menu)
|
||||
|
||||
@ -2,10 +2,58 @@ SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- 历史 admin_app_configs 没有租户列,所有 App 共同读取同一行。迁移把这些行保留为 app_code=''
|
||||
-- 的只读默认基线:未知或未来新增 App 仍能读取原配置,后续写操作只创建当前 App 的 scoped override。
|
||||
-- 同一个 key 可以同时存在一行 legacy 基线和多行 App override;三元唯一键是隔离写入的最终兜底。
|
||||
ALTER TABLE admin_app_configs
|
||||
ADD COLUMN app_code VARCHAR(32) NOT NULL DEFAULT '' COMMENT '应用编码;空值仅表示迁移前的只读默认基线' AFTER id,
|
||||
ADD COLUMN is_deleted BOOLEAN NOT NULL DEFAULT FALSE COMMENT '当前 App 删除墓碑;用于屏蔽默认基线' AFTER description,
|
||||
DROP INDEX uk_admin_app_configs_group_key,
|
||||
ADD UNIQUE KEY uk_admin_app_config_app_group_key (app_code, `group`, `key`),
|
||||
ADD KEY idx_admin_app_configs_app_group (app_code, `group`);
|
||||
-- 早期 AutoMigrate 和 024 SQL 使用过两个不同的 legacy 唯一索引名;每一步均按元数据执行,
|
||||
-- 使全新数据库、任一历史形态以及失败后重跑都收敛到同一三元唯一键。
|
||||
SET @ddl := IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'admin_app_configs' AND COLUMN_NAME = 'app_code') = 0,
|
||||
'ALTER TABLE admin_app_configs ADD COLUMN app_code VARCHAR(32) NOT NULL DEFAULT '''' COMMENT ''应用编码;空值仅表示迁移前的只读默认基线'' AFTER id',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @ddl := IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'admin_app_configs' AND COLUMN_NAME = 'is_deleted') = 0,
|
||||
'ALTER TABLE admin_app_configs ADD COLUMN is_deleted BOOLEAN NOT NULL DEFAULT FALSE COMMENT ''当前 App 删除墓碑;用于屏蔽默认基线'' AFTER description',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @ddl := IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'admin_app_configs' AND INDEX_NAME = 'uk_admin_app_configs_group_key') > 0,
|
||||
'ALTER TABLE admin_app_configs DROP INDEX uk_admin_app_configs_group_key',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @ddl := IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'admin_app_configs' AND INDEX_NAME = 'idx_admin_app_config_group_key') > 0,
|
||||
'ALTER TABLE admin_app_configs DROP INDEX idx_admin_app_config_group_key',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @ddl := IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'admin_app_configs' AND INDEX_NAME = 'uk_admin_app_config_app_group_key') = 0,
|
||||
'ALTER TABLE admin_app_configs ADD UNIQUE KEY uk_admin_app_config_app_group_key (app_code, `group`, `key`)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @ddl := IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'admin_app_configs' AND INDEX_NAME = 'idx_admin_app_configs_app_group') = 0,
|
||||
'ALTER TABLE admin_app_configs ADD KEY idx_admin_app_configs_app_group (app_code, `group`)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
62
server/admin/migrations/096_activity_template_navigation.sql
Normal file
62
server/admin/migrations/096_activity_template_navigation.sql
Normal file
@ -0,0 +1,62 @@
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- 活动模版配置事实由 activity-service 持有;后台仅提供 RBAC、审计和 HTTP 到 gRPC 的协议适配。
|
||||
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
|
||||
|
||||
INSERT INTO admin_permissions (name, code, kind, description, created_at_ms, updated_at_ms) VALUES
|
||||
('活动模版查看', 'activity-template:view', 'menu', '', @now_ms, @now_ms),
|
||||
('活动模版创建', 'activity-template:create', 'button', '', @now_ms, @now_ms),
|
||||
('活动模版更新', 'activity-template:update', 'button', '', @now_ms, @now_ms),
|
||||
('活动模版发布', 'activity-template:publish', 'button', '', @now_ms, @now_ms),
|
||||
('活动模版删除', 'activity-template:delete', 'button', '', @now_ms, @now_ms),
|
||||
('活动模版数据', 'activity-template:data', 'button', '查看活动聚合数据、榜单与奖励发放记录', @now_ms, @now_ms),
|
||||
('活动模版导出', 'activity-template:export', 'button', '导出受时间范围和维度上限约束的活动聚合数据', @now_ms, @now_ms),
|
||||
('活动模版重试', 'activity-template:retry', 'button', '重试失败的活动榜单奖励发放任务', @now_ms, @now_ms)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
kind = VALUES(kind),
|
||||
description = VALUES(description),
|
||||
updated_at_ms = @now_ms;
|
||||
|
||||
-- parent code 保持 activities 兼容既有路径和角色,但展示名按本期产品信息架构统一为“运营活动”。
|
||||
INSERT INTO admin_menus (parent_id, title, code, path, icon, permission_code, sort, visible, created_at_ms, updated_at_ms) VALUES
|
||||
(NULL, '运营活动', 'activities', '', 'campaign', '', 90, TRUE, @now_ms, @now_ms)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
title = VALUES(title),
|
||||
path = VALUES(path),
|
||||
icon = VALUES(icon),
|
||||
permission_code = VALUES(permission_code),
|
||||
sort = VALUES(sort),
|
||||
visible = VALUES(visible),
|
||||
updated_at_ms = @now_ms;
|
||||
|
||||
INSERT INTO admin_menus (parent_id, title, code, path, icon, permission_code, sort, visible, created_at_ms, updated_at_ms)
|
||||
SELECT parent.id, '活动模版', 'activity-template', '/activities/templates', 'campaign', 'activity-template:view', 68, TRUE, @now_ms, @now_ms
|
||||
FROM admin_menus parent
|
||||
WHERE parent.code = 'activities'
|
||||
ON DUPLICATE KEY UPDATE
|
||||
parent_id = VALUES(parent_id),
|
||||
title = VALUES(title),
|
||||
path = VALUES(path),
|
||||
icon = VALUES(icon),
|
||||
permission_code = VALUES(permission_code),
|
||||
sort = VALUES(sort),
|
||||
visible = VALUES(visible),
|
||||
updated_at_ms = @now_ms;
|
||||
|
||||
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
|
||||
SELECT admin_role.id, admin_permission.id
|
||||
FROM admin_roles admin_role
|
||||
JOIN admin_permissions admin_permission
|
||||
WHERE admin_role.code IN ('platform-admin', 'ops-admin', 'product-lead')
|
||||
AND admin_permission.code IN (
|
||||
'activity-template:view', 'activity-template:create', 'activity-template:update', 'activity-template:publish',
|
||||
'activity-template:delete', 'activity-template:data', 'activity-template:export', 'activity-template:retry'
|
||||
);
|
||||
|
||||
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
|
||||
SELECT admin_role.id, admin_permission.id
|
||||
FROM admin_roles admin_role
|
||||
JOIN admin_permissions admin_permission
|
||||
WHERE admin_role.code IN ('operations-specialist', 'product-specialist', 'auditor', 'readonly')
|
||||
AND admin_permission.code IN ('activity-template:view', 'activity-template:data');
|
||||
@ -37,6 +37,13 @@ wheel_reward_worker:
|
||||
batch_size: 50
|
||||
lock_ttl: "30s"
|
||||
max_retry: 16
|
||||
activity_template_stats_worker:
|
||||
enabled: true
|
||||
poll_interval: "1s"
|
||||
batch_size: 100
|
||||
lock_ttl: "30s"
|
||||
activity_template_runtime:
|
||||
settlement_lateness: "10m"
|
||||
message_action_confirm_worker:
|
||||
enabled: true
|
||||
outbox_poll_interval: "1s"
|
||||
@ -77,6 +84,7 @@ rocketmq:
|
||||
enabled: true
|
||||
topic: "hyapp_room_outbox"
|
||||
consumer_group: "hyapp-activity-room-outbox"
|
||||
activity_template_consumer_group: "hyapp-activity-template-room-outbox"
|
||||
consumer_max_reconsume_times: 16
|
||||
lucky_gift_outbox:
|
||||
enabled: true
|
||||
@ -103,6 +111,12 @@ rocketmq:
|
||||
invite_activity_consumer_group: "hyapp-activity-invite-user-outbox"
|
||||
message_action_consumer_group: "hyapp-activity-message-action-user-outbox"
|
||||
consumer_max_reconsume_times: 16
|
||||
activity_template_outbox:
|
||||
enabled: true
|
||||
topic: "hyapp_activity_outbox"
|
||||
producer_group: "hyapp-activity-template-outbox"
|
||||
send_timeout: "5s"
|
||||
retry: 2
|
||||
message_action_outbox:
|
||||
enabled: true
|
||||
topic: "hyapp_message_action_outbox"
|
||||
|
||||
@ -37,6 +37,13 @@ wheel_reward_worker:
|
||||
batch_size: 50
|
||||
lock_ttl: "30s"
|
||||
max_retry: 16
|
||||
activity_template_stats_worker:
|
||||
enabled: true
|
||||
poll_interval: "1s"
|
||||
batch_size: 100
|
||||
lock_ttl: "30s"
|
||||
activity_template_runtime:
|
||||
settlement_lateness: "10m"
|
||||
message_action_confirm_worker:
|
||||
enabled: true
|
||||
outbox_poll_interval: "1s"
|
||||
@ -78,6 +85,7 @@ rocketmq:
|
||||
enabled: true
|
||||
topic: "hyapp_room_outbox"
|
||||
consumer_group: "hyapp-activity-room-outbox"
|
||||
activity_template_consumer_group: "hyapp-activity-template-room-outbox"
|
||||
consumer_max_reconsume_times: 16
|
||||
lucky_gift_outbox:
|
||||
enabled: true
|
||||
@ -104,6 +112,12 @@ rocketmq:
|
||||
invite_activity_consumer_group: "hyapp-activity-invite-user-outbox"
|
||||
message_action_consumer_group: "hyapp-activity-message-action-user-outbox"
|
||||
consumer_max_reconsume_times: 16
|
||||
activity_template_outbox:
|
||||
enabled: true
|
||||
topic: "hyapp_activity_outbox"
|
||||
producer_group: "hyapp-activity-template-outbox"
|
||||
send_timeout: "5s"
|
||||
retry: 2
|
||||
message_action_outbox:
|
||||
enabled: true
|
||||
topic: "hyapp_message_action_outbox"
|
||||
|
||||
@ -37,6 +37,13 @@ wheel_reward_worker:
|
||||
batch_size: 50
|
||||
lock_ttl: "30s"
|
||||
max_retry: 16
|
||||
activity_template_stats_worker:
|
||||
enabled: true
|
||||
poll_interval: "1s"
|
||||
batch_size: 100
|
||||
lock_ttl: "30s"
|
||||
activity_template_runtime:
|
||||
settlement_lateness: "10m"
|
||||
message_action_confirm_worker:
|
||||
enabled: true
|
||||
outbox_poll_interval: "1s"
|
||||
@ -78,6 +85,7 @@ rocketmq:
|
||||
enabled: true
|
||||
topic: "hyapp_room_outbox"
|
||||
consumer_group: "hyapp-activity-room-outbox"
|
||||
activity_template_consumer_group: "hyapp-activity-template-room-outbox"
|
||||
consumer_max_reconsume_times: 16
|
||||
lucky_gift_outbox:
|
||||
enabled: true
|
||||
@ -104,6 +112,12 @@ rocketmq:
|
||||
invite_activity_consumer_group: "hyapp-activity-invite-user-outbox"
|
||||
message_action_consumer_group: "hyapp-activity-message-action-user-outbox"
|
||||
consumer_max_reconsume_times: 16
|
||||
activity_template_outbox:
|
||||
enabled: true
|
||||
topic: "hyapp_activity_outbox"
|
||||
producer_group: "hyapp-activity-template-outbox"
|
||||
send_timeout: "5s"
|
||||
retry: 2
|
||||
message_action_outbox:
|
||||
enabled: true
|
||||
topic: "hyapp_message_action_outbox"
|
||||
|
||||
@ -1479,3 +1479,366 @@ CREATE TABLE IF NOT EXISTS invite_activity_reward_claims (
|
||||
KEY idx_invite_activity_reward_claim_user (app_code, cycle_key, user_id, created_at_ms),
|
||||
KEY idx_invite_activity_reward_claim_status (app_code, status, created_at_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='邀请活动金币领取记录';
|
||||
|
||||
-- 活动模版配置和发布版本由 activity-service 统一持久化;admin-server 只能通过 AdminActivityTemplateService 访问。
|
||||
CREATE TABLE IF NOT EXISTS activity_templates (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
template_id VARCHAR(96) NOT NULL COMMENT '活动模版 ID',
|
||||
template_code VARCHAR(64) NOT NULL COMMENT 'App 内唯一业务编码',
|
||||
name VARCHAR(128) NOT NULL COMMENT '后台内部名称',
|
||||
activity_type VARCHAR(32) NOT NULL DEFAULT 'gift_challenge' COMMENT '活动类型',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'draft' COMMENT 'draft/published/disabled/archived',
|
||||
start_ms BIGINT NOT NULL DEFAULT 0 COMMENT '活动开始,UTC epoch ms,包含',
|
||||
end_ms BIGINT NOT NULL DEFAULT 0 COMMENT '活动结束,UTC epoch ms,不包含',
|
||||
all_regions BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否覆盖全部区域',
|
||||
daily_leaderboard_size INT NOT NULL DEFAULT 100 COMMENT '日榜最多展示人数',
|
||||
total_leaderboard_size INT NOT NULL DEFAULT 100 COMMENT '总榜最多展示人数',
|
||||
display_config_json JSON NOT NULL COMMENT '客户端布局和样式配置',
|
||||
revision BIGINT NOT NULL DEFAULT 1 COMMENT '后台编辑乐观锁版本',
|
||||
published_version BIGINT NOT NULL DEFAULT 0 COMMENT '最近发布快照版本',
|
||||
created_by_admin_id BIGINT NOT NULL COMMENT '创建管理员',
|
||||
updated_by_admin_id BIGINT NOT NULL COMMENT '最后更新管理员',
|
||||
published_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '最近发布管理员',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
published_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '最近发布时间,UTC epoch ms',
|
||||
archived_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '软归档时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, template_id),
|
||||
UNIQUE KEY uk_activity_template_code (app_code, template_code),
|
||||
KEY idx_activity_template_list (app_code, status, updated_at_ms, template_id),
|
||||
KEY idx_activity_template_period (app_code, status, start_ms, end_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='可排期运营活动模版实例';
|
||||
|
||||
-- 发布互斥行按 App 串行化“检查重叠 + 写入 published”。仅锁候选活动行会出现两个草稿并发发布时互相不可见的写偏差。
|
||||
CREATE TABLE IF NOT EXISTS activity_template_publish_locks (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '最近获取发布锁时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版发布事务互斥行';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_regions (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
template_id VARCHAR(96) NOT NULL COMMENT '活动模版 ID',
|
||||
region_id BIGINT NOT NULL COMMENT '投放区域 ID',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, template_id, region_id),
|
||||
KEY idx_activity_template_region_match (app_code, region_id, template_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版投放区域';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_locales (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
template_id VARCHAR(96) NOT NULL COMMENT '活动模版 ID',
|
||||
locale VARCHAR(24) NOT NULL COMMENT 'BCP-47 基础语言码',
|
||||
title VARCHAR(160) NOT NULL DEFAULT '' COMMENT '客户端活动标题',
|
||||
rules MEDIUMTEXT NOT NULL COMMENT '客户端活动规则正文',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, template_id, locale)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版多语言文案';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_gifts (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
template_id VARCHAR(96) NOT NULL COMMENT '活动模版 ID',
|
||||
gift_id VARCHAR(96) NOT NULL COMMENT 'wallet 礼物 ID',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '展示顺序',
|
||||
name VARCHAR(128) NOT NULL DEFAULT '' COMMENT '发布展示名称快照',
|
||||
icon_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '发布展示图标快照',
|
||||
coin_price BIGINT NOT NULL DEFAULT 0 COMMENT '发布时金币价格快照,仅供展示',
|
||||
price_version VARCHAR(64) NOT NULL DEFAULT '' COMMENT '发布时价格版本快照',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, template_id, gift_id),
|
||||
KEY idx_activity_template_gift_match (app_code, gift_id, template_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版指定礼物和展示快照';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_tasks (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
template_id VARCHAR(96) NOT NULL COMMENT '活动模版 ID',
|
||||
task_key VARCHAR(64) NOT NULL COMMENT '模版内稳定任务键',
|
||||
task_type VARCHAR(32) NOT NULL COMMENT 'daily_visit/gift_count/gift_coin_amount',
|
||||
target_value BIGINT NOT NULL DEFAULT 0 COMMENT '任务达成阈值',
|
||||
reward_resource_group_id BIGINT NOT NULL DEFAULT 0 COMMENT 'wallet 奖励资源组 ID',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '展示顺序',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, template_id, task_key)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版每日任务配置';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_rank_rewards (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
template_id VARCHAR(96) NOT NULL COMMENT '活动模版 ID',
|
||||
board_type VARCHAR(16) NOT NULL COMMENT 'daily/total',
|
||||
rank_from INT NOT NULL COMMENT '奖励起始名次,闭区间',
|
||||
rank_to INT NOT NULL COMMENT '奖励结束名次,闭区间',
|
||||
resource_group_id BIGINT NOT NULL DEFAULT 0 COMMENT 'wallet 奖励资源组 ID',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '展示顺序',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, template_id, board_type, rank_from, rank_to)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版日榜和总榜名次奖励';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_assets (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
template_id VARCHAR(96) NOT NULL COMMENT '活动模版 ID',
|
||||
asset_key VARCHAR(64) NOT NULL COMMENT '素材位键',
|
||||
locale VARCHAR(24) NOT NULL DEFAULT '*' COMMENT '* 表示共享素材,否则为语言码',
|
||||
url VARCHAR(1024) NOT NULL COMMENT 'COS/CDN 素材地址',
|
||||
media_type VARCHAR(16) NOT NULL COMMENT 'image/webp/svga/pag',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '展示顺序',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, template_id, asset_key, locale)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版客户端素材';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_versions (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
template_id VARCHAR(96) NOT NULL COMMENT '活动模版 ID',
|
||||
version_no BIGINT NOT NULL COMMENT '单模版递增发布版本',
|
||||
snapshot_json JSON NOT NULL COMMENT '发布时完整不可变配置快照',
|
||||
published_by_admin_id BIGINT NOT NULL COMMENT '发布管理员',
|
||||
published_at_ms BIGINT NOT NULL COMMENT '发布时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, template_id, version_no),
|
||||
KEY idx_activity_template_version_list (app_code, template_id, published_at_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版不可变发布版本';
|
||||
|
||||
-- 活动模版真实运行时、任务、榜单结算及统计事实 outbox。
|
||||
-- Activity-template runtime facts are version-bound. Configuration edits never rewrite these tables.
|
||||
CREATE TABLE IF NOT EXISTS activity_template_runtime_versions (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
start_ms BIGINT NOT NULL,
|
||||
end_ms BIGINT NOT NULL,
|
||||
all_regions BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
runtime_from_ms BIGINT NOT NULL,
|
||||
runtime_to_ms BIGINT NOT NULL DEFAULT 0,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, template_id, version_no),
|
||||
KEY idx_activity_template_runtime_current (app_code, template_code, start_ms, end_ms, runtime_from_ms, runtime_to_ms),
|
||||
KEY idx_activity_template_runtime_due (app_code, end_ms, runtime_to_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版不可变运行版本启停窗口';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_runtime_regions (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
region_id BIGINT NOT NULL,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, template_id, version_no, region_id),
|
||||
KEY idx_activity_template_runtime_region (app_code, region_id, template_id, version_no)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版运行版本区域快照';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_event_consumption (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
source_event_id VARCHAR(128) NOT NULL,
|
||||
event_type VARCHAR(64) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL DEFAULT '',
|
||||
template_code VARCHAR(64) NOT NULL DEFAULT '',
|
||||
version_no BIGINT NOT NULL DEFAULT 0,
|
||||
user_id BIGINT NOT NULL DEFAULT 0,
|
||||
region_id BIGINT NOT NULL DEFAULT 0,
|
||||
task_day VARCHAR(10) NOT NULL DEFAULT '',
|
||||
status VARCHAR(24) NOT NULL,
|
||||
skip_reason VARCHAR(128) NOT NULL DEFAULT '',
|
||||
occurred_at_ms BIGINT NOT NULL,
|
||||
consumed_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, source_event_id),
|
||||
KEY idx_activity_template_event_template (app_code, template_id, version_no, occurred_at_ms),
|
||||
KEY idx_activity_template_event_status (app_code, status, consumed_at_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版运行事件幂等事实';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_user_day_scores (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
task_day VARCHAR(10) NOT NULL,
|
||||
day_start_ms BIGINT NOT NULL,
|
||||
day_end_ms BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
score BIGINT NOT NULL DEFAULT 0,
|
||||
gift_count BIGINT NOT NULL DEFAULT 0,
|
||||
coin_amount BIGINT NOT NULL DEFAULT 0,
|
||||
first_scored_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, template_id, version_no, task_day, user_id),
|
||||
KEY idx_activity_template_day_rank (app_code, template_id, version_no, task_day, score DESC, first_scored_at_ms, user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版 UTC 日榜实时分数';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_user_total_scores (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
score BIGINT NOT NULL DEFAULT 0,
|
||||
gift_count BIGINT NOT NULL DEFAULT 0,
|
||||
coin_amount BIGINT NOT NULL DEFAULT 0,
|
||||
first_scored_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, template_id, version_no, user_id),
|
||||
KEY idx_activity_template_total_rank (app_code, template_id, version_no, score DESC, first_scored_at_ms, user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版活动总榜实时分数';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_daily_visits (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
task_day VARCHAR(10) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
region_id BIGINT NOT NULL DEFAULT 0,
|
||||
command_id VARCHAR(128) NOT NULL,
|
||||
visited_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, template_id, version_no, task_day, user_id),
|
||||
UNIQUE KEY uk_activity_template_visit_command (app_code, command_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版 daily_visit 自然幂等事实';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_task_progress (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
task_day VARCHAR(10) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
first_region_id BIGINT NOT NULL DEFAULT 0,
|
||||
task_key VARCHAR(64) NOT NULL,
|
||||
task_type VARCHAR(32) NOT NULL,
|
||||
target_value BIGINT NOT NULL,
|
||||
progress_value BIGINT NOT NULL DEFAULT 0,
|
||||
reward_resource_group_id BIGINT NOT NULL,
|
||||
reward_snapshot_id VARCHAR(96) NOT NULL DEFAULT '',
|
||||
reward_snapshot_json JSON NULL,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
status VARCHAR(24) NOT NULL,
|
||||
completed_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||
earning_region_id BIGINT NOT NULL DEFAULT 0,
|
||||
attribution_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||
claimed_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, template_id, version_no, task_day, user_id, task_key),
|
||||
KEY idx_activity_template_task_data (app_code, template_id, version_no, task_day, task_key, status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版用户每日任务进度';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_task_claims (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
claim_id VARCHAR(96) NOT NULL,
|
||||
command_id VARCHAR(128) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
task_day VARCHAR(10) NOT NULL,
|
||||
task_key VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
earning_region_id BIGINT NOT NULL DEFAULT 0,
|
||||
attribution_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||
reward_resource_group_id BIGINT NOT NULL,
|
||||
reward_snapshot_id VARCHAR(96) NOT NULL DEFAULT '',
|
||||
reward_snapshot_json JSON NULL,
|
||||
wallet_command_id VARCHAR(160) NOT NULL,
|
||||
wallet_grant_id VARCHAR(96) NOT NULL DEFAULT '',
|
||||
status VARCHAR(24) NOT NULL,
|
||||
failure_reason VARCHAR(512) NOT NULL DEFAULT '',
|
||||
attempt_count INT NOT NULL DEFAULT 0,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
granted_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (app_code, claim_id),
|
||||
UNIQUE KEY uk_activity_template_claim_command (app_code, command_id),
|
||||
UNIQUE KEY uk_activity_template_claim_task (app_code, template_id, version_no, task_day, user_id, task_key),
|
||||
UNIQUE KEY uk_activity_template_claim_wallet (app_code, wallet_command_id),
|
||||
KEY idx_activity_template_claim_status (app_code, status, updated_at_ms),
|
||||
KEY idx_activity_template_claim_admin (app_code, template_id, version_no, task_day, task_key, user_id, status, updated_at_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版任务资源组领奖事实';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_rank_periods (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
board_type VARCHAR(16) NOT NULL,
|
||||
period_key VARCHAR(24) NOT NULL,
|
||||
period_start_ms BIGINT NOT NULL,
|
||||
period_end_ms BIGINT NOT NULL,
|
||||
settle_after_ms BIGINT NOT NULL COMMENT '结算水位:周期结束加允许迟到窗口,UTC epoch ms',
|
||||
leaderboard_size INT NOT NULL,
|
||||
status VARCHAR(24) NOT NULL,
|
||||
locked_by VARCHAR(128) NOT NULL DEFAULT '',
|
||||
lock_until_ms BIGINT NOT NULL DEFAULT 0,
|
||||
settled_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, template_id, version_no, board_type, period_key),
|
||||
KEY idx_activity_template_period_settle_due (app_code, status, settle_after_ms, lock_until_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版榜单结算周期';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_rank_snapshots (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
board_type VARCHAR(16) NOT NULL,
|
||||
period_key VARCHAR(24) NOT NULL,
|
||||
rank_no INT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
score BIGINT NOT NULL,
|
||||
gift_count BIGINT NOT NULL,
|
||||
coin_amount BIGINT NOT NULL,
|
||||
first_scored_at_ms BIGINT NOT NULL,
|
||||
snapshot_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, template_id, version_no, board_type, period_key, rank_no),
|
||||
UNIQUE KEY uk_activity_template_rank_user (app_code, template_id, version_no, board_type, period_key, user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版冻结榜单快照';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_reward_jobs (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
reward_job_id VARCHAR(96) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
board_type VARCHAR(16) NOT NULL,
|
||||
period_key VARCHAR(24) NOT NULL,
|
||||
rank_no INT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
score BIGINT NOT NULL,
|
||||
resource_group_id BIGINT NOT NULL,
|
||||
reward_snapshot_id VARCHAR(96) NOT NULL DEFAULT '',
|
||||
reward_snapshot_json JSON NULL,
|
||||
wallet_command_id VARCHAR(192) NOT NULL,
|
||||
wallet_grant_id VARCHAR(96) NOT NULL DEFAULT '',
|
||||
status VARCHAR(24) NOT NULL,
|
||||
failure_reason VARCHAR(512) NOT NULL DEFAULT '',
|
||||
attempt_count INT NOT NULL DEFAULT 0,
|
||||
max_attempts INT NOT NULL DEFAULT 8 COMMENT '自动投递总尝试上限;dead 手工复活时只扩一轮预算',
|
||||
next_retry_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '下一次可领取时间,UTC epoch ms',
|
||||
locked_by VARCHAR(128) NOT NULL DEFAULT '' COMMENT '当前 job lease owner',
|
||||
lock_until_ms BIGINT NOT NULL DEFAULT 0 COMMENT 'job lease 过期时间,UTC epoch ms',
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
granted_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (app_code, reward_job_id),
|
||||
UNIQUE KEY uk_activity_template_reward_rank (app_code, template_id, version_no, board_type, period_key, rank_no),
|
||||
UNIQUE KEY uk_activity_template_reward_wallet (app_code, wallet_command_id),
|
||||
KEY idx_activity_template_reward_query (app_code, template_id, version_no, board_type, period_key, status),
|
||||
KEY idx_activity_template_reward_retry (app_code, status, updated_at_ms),
|
||||
KEY idx_activity_template_reward_claim (app_code, status, next_retry_at_ms, lock_until_ms, created_at_ms, reward_job_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版榜单奖励发放任务';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_stats_outbox (
|
||||
outbox_id VARCHAR(96) NOT NULL,
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
event_type VARCHAR(64) NOT NULL,
|
||||
payload BLOB NOT NULL,
|
||||
status VARCHAR(24) NOT NULL,
|
||||
retry_count INT NOT NULL DEFAULT 0,
|
||||
next_retry_at_ms BIGINT NOT NULL,
|
||||
locked_by VARCHAR(128) NOT NULL DEFAULT '',
|
||||
lock_until_ms BIGINT NOT NULL DEFAULT 0,
|
||||
last_error VARCHAR(512) NOT NULL DEFAULT '',
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (outbox_id),
|
||||
KEY idx_activity_template_stats_outbox_claim (status, next_retry_at_ms, lock_until_ms, created_at_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版统计事实 RocketMQ transactional outbox';
|
||||
|
||||
@ -0,0 +1,119 @@
|
||||
CREATE TABLE IF NOT EXISTS activity_templates (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
template_id VARCHAR(96) NOT NULL COMMENT '活动模版 ID',
|
||||
template_code VARCHAR(64) NOT NULL COMMENT 'App 内唯一业务编码',
|
||||
name VARCHAR(128) NOT NULL COMMENT '后台内部名称',
|
||||
activity_type VARCHAR(32) NOT NULL DEFAULT 'gift_challenge' COMMENT '活动类型',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'draft' COMMENT 'draft/published/disabled/archived',
|
||||
start_ms BIGINT NOT NULL DEFAULT 0 COMMENT '活动开始,UTC epoch ms,包含',
|
||||
end_ms BIGINT NOT NULL DEFAULT 0 COMMENT '活动结束,UTC epoch ms,不包含',
|
||||
all_regions BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否覆盖全部区域',
|
||||
daily_leaderboard_size INT NOT NULL DEFAULT 100 COMMENT '日榜最多展示人数',
|
||||
total_leaderboard_size INT NOT NULL DEFAULT 100 COMMENT '总榜最多展示人数',
|
||||
display_config_json JSON NOT NULL COMMENT '客户端布局和样式配置',
|
||||
revision BIGINT NOT NULL DEFAULT 1 COMMENT '后台编辑乐观锁版本',
|
||||
published_version BIGINT NOT NULL DEFAULT 0 COMMENT '最近发布快照版本',
|
||||
created_by_admin_id BIGINT NOT NULL COMMENT '创建管理员',
|
||||
updated_by_admin_id BIGINT NOT NULL COMMENT '最后更新管理员',
|
||||
published_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '最近发布管理员',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
published_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '最近发布时间,UTC epoch ms',
|
||||
archived_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '软归档时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, template_id),
|
||||
UNIQUE KEY uk_activity_template_code (app_code, template_code),
|
||||
KEY idx_activity_template_list (app_code, status, updated_at_ms, template_id),
|
||||
KEY idx_activity_template_period (app_code, status, start_ms, end_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='可排期运营活动模版实例';
|
||||
|
||||
-- 发布互斥行按 App 串行化“检查重叠 + 写入 published”。仅锁候选活动行会出现两个草稿并发发布时互相不可见的写偏差。
|
||||
CREATE TABLE IF NOT EXISTS activity_template_publish_locks (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '最近获取发布锁时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版发布事务互斥行';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_regions (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
template_id VARCHAR(96) NOT NULL COMMENT '活动模版 ID',
|
||||
region_id BIGINT NOT NULL COMMENT '投放区域 ID',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, template_id, region_id),
|
||||
KEY idx_activity_template_region_match (app_code, region_id, template_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版投放区域';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_locales (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
template_id VARCHAR(96) NOT NULL COMMENT '活动模版 ID',
|
||||
locale VARCHAR(24) NOT NULL COMMENT 'BCP-47 基础语言码',
|
||||
title VARCHAR(160) NOT NULL DEFAULT '' COMMENT '客户端活动标题',
|
||||
rules MEDIUMTEXT NOT NULL COMMENT '客户端活动规则正文',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, template_id, locale)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版多语言文案';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_gifts (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
template_id VARCHAR(96) NOT NULL COMMENT '活动模版 ID',
|
||||
gift_id VARCHAR(96) NOT NULL COMMENT 'wallet 礼物 ID',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '展示顺序',
|
||||
name VARCHAR(128) NOT NULL DEFAULT '' COMMENT '发布展示名称快照',
|
||||
icon_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '发布展示图标快照',
|
||||
coin_price BIGINT NOT NULL DEFAULT 0 COMMENT '发布时金币价格快照,仅供展示',
|
||||
price_version VARCHAR(64) NOT NULL DEFAULT '' COMMENT '发布时价格版本快照',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, template_id, gift_id),
|
||||
KEY idx_activity_template_gift_match (app_code, gift_id, template_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版指定礼物和展示快照';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_tasks (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
template_id VARCHAR(96) NOT NULL COMMENT '活动模版 ID',
|
||||
task_key VARCHAR(64) NOT NULL COMMENT '模版内稳定任务键',
|
||||
task_type VARCHAR(32) NOT NULL COMMENT 'daily_visit/gift_count/gift_coin_amount',
|
||||
target_value BIGINT NOT NULL DEFAULT 0 COMMENT '任务达成阈值',
|
||||
reward_resource_group_id BIGINT NOT NULL DEFAULT 0 COMMENT 'wallet 奖励资源组 ID',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '展示顺序',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, template_id, task_key)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版每日任务配置';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_rank_rewards (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
template_id VARCHAR(96) NOT NULL COMMENT '活动模版 ID',
|
||||
board_type VARCHAR(16) NOT NULL COMMENT 'daily/total',
|
||||
rank_from INT NOT NULL COMMENT '奖励起始名次,闭区间',
|
||||
rank_to INT NOT NULL COMMENT '奖励结束名次,闭区间',
|
||||
resource_group_id BIGINT NOT NULL DEFAULT 0 COMMENT 'wallet 奖励资源组 ID',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '展示顺序',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, template_id, board_type, rank_from, rank_to)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版日榜和总榜名次奖励';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_assets (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
template_id VARCHAR(96) NOT NULL COMMENT '活动模版 ID',
|
||||
asset_key VARCHAR(64) NOT NULL COMMENT '素材位键',
|
||||
locale VARCHAR(24) NOT NULL DEFAULT '*' COMMENT '* 表示共享素材,否则为语言码',
|
||||
url VARCHAR(1024) NOT NULL COMMENT 'COS/CDN 素材地址',
|
||||
media_type VARCHAR(16) NOT NULL COMMENT 'image/webp/svga/pag',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '展示顺序',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, template_id, asset_key, locale)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版客户端素材';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_versions (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
template_id VARCHAR(96) NOT NULL COMMENT '活动模版 ID',
|
||||
version_no BIGINT NOT NULL COMMENT '单模版递增发布版本',
|
||||
snapshot_json JSON NOT NULL COMMENT '发布时完整不可变配置快照',
|
||||
published_by_admin_id BIGINT NOT NULL COMMENT '发布管理员',
|
||||
published_at_ms BIGINT NOT NULL COMMENT '发布时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, template_id, version_no),
|
||||
KEY idx_activity_template_version_list (app_code, template_id, published_at_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版不可变发布版本';
|
||||
@ -0,0 +1,235 @@
|
||||
-- Activity-template runtime facts are version-bound. Configuration edits never rewrite these tables.
|
||||
CREATE TABLE IF NOT EXISTS activity_template_runtime_versions (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
start_ms BIGINT NOT NULL,
|
||||
end_ms BIGINT NOT NULL,
|
||||
all_regions BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
runtime_from_ms BIGINT NOT NULL,
|
||||
runtime_to_ms BIGINT NOT NULL DEFAULT 0,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, template_id, version_no),
|
||||
KEY idx_activity_template_runtime_current (app_code, template_code, start_ms, end_ms, runtime_from_ms, runtime_to_ms),
|
||||
KEY idx_activity_template_runtime_due (app_code, end_ms, runtime_to_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版不可变运行版本启停窗口';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_runtime_regions (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
region_id BIGINT NOT NULL,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, template_id, version_no, region_id),
|
||||
KEY idx_activity_template_runtime_region (app_code, region_id, template_id, version_no)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版运行版本区域快照';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_event_consumption (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
source_event_id VARCHAR(128) NOT NULL,
|
||||
event_type VARCHAR(64) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL DEFAULT '',
|
||||
template_code VARCHAR(64) NOT NULL DEFAULT '',
|
||||
version_no BIGINT NOT NULL DEFAULT 0,
|
||||
user_id BIGINT NOT NULL DEFAULT 0,
|
||||
region_id BIGINT NOT NULL DEFAULT 0,
|
||||
task_day VARCHAR(10) NOT NULL DEFAULT '',
|
||||
status VARCHAR(24) NOT NULL,
|
||||
skip_reason VARCHAR(128) NOT NULL DEFAULT '',
|
||||
occurred_at_ms BIGINT NOT NULL,
|
||||
consumed_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, source_event_id),
|
||||
KEY idx_activity_template_event_template (app_code, template_id, version_no, occurred_at_ms),
|
||||
KEY idx_activity_template_event_status (app_code, status, consumed_at_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版运行事件幂等事实';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_user_day_scores (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
task_day VARCHAR(10) NOT NULL,
|
||||
day_start_ms BIGINT NOT NULL,
|
||||
day_end_ms BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
score BIGINT NOT NULL DEFAULT 0,
|
||||
gift_count BIGINT NOT NULL DEFAULT 0,
|
||||
coin_amount BIGINT NOT NULL DEFAULT 0,
|
||||
first_scored_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, template_id, version_no, task_day, user_id),
|
||||
KEY idx_activity_template_day_rank (app_code, template_id, version_no, task_day, score DESC, first_scored_at_ms, user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版 UTC 日榜实时分数';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_user_total_scores (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
score BIGINT NOT NULL DEFAULT 0,
|
||||
gift_count BIGINT NOT NULL DEFAULT 0,
|
||||
coin_amount BIGINT NOT NULL DEFAULT 0,
|
||||
first_scored_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, template_id, version_no, user_id),
|
||||
KEY idx_activity_template_total_rank (app_code, template_id, version_no, score DESC, first_scored_at_ms, user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版活动总榜实时分数';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_daily_visits (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
task_day VARCHAR(10) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
region_id BIGINT NOT NULL DEFAULT 0,
|
||||
command_id VARCHAR(128) NOT NULL,
|
||||
visited_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, template_id, version_no, task_day, user_id),
|
||||
UNIQUE KEY uk_activity_template_visit_command (app_code, command_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版 daily_visit 自然幂等事实';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_task_progress (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
task_day VARCHAR(10) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
first_region_id BIGINT NOT NULL DEFAULT 0,
|
||||
task_key VARCHAR(64) NOT NULL,
|
||||
task_type VARCHAR(32) NOT NULL,
|
||||
target_value BIGINT NOT NULL,
|
||||
progress_value BIGINT NOT NULL DEFAULT 0,
|
||||
reward_resource_group_id BIGINT NOT NULL,
|
||||
reward_snapshot_id VARCHAR(96) NOT NULL DEFAULT '',
|
||||
reward_snapshot_json JSON NULL,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
status VARCHAR(24) NOT NULL,
|
||||
completed_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||
earning_region_id BIGINT NOT NULL DEFAULT 0,
|
||||
attribution_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||
claimed_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, template_id, version_no, task_day, user_id, task_key),
|
||||
KEY idx_activity_template_task_data (app_code, template_id, version_no, task_day, task_key, status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版用户每日任务进度';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_task_claims (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
claim_id VARCHAR(96) NOT NULL,
|
||||
command_id VARCHAR(128) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
task_day VARCHAR(10) NOT NULL,
|
||||
task_key VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
earning_region_id BIGINT NOT NULL DEFAULT 0,
|
||||
attribution_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||
reward_resource_group_id BIGINT NOT NULL,
|
||||
reward_snapshot_id VARCHAR(96) NOT NULL DEFAULT '',
|
||||
reward_snapshot_json JSON NULL,
|
||||
wallet_command_id VARCHAR(160) NOT NULL,
|
||||
wallet_grant_id VARCHAR(96) NOT NULL DEFAULT '',
|
||||
status VARCHAR(24) NOT NULL,
|
||||
failure_reason VARCHAR(512) NOT NULL DEFAULT '',
|
||||
attempt_count INT NOT NULL DEFAULT 0,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
granted_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (app_code, claim_id),
|
||||
UNIQUE KEY uk_activity_template_claim_command (app_code, command_id),
|
||||
UNIQUE KEY uk_activity_template_claim_task (app_code, template_id, version_no, task_day, user_id, task_key),
|
||||
UNIQUE KEY uk_activity_template_claim_wallet (app_code, wallet_command_id),
|
||||
KEY idx_activity_template_claim_status (app_code, status, updated_at_ms),
|
||||
KEY idx_activity_template_claim_admin (app_code, template_id, version_no, task_day, task_key, user_id, status, updated_at_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版任务资源组领奖事实';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_rank_periods (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
board_type VARCHAR(16) NOT NULL,
|
||||
period_key VARCHAR(24) NOT NULL,
|
||||
period_start_ms BIGINT NOT NULL,
|
||||
period_end_ms BIGINT NOT NULL,
|
||||
settle_after_ms BIGINT NOT NULL COMMENT '结算水位:周期结束加允许迟到窗口,UTC epoch ms',
|
||||
leaderboard_size INT NOT NULL,
|
||||
status VARCHAR(24) NOT NULL,
|
||||
locked_by VARCHAR(128) NOT NULL DEFAULT '',
|
||||
lock_until_ms BIGINT NOT NULL DEFAULT 0,
|
||||
settled_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, template_id, version_no, board_type, period_key),
|
||||
KEY idx_activity_template_period_settle_due (app_code, status, settle_after_ms, lock_until_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版榜单结算周期';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_rank_snapshots (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
board_type VARCHAR(16) NOT NULL,
|
||||
period_key VARCHAR(24) NOT NULL,
|
||||
rank_no INT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
score BIGINT NOT NULL,
|
||||
gift_count BIGINT NOT NULL,
|
||||
coin_amount BIGINT NOT NULL,
|
||||
first_scored_at_ms BIGINT NOT NULL,
|
||||
snapshot_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, template_id, version_no, board_type, period_key, rank_no),
|
||||
UNIQUE KEY uk_activity_template_rank_user (app_code, template_id, version_no, board_type, period_key, user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版冻结榜单快照';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_reward_jobs (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
reward_job_id VARCHAR(96) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
board_type VARCHAR(16) NOT NULL,
|
||||
period_key VARCHAR(24) NOT NULL,
|
||||
rank_no INT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
score BIGINT NOT NULL,
|
||||
resource_group_id BIGINT NOT NULL,
|
||||
reward_snapshot_id VARCHAR(96) NOT NULL DEFAULT '',
|
||||
reward_snapshot_json JSON NULL,
|
||||
wallet_command_id VARCHAR(192) NOT NULL,
|
||||
wallet_grant_id VARCHAR(96) NOT NULL DEFAULT '',
|
||||
status VARCHAR(24) NOT NULL,
|
||||
failure_reason VARCHAR(512) NOT NULL DEFAULT '',
|
||||
attempt_count INT NOT NULL DEFAULT 0,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
granted_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (app_code, reward_job_id),
|
||||
UNIQUE KEY uk_activity_template_reward_rank (app_code, template_id, version_no, board_type, period_key, rank_no),
|
||||
UNIQUE KEY uk_activity_template_reward_wallet (app_code, wallet_command_id),
|
||||
KEY idx_activity_template_reward_query (app_code, template_id, version_no, board_type, period_key, status),
|
||||
KEY idx_activity_template_reward_retry (app_code, status, updated_at_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版榜单奖励发放任务';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_template_stats_outbox (
|
||||
outbox_id VARCHAR(96) NOT NULL,
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
event_type VARCHAR(64) NOT NULL,
|
||||
payload BLOB NOT NULL,
|
||||
status VARCHAR(24) NOT NULL,
|
||||
retry_count INT NOT NULL DEFAULT 0,
|
||||
next_retry_at_ms BIGINT NOT NULL,
|
||||
locked_by VARCHAR(128) NOT NULL DEFAULT '',
|
||||
lock_until_ms BIGINT NOT NULL DEFAULT 0,
|
||||
last_error VARCHAR(512) NOT NULL DEFAULT '',
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (outbox_id),
|
||||
KEY idx_activity_template_stats_outbox_claim (status, next_retry_at_ms, lock_until_ms, created_at_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版统计事实 RocketMQ transactional outbox';
|
||||
@ -0,0 +1,12 @@
|
||||
-- 榜单周期冻结与钱包发奖解耦:job 独立租约允许水平扩展,退避/终态避免钱包故障时形成重试风暴。
|
||||
ALTER TABLE activity_template_reward_jobs
|
||||
ADD COLUMN max_attempts INT NOT NULL DEFAULT 8
|
||||
COMMENT '自动投递总尝试上限;dead 手工复活时只扩一轮预算' AFTER attempt_count,
|
||||
ADD COLUMN next_retry_at_ms BIGINT NOT NULL DEFAULT 0
|
||||
COMMENT '下一次可领取时间,UTC epoch ms' AFTER max_attempts,
|
||||
ADD COLUMN locked_by VARCHAR(128) NOT NULL DEFAULT ''
|
||||
COMMENT '当前 job lease owner' AFTER next_retry_at_ms,
|
||||
ADD COLUMN lock_until_ms BIGINT NOT NULL DEFAULT 0
|
||||
COMMENT 'job lease 过期时间,UTC epoch ms' AFTER locked_by,
|
||||
ADD KEY idx_activity_template_reward_claim
|
||||
(app_code, status, next_retry_at_ms, lock_until_ms, created_at_ms, reward_job_id);
|
||||
@ -0,0 +1,30 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"hyapp/pkg/logx"
|
||||
)
|
||||
|
||||
func (a *App) runActivityTemplateStatsOutboxWorker(ctx context.Context) {
|
||||
options := a.activityTemplateStatsWorkerOptions
|
||||
ticker := time.NewTicker(options.PollInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
claimed, _, err := a.activityTemplate.ProcessStatsOutboxBatch(ctx, a.activityTemplateProducer,
|
||||
a.activityTemplateOutboxTopic, a.workerNodeID+":activity-template-stats", options.BatchSize, options.LockTTL)
|
||||
if err != nil && ctx.Err() == nil {
|
||||
logx.Error(ctx, "activity_template_stats_outbox_publish_failed", err, slog.String("node_id", a.workerNodeID))
|
||||
}
|
||||
if claimed >= options.BatchSize {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -16,6 +16,7 @@ import (
|
||||
"hyapp/pkg/servicekit/grpcserver"
|
||||
servicemq "hyapp/pkg/servicekit/mq"
|
||||
"hyapp/services/activity-service/internal/config"
|
||||
activitytemplateservice "hyapp/services/activity-service/internal/service/activitytemplate"
|
||||
broadcastservice "hyapp/services/activity-service/internal/service/broadcast"
|
||||
cumulativerechargeservice "hyapp/services/activity-service/internal/service/cumulativerecharge"
|
||||
firstrechargeservice "hyapp/services/activity-service/internal/service/firstrecharge"
|
||||
@ -28,32 +29,37 @@ import (
|
||||
// App 装配 activity-service 的 gRPC 入口、健康检查、后台 worker 和外部连接。
|
||||
// 具体业务模块的创建和注册都放在独立文件里,避免启动入口继续膨胀成一个难维护的大函数。
|
||||
type App struct {
|
||||
server *grpc.Server
|
||||
listener net.Listener
|
||||
health *grpchealth.ServingChecker
|
||||
healthHTTP *healthhttp.Server
|
||||
mysqlRepo *mysqlstorage.Repository
|
||||
broadcast *broadcastservice.Service
|
||||
firstRechargeReward *firstrechargeservice.Service
|
||||
cumulativeRecharge *cumulativerechargeservice.Service
|
||||
inviteActivityReward *inviteactivityservice.Service
|
||||
actionConfirm *messageservice.ActionConfirmService
|
||||
wheel *wheelservice.Service
|
||||
broadcastWorkerEnabled bool
|
||||
messageActionWorkerEnabled bool
|
||||
wheelRewardWorkerEnabled bool
|
||||
workerNodeID string
|
||||
messageActionWorkerOptions config.MessageActionConfirmWorkerConfig
|
||||
wheelRewardWorkerOptions config.WheelRewardWorkerConfig
|
||||
messageActionOutboxTopic string
|
||||
userLeaderboardRedisClose func() error
|
||||
mqConsumers []*rocketmqx.Consumer
|
||||
messageActionProducer *rocketmqx.Producer
|
||||
userConn *grpc.ClientConn
|
||||
walletConn *grpc.ClientConn
|
||||
roomConn *grpc.ClientConn
|
||||
workers *serviceapp.BackgroundGroup
|
||||
closeOnce sync.Once
|
||||
server *grpc.Server
|
||||
listener net.Listener
|
||||
health *grpchealth.ServingChecker
|
||||
healthHTTP *healthhttp.Server
|
||||
mysqlRepo *mysqlstorage.Repository
|
||||
broadcast *broadcastservice.Service
|
||||
firstRechargeReward *firstrechargeservice.Service
|
||||
cumulativeRecharge *cumulativerechargeservice.Service
|
||||
inviteActivityReward *inviteactivityservice.Service
|
||||
actionConfirm *messageservice.ActionConfirmService
|
||||
wheel *wheelservice.Service
|
||||
activityTemplate *activitytemplateservice.Service
|
||||
broadcastWorkerEnabled bool
|
||||
messageActionWorkerEnabled bool
|
||||
wheelRewardWorkerEnabled bool
|
||||
activityTemplateStatsWorkerEnabled bool
|
||||
workerNodeID string
|
||||
messageActionWorkerOptions config.MessageActionConfirmWorkerConfig
|
||||
wheelRewardWorkerOptions config.WheelRewardWorkerConfig
|
||||
activityTemplateStatsWorkerOptions config.ActivityTemplateStatsWorkerConfig
|
||||
messageActionOutboxTopic string
|
||||
activityTemplateOutboxTopic string
|
||||
userLeaderboardRedisClose func() error
|
||||
mqConsumers []*rocketmqx.Consumer
|
||||
messageActionProducer *rocketmqx.Producer
|
||||
activityTemplateProducer *rocketmqx.Producer
|
||||
userConn *grpc.ClientConn
|
||||
walletConn *grpc.ClientConn
|
||||
roomConn *grpc.ClientConn
|
||||
workers *serviceapp.BackgroundGroup
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
// New 初始化 activity-service 应用。
|
||||
@ -69,10 +75,11 @@ func New(cfg config.Config) (*App, error) {
|
||||
var clients externalClients
|
||||
var mqConsumers []*rocketmqx.Consumer
|
||||
var messageActionProducer *rocketmqx.Producer
|
||||
var activityTemplateProducer *rocketmqx.Producer
|
||||
var userLeaderboardRedisClose func() error
|
||||
cleanup := func() {
|
||||
shutdownConsumers(mqConsumers)
|
||||
servicemq.ShutdownProducers([]*rocketmqx.Producer{messageActionProducer})
|
||||
servicemq.ShutdownProducers([]*rocketmqx.Producer{messageActionProducer, activityTemplateProducer})
|
||||
if userLeaderboardRedisClose != nil {
|
||||
_ = userLeaderboardRedisClose()
|
||||
}
|
||||
@ -120,6 +127,13 @@ func New(cfg config.Config) (*App, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if cfg.RocketMQ.ActivityTemplateOutbox.Enabled {
|
||||
activityTemplateProducer, err = rocketmqx.NewProducer(activityTemplateProducerConfig(cfg.RocketMQ))
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
health, healthHTTP, err := newHealthServers(cfg, server, repository)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
@ -127,31 +141,36 @@ func New(cfg config.Config) (*App, error) {
|
||||
}
|
||||
|
||||
return &App{
|
||||
server: server,
|
||||
listener: listener,
|
||||
health: health,
|
||||
healthHTTP: healthHTTP,
|
||||
mysqlRepo: repository,
|
||||
broadcast: services.broadcast,
|
||||
firstRechargeReward: services.firstRechargeReward,
|
||||
cumulativeRecharge: services.cumulativeRecharge,
|
||||
inviteActivityReward: services.inviteActivityReward,
|
||||
actionConfirm: services.actionConfirm,
|
||||
wheel: services.wheel,
|
||||
broadcastWorkerEnabled: cfg.Broadcast.Enabled,
|
||||
messageActionWorkerEnabled: cfg.MessageActionConfirmWorker.Enabled,
|
||||
wheelRewardWorkerEnabled: cfg.WheelRewardWorker.Enabled,
|
||||
workerNodeID: cfg.NodeID,
|
||||
messageActionWorkerOptions: cfg.MessageActionConfirmWorker,
|
||||
wheelRewardWorkerOptions: cfg.WheelRewardWorker,
|
||||
messageActionOutboxTopic: cfg.RocketMQ.MessageActionOutbox.Topic,
|
||||
userLeaderboardRedisClose: userLeaderboardRedisClose,
|
||||
mqConsumers: mqConsumers,
|
||||
messageActionProducer: messageActionProducer,
|
||||
userConn: clients.userConn,
|
||||
walletConn: clients.walletConn,
|
||||
roomConn: clients.roomConn,
|
||||
workers: serviceapp.NewBackground(context.Background()),
|
||||
server: server,
|
||||
listener: listener,
|
||||
health: health,
|
||||
healthHTTP: healthHTTP,
|
||||
mysqlRepo: repository,
|
||||
broadcast: services.broadcast,
|
||||
firstRechargeReward: services.firstRechargeReward,
|
||||
cumulativeRecharge: services.cumulativeRecharge,
|
||||
inviteActivityReward: services.inviteActivityReward,
|
||||
actionConfirm: services.actionConfirm,
|
||||
wheel: services.wheel,
|
||||
activityTemplate: services.activityTemplate,
|
||||
broadcastWorkerEnabled: cfg.Broadcast.Enabled,
|
||||
messageActionWorkerEnabled: cfg.MessageActionConfirmWorker.Enabled,
|
||||
wheelRewardWorkerEnabled: cfg.WheelRewardWorker.Enabled,
|
||||
activityTemplateStatsWorkerEnabled: cfg.ActivityTemplateStatsWorker.Enabled,
|
||||
workerNodeID: cfg.NodeID,
|
||||
messageActionWorkerOptions: cfg.MessageActionConfirmWorker,
|
||||
wheelRewardWorkerOptions: cfg.WheelRewardWorker,
|
||||
activityTemplateStatsWorkerOptions: cfg.ActivityTemplateStatsWorker,
|
||||
messageActionOutboxTopic: cfg.RocketMQ.MessageActionOutbox.Topic,
|
||||
activityTemplateOutboxTopic: cfg.RocketMQ.ActivityTemplateOutbox.Topic,
|
||||
userLeaderboardRedisClose: userLeaderboardRedisClose,
|
||||
mqConsumers: mqConsumers,
|
||||
messageActionProducer: messageActionProducer,
|
||||
activityTemplateProducer: activityTemplateProducer,
|
||||
userConn: clients.userConn,
|
||||
walletConn: clients.walletConn,
|
||||
roomConn: clients.roomConn,
|
||||
workers: serviceapp.NewBackground(context.Background()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -185,6 +204,11 @@ func (a *App) Run() error {
|
||||
})
|
||||
})
|
||||
}
|
||||
if a.activityTemplateStatsWorkerEnabled && a.activityTemplate != nil && a.activityTemplateProducer != nil && a.workers != nil {
|
||||
a.workers.Go(func(ctx context.Context) {
|
||||
a.runActivityTemplateStatsOutboxWorker(ctx)
|
||||
})
|
||||
}
|
||||
err := a.server.Serve(a.listener)
|
||||
if a.workers != nil {
|
||||
a.workers.StopAndWait()
|
||||
@ -248,7 +272,7 @@ func (a *App) closeHealthHTTP() {
|
||||
}
|
||||
|
||||
func (a *App) startMQ() error {
|
||||
if err := servicemq.StartProducers([]*rocketmqx.Producer{a.messageActionProducer}); err != nil {
|
||||
if err := servicemq.StartProducers([]*rocketmqx.Producer{a.messageActionProducer, a.activityTemplateProducer}); err != nil {
|
||||
return err
|
||||
}
|
||||
return servicemq.StartConsumers(a.mqConsumers)
|
||||
@ -256,5 +280,5 @@ func (a *App) startMQ() error {
|
||||
|
||||
func (a *App) shutdownMQ() {
|
||||
servicemq.ShutdownConsumers(a.mqConsumers)
|
||||
servicemq.ShutdownProducers([]*rocketmqx.Producer{a.messageActionProducer})
|
||||
servicemq.ShutdownProducers([]*rocketmqx.Producer{a.messageActionProducer, a.activityTemplateProducer})
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ func openRepository(ctx context.Context, cfg config.Config) (*mysqlstorage.Repos
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repository.SetActivityTemplateSettlementLateness(cfg.ActivityTemplateRuntime.SettlementLateness)
|
||||
if cfg.MySQLAutoMigrate {
|
||||
if err := repository.Migrate(ctx); err != nil {
|
||||
_ = repository.Close()
|
||||
|
||||
@ -8,11 +8,13 @@ import (
|
||||
|
||||
func registerGRPCServers(server *grpc.Server, services *serviceBundle) {
|
||||
activityv1.RegisterActivityServiceServer(server, grpcserver.NewServer(services.activity))
|
||||
activityv1.RegisterAdminActivityTemplateServiceServer(server, grpcserver.NewAdminActivityTemplateServer(services.activityTemplate))
|
||||
activityv1.RegisterActivityTemplateRuntimeServiceServer(server, grpcserver.NewActivityTemplateRuntimeServer(services.activityTemplate))
|
||||
activityv1.RegisterMessageInboxServiceServer(server, grpcserver.NewMessageServer(services.message))
|
||||
activityv1.RegisterMessageActionConfirmServiceServer(server, grpcserver.NewActionConfirmServer(services.actionConfirm))
|
||||
// cron 入口只做调度适配;具体状态机仍在各 activity service 内部。
|
||||
// weekly-star 和 room-turnover 都依赖 wallet 发奖,所以这里必须把同一批已装配的 service 注入 cron server。
|
||||
activityv1.RegisterActivityCronServiceServer(server, grpcserver.NewCronServer(services.message, services.growth, services.achievement, services.weeklyStar, services.roomTurnoverReward, services.agencyOpening, services.cpWeeklyRank))
|
||||
activityv1.RegisterActivityCronServiceServer(server, grpcserver.NewCronServer(services.message, services.growth, services.achievement, services.weeklyStar, services.roomTurnoverReward, services.agencyOpening, services.cpWeeklyRank, services.activityTemplate))
|
||||
activityv1.RegisterTaskServiceServer(server, grpcserver.NewTaskServer(services.task))
|
||||
activityv1.RegisterAdminTaskServiceServer(server, grpcserver.NewAdminTaskServer(services.task))
|
||||
activityv1.RegisterGrowthLevelServiceServer(server, grpcserver.NewGrowthLevelServer(services.growth))
|
||||
@ -36,7 +38,7 @@ func registerGRPCServers(server *grpc.Server, services *serviceBundle) {
|
||||
activityv1.RegisterAdminRoomTurnoverRewardServiceServer(server, grpcserver.NewAdminRoomTurnoverRewardServer(services.roomTurnoverReward))
|
||||
// BroadcastServer 同时承载 BroadcastService 和 RoomEventConsumerService。
|
||||
// gRPC 直连投递和 MQ 消费都必须经过相同的 room event fanout 顺序,否则本地验证会通过但线上 MQ 路径漏积分。
|
||||
broadcastServer := grpcserver.NewBroadcastServer(services.broadcast, services.growth, services.weeklyStar, services.task, services.roomTurnoverReward, services.agencyOpening)
|
||||
broadcastServer := grpcserver.NewBroadcastServer(services.broadcast, services.growth, services.weeklyStar, services.task, services.roomTurnoverReward, services.agencyOpening, services.activityTemplate)
|
||||
activityv1.RegisterBroadcastServiceServer(server, broadcastServer)
|
||||
activityv1.RegisterRoomEventConsumerServiceServer(server, broadcastServer)
|
||||
activityv1.RegisterFirstRechargeRewardServiceServer(server, grpcserver.NewFirstRechargeRewardServer(services.firstRechargeReward))
|
||||
|
||||
@ -27,6 +27,12 @@ func buildMQConsumers(cfg config.Config, services *serviceBundle) ([]*rocketmqx.
|
||||
return nil, err
|
||||
}
|
||||
mqConsumers = append(mqConsumers, consumer)
|
||||
activityTemplateConsumer, err := newActivityTemplateRoomOutboxConsumer(cfg, services)
|
||||
if err != nil {
|
||||
shutdownConsumers(mqConsumers)
|
||||
return nil, err
|
||||
}
|
||||
mqConsumers = append(mqConsumers, activityTemplateConsumer)
|
||||
}
|
||||
if cfg.RocketMQ.LuckyGiftOutbox.Enabled {
|
||||
consumer, err := newLuckyGiftOutboxConsumer(cfg, services)
|
||||
@ -177,6 +183,34 @@ func newRoomOutboxConsumer(cfg config.Config, services *serviceBundle) (*rocketm
|
||||
return consumer, nil
|
||||
}
|
||||
|
||||
func newActivityTemplateRoomOutboxConsumer(cfg config.Config, services *serviceBundle) (*rocketmqx.Consumer, error) {
|
||||
consumer, err := rocketmqx.NewConsumer(activityTemplateRoomOutboxConsumerConfig(cfg.RocketMQ))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tagExpression, err := roommq.LegacyCompatibleTagExpression(roommq.EventTypeRoomGiftSent)
|
||||
if err != nil {
|
||||
_ = consumer.Shutdown()
|
||||
return nil, err
|
||||
}
|
||||
if err := consumer.Subscribe(cfg.RocketMQ.RoomOutbox.Topic, tagExpression, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
|
||||
envelope, _, err := roommq.DecodeRoomOutboxMessage(message.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := services.activityTemplate.HandleRoomEvent(appcode.WithContext(ctx, envelope.GetAppCode()), envelope); err != nil {
|
||||
// Returning the error preserves RocketMQ retry/DLQ semantics on this dedicated consumer group without
|
||||
// delaying unrelated broadcast/growth/task projections.
|
||||
return fmt.Errorf("room outbox activity template projection: %w", err)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
_ = consumer.Shutdown()
|
||||
return nil, err
|
||||
}
|
||||
return consumer, nil
|
||||
}
|
||||
|
||||
func activityRoomEventTypes() []string {
|
||||
// 六个投影共用同一 consumer group,因此 selector 必须取其事件集合并集。
|
||||
// RoomGiftSent 驱动增长/任务/周星/流水,另外三个事件只驱动播报;其他房间
|
||||
@ -447,6 +481,14 @@ func roomOutboxConsumerConfig(cfg config.RocketMQConfig) rocketmqx.ConsumerConfi
|
||||
return consumerConfig
|
||||
}
|
||||
|
||||
func activityTemplateRoomOutboxConsumerConfig(cfg config.RocketMQConfig) rocketmqx.ConsumerConfig {
|
||||
consumerConfig := rocketMQConsumerConfig(cfg, cfg.RoomOutbox.ActivityTemplateConsumerGroup, cfg.RoomOutbox.ConsumerMaxReconsumeTimes)
|
||||
// Gift scoring serializes per queue to reduce rank/task row lock inversions; horizontal instances still
|
||||
// distribute RocketMQ queues inside this dedicated group.
|
||||
consumerConfig.ConsumeGoroutines = 1
|
||||
return consumerConfig
|
||||
}
|
||||
|
||||
func luckyGiftOutboxConsumerConfig(cfg config.RocketMQConfig) rocketmqx.ConsumerConfig {
|
||||
// 顶部飘屏是实时副作用,新建或误改 group 时不能从 topic 最早位点重放历史大奖;
|
||||
// 首次上线必须先部署 consumer,再开启 lucky-gift producer,确保 latest offset 不丢上线窗口事实。
|
||||
@ -503,6 +545,17 @@ func messageActionProducerConfig(cfg config.RocketMQConfig) rocketmqx.ProducerCo
|
||||
}
|
||||
}
|
||||
|
||||
func activityTemplateProducerConfig(cfg config.RocketMQConfig) rocketmqx.ProducerConfig {
|
||||
return rocketmqx.ProducerConfig{
|
||||
EndpointConfig: rocketmqx.EndpointConfig{
|
||||
NameServers: cfg.NameServers, NameServerDomain: cfg.NameServerDomain, AccessKey: cfg.AccessKey,
|
||||
SecretKey: cfg.SecretKey, SecurityToken: cfg.SecurityToken, Namespace: cfg.Namespace, VIPChannel: cfg.VIPChannel,
|
||||
},
|
||||
GroupName: cfg.ActivityTemplateOutbox.ProducerGroup, SendTimeout: cfg.ActivityTemplateOutbox.SendTimeout,
|
||||
Retry: cfg.ActivityTemplateOutbox.Retry,
|
||||
}
|
||||
}
|
||||
|
||||
func redPacketWalletOutboxTopic(cfg config.WalletOutboxMQConfig) string {
|
||||
// 钱包实时 topic 显式配置后,红包 worker 只消费实时通道,避免继续被普通账务 topic 的高频消息拖慢。
|
||||
if cfg.RealtimeTopic != "" {
|
||||
|
||||
@ -21,6 +21,19 @@ func TestRoomOutboxConsumerConfigSerializesCompositeProjection(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityTemplateRoomConsumerUsesIndependentGiftProjectionGroup(t *testing.T) {
|
||||
cfg := config.Default().RocketMQ
|
||||
cfg.RoomOutbox.ConsumerGroup = "activity-room-composite-test"
|
||||
cfg.RoomOutbox.ActivityTemplateConsumerGroup = "activity-template-gift-test"
|
||||
consumerConfig := activityTemplateRoomOutboxConsumerConfig(cfg)
|
||||
if consumerConfig.GroupName != cfg.RoomOutbox.ActivityTemplateConsumerGroup || consumerConfig.GroupName == cfg.RoomOutbox.ConsumerGroup {
|
||||
t.Fatalf("activity template consumer must own an independent offset: %+v", consumerConfig)
|
||||
}
|
||||
if consumerConfig.ConsumeGoroutines != 1 {
|
||||
t.Fatalf("activity template gift consumer goroutines = %d, want 1", consumerConfig.ConsumeGoroutines)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLuckyGiftOutboxConsumerUsesIndependentRealtimeGroup(t *testing.T) {
|
||||
cfg := config.Default().RocketMQ
|
||||
cfg.LuckyGiftOutbox.ConsumerGroup = "activity-lucky-gift-test"
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
"hyapp/services/activity-service/internal/config"
|
||||
achievementservice "hyapp/services/activity-service/internal/service/achievement"
|
||||
activityservice "hyapp/services/activity-service/internal/service/activity"
|
||||
activitytemplateservice "hyapp/services/activity-service/internal/service/activitytemplate"
|
||||
agencyopeningservice "hyapp/services/activity-service/internal/service/agencyopening"
|
||||
broadcastservice "hyapp/services/activity-service/internal/service/broadcast"
|
||||
cpweeklyrankservice "hyapp/services/activity-service/internal/service/cpweeklyrank"
|
||||
@ -30,6 +31,7 @@ import (
|
||||
|
||||
type serviceBundle struct {
|
||||
activity *activityservice.Service
|
||||
activityTemplate *activitytemplateservice.Service
|
||||
message *messageservice.Service
|
||||
actionConfirm *messageservice.ActionConfirmService
|
||||
task *taskservice.Service
|
||||
@ -53,9 +55,12 @@ type serviceBundle struct {
|
||||
func buildServiceBundle(cfg config.Config, repository *mysqlstorage.Repository, clients externalClients, userLeaderboardStore *userleaderboard.Store) (*serviceBundle, error) {
|
||||
walletClient := walletv1.NewWalletServiceClient(clients.walletConn)
|
||||
roomClient := roomv1.NewRoomQueryServiceClient(clients.roomConn)
|
||||
regionSource := activityclient.NewGRPCRegionSource(clients.userConn)
|
||||
|
||||
activitySvc := activityservice.New(activityservice.Config{NodeID: cfg.NodeID}, repository)
|
||||
activityTemplateSvc := activitytemplateservice.New(repository, walletClient, regionSource)
|
||||
messageSvc := messageservice.New(messageservice.Config{NodeID: cfg.NodeID}, repository, messageservice.WithTargetSource(activityclient.NewGRPCUserTargetSource(clients.userConn)))
|
||||
activityTemplateSvc.BindNotice(messageSvc)
|
||||
actionConfirmSvc := messageservice.NewActionConfirm(messageservice.Config{NodeID: cfg.NodeID}, repository, activityclient.NewGRPCRoleInvitationClient(clients.userConn))
|
||||
taskSvc := taskservice.New(repository, walletClient)
|
||||
registrationRewardSvc := registrationrewardservice.New(repository, walletClient)
|
||||
@ -92,7 +97,7 @@ func buildServiceBundle(cfg config.Config, repository *mysqlstorage.Repository,
|
||||
EnsureGroupsOnStartup: cfg.Broadcast.EnsureGroupsOnStartup,
|
||||
EnsureGroupsInterval: cfg.Broadcast.EnsureGroupsInterval,
|
||||
GroupIDPrefix: cfg.TencentIM.GroupIDPrefix,
|
||||
}, repository, broadcastPublisher, activityclient.NewGRPCRegionSource(clients.userConn))
|
||||
}, repository, broadcastPublisher, regionSource)
|
||||
userProfileSource := activityclient.NewGRPCUserProfileSource(clients.userConn)
|
||||
broadcastSvc.SetSenderProfileSource(userProfileSource)
|
||||
wheelSvc := wheelservice.New(repository, wheelservice.WithWallet(walletClient))
|
||||
@ -102,6 +107,7 @@ func buildServiceBundle(cfg config.Config, repository *mysqlstorage.Repository,
|
||||
|
||||
return &serviceBundle{
|
||||
activity: activitySvc,
|
||||
activityTemplate: activityTemplateSvc,
|
||||
message: messageSvc,
|
||||
actionConfirm: actionConfirmSvc,
|
||||
task: taskSvc,
|
||||
|
||||
@ -42,6 +42,10 @@ type Config struct {
|
||||
UserLeaderboardWorker UserLeaderboardWorkerConfig `yaml:"user_leaderboard_worker"`
|
||||
// WheelRewardWorker 补偿抽奖事务已经确定、但同步钱包发奖未完成的 WheelRewardSettlement outbox。
|
||||
WheelRewardWorker WheelRewardWorkerConfig `yaml:"wheel_reward_worker"`
|
||||
// ActivityTemplateStatsWorker relays committed runtime facts to statistics-service through RocketMQ.
|
||||
ActivityTemplateStatsWorker ActivityTemplateStatsWorkerConfig `yaml:"activity_template_stats_worker"`
|
||||
// ActivityTemplateRuntime 固化迟到事件水位等运行策略;发布时会写入物化周期,后续改配置不重写历史版本。
|
||||
ActivityTemplateRuntime ActivityTemplateRuntimeConfig `yaml:"activity_template_runtime"`
|
||||
// TencentIM 是 activity-service 发送全局/区域播报群消息的 REST 配置。
|
||||
TencentIM TencentIMConfig `yaml:"tencent_im"`
|
||||
// Broadcast 控制 IM 播报群 reconciler、outbox worker 和贵重礼物阈值。
|
||||
@ -133,6 +137,17 @@ type WheelRewardWorkerConfig struct {
|
||||
MaxRetry int `yaml:"max_retry"`
|
||||
}
|
||||
|
||||
type ActivityTemplateStatsWorkerConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
PollInterval time.Duration `yaml:"poll_interval"`
|
||||
BatchSize int `yaml:"batch_size"`
|
||||
LockTTL time.Duration `yaml:"lock_ttl"`
|
||||
}
|
||||
|
||||
type ActivityTemplateRuntimeConfig struct {
|
||||
SettlementLateness time.Duration `yaml:"settlement_lateness"`
|
||||
}
|
||||
|
||||
// TencentIMConfig 描述 activity-service 调用腾讯云 IM REST API 的配置。
|
||||
type TencentIMConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
@ -182,27 +197,29 @@ type BroadcastConfig struct {
|
||||
|
||||
// RocketMQConfig 描述 activity-service 消费房间事实的 MQ 连接。
|
||||
type RocketMQConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
NameServers []string `yaml:"name_servers"`
|
||||
NameServerDomain string `yaml:"name_server_domain"`
|
||||
AccessKey string `yaml:"access_key"`
|
||||
SecretKey string `yaml:"secret_key"`
|
||||
SecurityToken string `yaml:"security_token"`
|
||||
Namespace string `yaml:"namespace"`
|
||||
VIPChannel bool `yaml:"vip_channel"`
|
||||
RoomOutbox RoomOutboxMQConfig `yaml:"room_outbox"`
|
||||
LuckyGiftOutbox LuckyGiftOutboxMQConfig `yaml:"lucky_gift_outbox"`
|
||||
WalletOutbox WalletOutboxMQConfig `yaml:"wallet_outbox"`
|
||||
UserOutbox UserOutboxMQConfig `yaml:"user_outbox"`
|
||||
MessageActionOutbox MessageActionOutboxMQConfig `yaml:"message_action_outbox"`
|
||||
Enabled bool `yaml:"enabled"`
|
||||
NameServers []string `yaml:"name_servers"`
|
||||
NameServerDomain string `yaml:"name_server_domain"`
|
||||
AccessKey string `yaml:"access_key"`
|
||||
SecretKey string `yaml:"secret_key"`
|
||||
SecurityToken string `yaml:"security_token"`
|
||||
Namespace string `yaml:"namespace"`
|
||||
VIPChannel bool `yaml:"vip_channel"`
|
||||
RoomOutbox RoomOutboxMQConfig `yaml:"room_outbox"`
|
||||
LuckyGiftOutbox LuckyGiftOutboxMQConfig `yaml:"lucky_gift_outbox"`
|
||||
WalletOutbox WalletOutboxMQConfig `yaml:"wallet_outbox"`
|
||||
UserOutbox UserOutboxMQConfig `yaml:"user_outbox"`
|
||||
MessageActionOutbox MessageActionOutboxMQConfig `yaml:"message_action_outbox"`
|
||||
ActivityTemplateOutbox ActivityTemplateOutboxMQConfig `yaml:"activity_template_outbox"`
|
||||
}
|
||||
|
||||
// RoomOutboxMQConfig 控制 activity 对 room_outbox topic 的消费位点。
|
||||
type RoomOutboxMQConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Topic string `yaml:"topic"`
|
||||
ConsumerGroup string `yaml:"consumer_group"`
|
||||
ConsumerMaxReconsumeTimes int32 `yaml:"consumer_max_reconsume_times"`
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Topic string `yaml:"topic"`
|
||||
ConsumerGroup string `yaml:"consumer_group"`
|
||||
ActivityTemplateConsumerGroup string `yaml:"activity_template_consumer_group"`
|
||||
ConsumerMaxReconsumeTimes int32 `yaml:"consumer_max_reconsume_times"`
|
||||
}
|
||||
|
||||
// LuckyGiftOutboxMQConfig 控制 activity 对幸运礼物 owner fact 的独立消费位点。
|
||||
@ -249,6 +266,14 @@ type MessageActionOutboxMQConfig struct {
|
||||
Retry int `yaml:"retry"`
|
||||
}
|
||||
|
||||
type ActivityTemplateOutboxMQConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Topic string `yaml:"topic"`
|
||||
ProducerGroup string `yaml:"producer_group"`
|
||||
SendTimeout time.Duration `yaml:"send_timeout"`
|
||||
Retry int `yaml:"retry"`
|
||||
}
|
||||
|
||||
// Default 返回本地默认配置。
|
||||
func Default() Config {
|
||||
return Config{
|
||||
@ -287,6 +312,10 @@ func Default() Config {
|
||||
LockTTL: 30 * time.Second,
|
||||
MaxRetry: 16,
|
||||
},
|
||||
ActivityTemplateStatsWorker: ActivityTemplateStatsWorkerConfig{
|
||||
Enabled: true, PollInterval: time.Second, BatchSize: 100, LockTTL: 30 * time.Second,
|
||||
},
|
||||
ActivityTemplateRuntime: ActivityTemplateRuntimeConfig{SettlementLateness: 10 * time.Minute},
|
||||
MessageActionConfirmWorker: MessageActionConfirmWorkerConfig{
|
||||
Enabled: true,
|
||||
OutboxPollInterval: time.Second,
|
||||
@ -335,10 +364,11 @@ func defaultRocketMQConfig() RocketMQConfig {
|
||||
return RocketMQConfig{
|
||||
Enabled: false,
|
||||
RoomOutbox: RoomOutboxMQConfig{
|
||||
Enabled: false,
|
||||
Topic: "hyapp_room_outbox",
|
||||
ConsumerGroup: "hyapp-activity-room-outbox",
|
||||
ConsumerMaxReconsumeTimes: 16,
|
||||
Enabled: false,
|
||||
Topic: "hyapp_room_outbox",
|
||||
ConsumerGroup: "hyapp-activity-room-outbox",
|
||||
ActivityTemplateConsumerGroup: "hyapp-activity-template-room-outbox",
|
||||
ConsumerMaxReconsumeTimes: 16,
|
||||
},
|
||||
LuckyGiftOutbox: LuckyGiftOutboxMQConfig{
|
||||
Enabled: false,
|
||||
@ -375,6 +405,10 @@ func defaultRocketMQConfig() RocketMQConfig {
|
||||
SendTimeout: 5 * time.Second,
|
||||
Retry: 2,
|
||||
},
|
||||
ActivityTemplateOutbox: ActivityTemplateOutboxMQConfig{
|
||||
Enabled: false, Topic: "hyapp_activity_outbox", ProducerGroup: "hyapp-activity-template-outbox",
|
||||
SendTimeout: 5 * time.Second, Retry: 2,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -484,6 +518,21 @@ func Load(path string) (Config, error) {
|
||||
if cfg.WheelRewardWorker.MaxRetry <= 0 {
|
||||
cfg.WheelRewardWorker.MaxRetry = 16
|
||||
}
|
||||
if cfg.ActivityTemplateStatsWorker.PollInterval <= 0 {
|
||||
cfg.ActivityTemplateStatsWorker.PollInterval = time.Second
|
||||
}
|
||||
if cfg.ActivityTemplateStatsWorker.BatchSize <= 0 {
|
||||
cfg.ActivityTemplateStatsWorker.BatchSize = 100
|
||||
}
|
||||
if cfg.ActivityTemplateStatsWorker.LockTTL <= 0 {
|
||||
cfg.ActivityTemplateStatsWorker.LockTTL = 30 * time.Second
|
||||
}
|
||||
if cfg.ActivityTemplateRuntime.SettlementLateness <= 0 {
|
||||
cfg.ActivityTemplateRuntime.SettlementLateness = 10 * time.Minute
|
||||
}
|
||||
if cfg.ActivityTemplateRuntime.SettlementLateness > 30*time.Minute {
|
||||
return Config{}, errors.New("activity_template_runtime.settlement_lateness must not exceed 30m")
|
||||
}
|
||||
rocketMQ, err := normalizeRocketMQConfig(cfg.RocketMQ)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
@ -506,6 +555,9 @@ func Load(path string) (Config, error) {
|
||||
if cfg.MessageActionConfirmWorker.Enabled && (!cfg.RocketMQ.UserOutbox.Enabled || !cfg.RocketMQ.MessageActionOutbox.Enabled) {
|
||||
return Config{}, errors.New("message action confirm worker requires rocketmq.user_outbox.enabled and rocketmq.message_action_outbox.enabled")
|
||||
}
|
||||
if cfg.ActivityTemplateStatsWorker.Enabled && !cfg.RocketMQ.ActivityTemplateOutbox.Enabled {
|
||||
return Config{}, errors.New("activity template stats worker requires rocketmq.activity_template_outbox.enabled")
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
@ -540,6 +592,9 @@ func normalizeRocketMQConfig(cfg RocketMQConfig) (RocketMQConfig, error) {
|
||||
if cfg.RoomOutbox.ConsumerGroup = strings.TrimSpace(cfg.RoomOutbox.ConsumerGroup); cfg.RoomOutbox.ConsumerGroup == "" {
|
||||
cfg.RoomOutbox.ConsumerGroup = defaults.RoomOutbox.ConsumerGroup
|
||||
}
|
||||
if cfg.RoomOutbox.ActivityTemplateConsumerGroup = strings.TrimSpace(cfg.RoomOutbox.ActivityTemplateConsumerGroup); cfg.RoomOutbox.ActivityTemplateConsumerGroup == "" {
|
||||
cfg.RoomOutbox.ActivityTemplateConsumerGroup = defaults.RoomOutbox.ActivityTemplateConsumerGroup
|
||||
}
|
||||
if cfg.RoomOutbox.ConsumerMaxReconsumeTimes <= 0 {
|
||||
cfg.RoomOutbox.ConsumerMaxReconsumeTimes = defaults.RoomOutbox.ConsumerMaxReconsumeTimes
|
||||
}
|
||||
@ -610,7 +665,19 @@ func normalizeRocketMQConfig(cfg RocketMQConfig) (RocketMQConfig, error) {
|
||||
if cfg.MessageActionOutbox.Retry < 0 {
|
||||
cfg.MessageActionOutbox.Retry = defaults.MessageActionOutbox.Retry
|
||||
}
|
||||
if cfg.RoomOutbox.Enabled || cfg.LuckyGiftOutbox.Enabled || cfg.WalletOutbox.Enabled || cfg.UserOutbox.Enabled || cfg.MessageActionOutbox.Enabled {
|
||||
if cfg.ActivityTemplateOutbox.Topic = strings.TrimSpace(cfg.ActivityTemplateOutbox.Topic); cfg.ActivityTemplateOutbox.Topic == "" {
|
||||
cfg.ActivityTemplateOutbox.Topic = defaults.ActivityTemplateOutbox.Topic
|
||||
}
|
||||
if cfg.ActivityTemplateOutbox.ProducerGroup = strings.TrimSpace(cfg.ActivityTemplateOutbox.ProducerGroup); cfg.ActivityTemplateOutbox.ProducerGroup == "" {
|
||||
cfg.ActivityTemplateOutbox.ProducerGroup = defaults.ActivityTemplateOutbox.ProducerGroup
|
||||
}
|
||||
if cfg.ActivityTemplateOutbox.SendTimeout <= 0 {
|
||||
cfg.ActivityTemplateOutbox.SendTimeout = defaults.ActivityTemplateOutbox.SendTimeout
|
||||
}
|
||||
if cfg.ActivityTemplateOutbox.Retry < 0 {
|
||||
cfg.ActivityTemplateOutbox.Retry = defaults.ActivityTemplateOutbox.Retry
|
||||
}
|
||||
if cfg.RoomOutbox.Enabled || cfg.LuckyGiftOutbox.Enabled || cfg.WalletOutbox.Enabled || cfg.UserOutbox.Enabled || cfg.MessageActionOutbox.Enabled || cfg.ActivityTemplateOutbox.Enabled {
|
||||
cfg.Enabled = true
|
||||
}
|
||||
if cfg.Enabled {
|
||||
|
||||
@ -0,0 +1,207 @@
|
||||
package activitytemplate
|
||||
|
||||
const (
|
||||
// ActivityTypeGiftChallenge 是当前原型对应的礼物挑战赛;类型字段保留,后续模板类型可复用同一生命周期框架。
|
||||
ActivityTypeGiftChallenge = "gift_challenge"
|
||||
|
||||
StatusDraft = "draft"
|
||||
StatusPublished = "published"
|
||||
StatusDisabled = "disabled"
|
||||
StatusArchived = "archived"
|
||||
|
||||
LifecycleDraft = "draft"
|
||||
LifecycleScheduled = "scheduled"
|
||||
LifecycleOngoing = "ongoing"
|
||||
LifecycleEnded = "ended"
|
||||
LifecycleDisabled = "disabled"
|
||||
LifecycleArchived = "archived"
|
||||
|
||||
TaskTypeDailyVisit = "daily_visit"
|
||||
TaskTypeGiftCount = "gift_count"
|
||||
TaskTypeGiftCoinAmount = "gift_coin_amount"
|
||||
|
||||
BoardTypeDaily = "daily"
|
||||
BoardTypeTotal = "total"
|
||||
|
||||
SharedAssetLocale = "*"
|
||||
)
|
||||
|
||||
// Locale 是活动页的单语言文案。运行侧先匹配用户语言,再回退到英语,避免把后台内部名称暴露给客户端。
|
||||
type Locale struct {
|
||||
Locale string `json:"locale"`
|
||||
Title string `json:"title"`
|
||||
Rules string `json:"rules"`
|
||||
}
|
||||
|
||||
// Gift 是参与任务和排行榜计分的礼物引用;礼物价格和上下架状态仍由礼物 owner 服务维护。
|
||||
type Gift struct {
|
||||
GiftID string `json:"gift_id"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
Name string `json:"name"`
|
||||
IconURL string `json:"icon_url"`
|
||||
CoinPrice int64 `json:"coin_price"`
|
||||
PriceVersion string `json:"price_version"`
|
||||
}
|
||||
|
||||
// RewardItem is the safe client-facing projection copied from a wallet-owned pinned resource-group
|
||||
// snapshot. It intentionally excludes grant strategies, admin IDs and mutable catalog metadata.
|
||||
type RewardItem struct {
|
||||
ItemType string `json:"item_type"`
|
||||
ResourceID int64 `json:"resource_id"`
|
||||
ResourceType string `json:"resource_type"`
|
||||
Name string `json:"name"`
|
||||
IconURL string `json:"icon_url"`
|
||||
Quantity int64 `json:"quantity"`
|
||||
DurationMS int64 `json:"duration_ms"`
|
||||
WalletAssetType string `json:"wallet_asset_type"`
|
||||
WalletAssetAmount int64 `json:"wallet_asset_amount"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
}
|
||||
|
||||
type RewardSnapshot struct {
|
||||
SourceGroupID int64 `json:"source_group_id"`
|
||||
SnapshotID string `json:"snapshot_id"`
|
||||
VersionNo int64 `json:"version_no"`
|
||||
SnapshotHash string `json:"snapshot_hash"`
|
||||
Name string `json:"name"`
|
||||
Items []RewardItem `json:"items"`
|
||||
}
|
||||
|
||||
// Task 是活动实例内的每日任务档位。奖励引用资源组,使复杂资产组合仍由 wallet-service 原子发放。
|
||||
type Task struct {
|
||||
TaskKey string `json:"task_key"`
|
||||
TaskType string `json:"task_type"`
|
||||
TargetValue int64 `json:"target_value"`
|
||||
RewardResourceGroupID int64 `json:"reward_resource_group_id"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
RewardSnapshot RewardSnapshot `json:"reward_snapshot"`
|
||||
}
|
||||
|
||||
// RankReward 是一个不重叠的名次区间。区间使用闭区间,活动周期本身仍使用 [start_ms,end_ms)。
|
||||
type RankReward struct {
|
||||
BoardType string `json:"board_type"`
|
||||
RankFrom int32 `json:"rank_from"`
|
||||
RankTo int32 `json:"rank_to"`
|
||||
ResourceGroupID int64 `json:"resource_group_id"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
RewardSnapshot RewardSnapshot `json:"reward_snapshot"`
|
||||
}
|
||||
|
||||
// Asset 是活动页素材清单;Locale="*" 是共享素材,具体语言素材覆盖共享项。
|
||||
type Asset struct {
|
||||
AssetKey string `json:"asset_key"`
|
||||
Locale string `json:"locale"`
|
||||
URL string `json:"url"`
|
||||
MediaType string `json:"media_type"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
}
|
||||
|
||||
// Template 是运营后台称作“活动模版”的可排期活动实例。
|
||||
// Revision 用于后台乐观锁,PublishedVersion 指向最近一次发布形成的不可变完整快照,两者不能混用。
|
||||
type Template struct {
|
||||
AppCode string `json:"app_code"`
|
||||
TemplateID string `json:"template_id"`
|
||||
TemplateCode string `json:"template_code"`
|
||||
Name string `json:"name"`
|
||||
ActivityType string `json:"activity_type"`
|
||||
Status string `json:"status"`
|
||||
LifecycleStatus string `json:"lifecycle_status"`
|
||||
StartMS int64 `json:"start_ms"`
|
||||
EndMS int64 `json:"end_ms"`
|
||||
AllRegions bool `json:"all_regions"`
|
||||
RegionIDs []int64 `json:"region_ids"`
|
||||
Locales []Locale `json:"locales"`
|
||||
Gifts []Gift `json:"gifts"`
|
||||
Tasks []Task `json:"tasks"`
|
||||
DailyLeaderboardSize int32 `json:"daily_leaderboard_size"`
|
||||
TotalLeaderboardSize int32 `json:"total_leaderboard_size"`
|
||||
RankRewards []RankReward `json:"rank_rewards"`
|
||||
Assets []Asset `json:"assets"`
|
||||
DisplayConfigJSON string `json:"display_config_json"`
|
||||
Revision int64 `json:"revision"`
|
||||
PublishedVersion int64 `json:"published_version"`
|
||||
CreatedByAdminID int64 `json:"created_by_admin_id"`
|
||||
UpdatedByAdminID int64 `json:"updated_by_admin_id"`
|
||||
PublishedByAdminID int64 `json:"published_by_admin_id"`
|
||||
CreatedAtMS int64 `json:"created_at_ms"`
|
||||
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||
PublishedAtMS int64 `json:"published_at_ms"`
|
||||
ArchivedAtMS int64 `json:"archived_at_ms"`
|
||||
}
|
||||
|
||||
// Summary 是列表专用轻量投影,只附带区域范围,不加载规则、任务、榜单奖励和素材大字段。
|
||||
type Summary struct {
|
||||
AppCode string
|
||||
TemplateID string
|
||||
TemplateCode string
|
||||
Name string
|
||||
ActivityType string
|
||||
Status string
|
||||
LifecycleStatus string
|
||||
StartMS int64
|
||||
EndMS int64
|
||||
AllRegions bool
|
||||
RegionIDs []int64
|
||||
Revision int64
|
||||
PublishedVersion int64
|
||||
CreatedByAdminID int64
|
||||
UpdatedByAdminID int64
|
||||
CreatedAtMS int64
|
||||
UpdatedAtMS int64
|
||||
}
|
||||
|
||||
// ListQuery 是后台列表筛选。Status 同时接受持久状态和 scheduled/ongoing/ended 生命周期状态。
|
||||
type ListQuery struct {
|
||||
Keyword string
|
||||
Status string
|
||||
RegionID *int64
|
||||
AllRegions *bool
|
||||
StartMS int64
|
||||
EndMS int64
|
||||
Page int32
|
||||
PageSize int32
|
||||
NowMS int64
|
||||
}
|
||||
|
||||
// UpdateCommand 承载整份配置替换;子配置必须和主表在一个事务里提交,避免读到半份模板。
|
||||
type UpdateCommand struct {
|
||||
Template Template
|
||||
ExpectedRevision int64
|
||||
OperatorAdminID int64
|
||||
}
|
||||
|
||||
// StatusCommand 是发布、停用和软归档的乐观锁命令。
|
||||
type StatusCommand struct {
|
||||
TemplateID string
|
||||
Status string
|
||||
ExpectedRevision int64
|
||||
OperatorAdminID int64
|
||||
RewardSnapshots map[int64]RewardSnapshot
|
||||
}
|
||||
|
||||
// Version 是一次发布的不可变快照。Snapshot 让后台可以审阅历史版本,运行侧也能按版本稳定读取配置。
|
||||
type Version struct {
|
||||
TemplateID string
|
||||
VersionNo int64
|
||||
Snapshot Template
|
||||
PublishedByAdminID int64
|
||||
PublishedAtMS int64
|
||||
RuntimeFromMS int64
|
||||
RuntimeToMS int64
|
||||
}
|
||||
|
||||
// CloneCommand 明确复制源版本和目标业务编码;源版本为 0 时复制当前编辑态配置。
|
||||
type CloneCommand struct {
|
||||
SourceTemplateID string
|
||||
SourceVersion int64
|
||||
TemplateCode string
|
||||
Name string
|
||||
OperatorAdminID int64
|
||||
}
|
||||
|
||||
// ValidationIssue 是可绑定到表单字段的稳定校验结果;Code 给前端做国际化,Message 便于后台直接展示。
|
||||
type ValidationIssue struct {
|
||||
Field string
|
||||
Code string
|
||||
Message string
|
||||
}
|
||||
@ -0,0 +1,225 @@
|
||||
package activitytemplate
|
||||
|
||||
const (
|
||||
TaskStatusInProgress = "in_progress"
|
||||
TaskStatusCompleted = "completed"
|
||||
TaskStatusClaimed = "claimed"
|
||||
|
||||
ClaimStatusPending = "pending"
|
||||
ClaimStatusRunning = "running"
|
||||
ClaimStatusGranted = "granted"
|
||||
ClaimStatusFailed = "failed"
|
||||
ClaimStatusDead = "dead"
|
||||
|
||||
PeriodStatusPending = "pending"
|
||||
PeriodStatusSettling = "settling"
|
||||
PeriodStatusRewarding = "rewarding"
|
||||
PeriodStatusSettled = "settled"
|
||||
PeriodStatusCancelled = "cancelled"
|
||||
|
||||
EventStatusConsumed = "consumed"
|
||||
EventStatusSkipped = "skipped"
|
||||
)
|
||||
|
||||
// Runtime 是一次发布形成的不可变运行版本及其真实启停窗口。
|
||||
// Template 来自 activity_template_versions.snapshot_json,不读取后台当前编辑态。
|
||||
type Runtime struct {
|
||||
Template Template
|
||||
VersionNo int64
|
||||
RuntimeFromMS int64
|
||||
RuntimeToMS int64
|
||||
}
|
||||
|
||||
// GiftEvent 是 room-service 已提交并完成钱包扣费的送礼事实。
|
||||
type GiftEvent struct {
|
||||
EventID string
|
||||
UserID int64
|
||||
GiftID string
|
||||
GiftCount int64
|
||||
CoinAmount int64
|
||||
RegionID int64
|
||||
OccurredAtMS int64
|
||||
}
|
||||
|
||||
type EventResult struct {
|
||||
EventID string
|
||||
Status string
|
||||
SkipReason string
|
||||
TemplateID string
|
||||
TemplateCode string
|
||||
VersionNo int64
|
||||
TaskDay string
|
||||
}
|
||||
|
||||
// TaskProgress 固化任务目标和奖励资源组,后台后续发布新版本不会改变已产生的进度与领奖内容。
|
||||
type TaskProgress struct {
|
||||
AppCode string
|
||||
TemplateID string
|
||||
TemplateCode string
|
||||
VersionNo int64
|
||||
TaskDay string
|
||||
UserID int64
|
||||
TaskKey string
|
||||
TaskType string
|
||||
TargetValue int64
|
||||
ProgressValue int64
|
||||
RewardResourceGroupID int64
|
||||
RewardSnapshot RewardSnapshot
|
||||
SortOrder int32
|
||||
Status string
|
||||
CompletedAtMS int64
|
||||
ClaimedAtMS int64
|
||||
UpdatedAtMS int64
|
||||
FirstRegionID int64
|
||||
EarningRegionID int64
|
||||
AttributionAtMS int64
|
||||
}
|
||||
|
||||
type VisitCommand struct {
|
||||
UserID int64
|
||||
RegionID int64
|
||||
TemplateCode string
|
||||
CommandID string
|
||||
VersionNo int64
|
||||
}
|
||||
|
||||
type VisitResult struct {
|
||||
Runtime Runtime
|
||||
Tasks []TaskProgress
|
||||
FirstVisit bool
|
||||
}
|
||||
|
||||
type ClaimCommand struct {
|
||||
UserID int64
|
||||
RegionID int64
|
||||
TemplateCode string
|
||||
VersionNo int64
|
||||
TaskDay string
|
||||
TaskKey string
|
||||
CommandID string
|
||||
}
|
||||
|
||||
type TaskClaim struct {
|
||||
AppCode string
|
||||
ClaimID string
|
||||
CommandID string
|
||||
TemplateID string
|
||||
TemplateCode string
|
||||
VersionNo int64
|
||||
TaskDay string
|
||||
TaskKey string
|
||||
UserID int64
|
||||
RewardResourceGroupID int64
|
||||
RewardSnapshot RewardSnapshot
|
||||
WalletCommandID string
|
||||
WalletGrantID string
|
||||
Status string
|
||||
FailureReason string
|
||||
AttemptCount int32
|
||||
CreatedAtMS int64
|
||||
UpdatedAtMS int64
|
||||
GrantedAtMS int64
|
||||
EarningRegionID int64
|
||||
AttributionAtMS int64
|
||||
}
|
||||
|
||||
type TaskClaimQuery struct {
|
||||
TemplateID string
|
||||
VersionNo int64
|
||||
TaskDay string
|
||||
TaskKey string
|
||||
UserID int64
|
||||
Status string
|
||||
Page int32
|
||||
PageSize int32
|
||||
}
|
||||
|
||||
type LeaderboardEntry struct {
|
||||
RankNo int32
|
||||
UserID int64
|
||||
Score int64
|
||||
GiftCount int64
|
||||
CoinAmount int64
|
||||
FirstScoredAtMS int64
|
||||
Snapshot bool
|
||||
}
|
||||
|
||||
type RankPeriod struct {
|
||||
AppCode string
|
||||
TemplateID string
|
||||
TemplateCode string
|
||||
VersionNo int64
|
||||
BoardType string
|
||||
PeriodKey string
|
||||
StartMS int64
|
||||
EndMS int64
|
||||
SettleAfterMS int64
|
||||
LeaderboardSize int32
|
||||
Status string
|
||||
SettledAtMS int64
|
||||
}
|
||||
|
||||
type LeaderboardQuery struct {
|
||||
TemplateID string
|
||||
TemplateCode string
|
||||
VersionNo int64
|
||||
RegionID int64
|
||||
UserID int64
|
||||
BoardType string
|
||||
PeriodKey string
|
||||
Page int32
|
||||
PageSize int32
|
||||
NowMS int64
|
||||
}
|
||||
|
||||
type LeaderboardResult struct {
|
||||
Runtime Runtime
|
||||
Period RankPeriod
|
||||
Entries []LeaderboardEntry
|
||||
MyEntry LeaderboardEntry
|
||||
MyFound bool
|
||||
Total int64
|
||||
}
|
||||
|
||||
type RewardJob struct {
|
||||
AppCode string
|
||||
RewardJobID string
|
||||
TemplateID string
|
||||
TemplateCode string
|
||||
VersionNo int64
|
||||
BoardType string
|
||||
PeriodKey string
|
||||
RankNo int32
|
||||
UserID int64
|
||||
Score int64
|
||||
ResourceGroupID int64
|
||||
RewardSnapshot RewardSnapshot
|
||||
WalletCommandID string
|
||||
WalletGrantID string
|
||||
Status string
|
||||
FailureReason string
|
||||
AttemptCount int32
|
||||
MaxAttempts int32
|
||||
NextRetryAtMS int64
|
||||
CreatedAtMS int64
|
||||
UpdatedAtMS int64
|
||||
GrantedAtMS int64
|
||||
}
|
||||
|
||||
type RewardJobQuery struct {
|
||||
TemplateID string
|
||||
VersionNo int64
|
||||
BoardType string
|
||||
PeriodKey string
|
||||
Status string
|
||||
Page int32
|
||||
PageSize int32
|
||||
}
|
||||
|
||||
// StatsOutboxRecord 是投递到 statistics-service 的 owner 事实;业务事务只写本地 outbox,MQ 失败不回滚计分。
|
||||
type StatsOutboxRecord struct {
|
||||
OutboxID string
|
||||
EventType string
|
||||
Payload []byte
|
||||
RetryCount int32
|
||||
}
|
||||
@ -0,0 +1,671 @@
|
||||
package activitytemplate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/proto"
|
||||
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||
"hyapp/pkg/activitymq"
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/idgen"
|
||||
"hyapp/pkg/logx"
|
||||
"hyapp/pkg/rocketmqx"
|
||||
"hyapp/pkg/xerr"
|
||||
domain "hyapp/services/activity-service/internal/domain/activitytemplate"
|
||||
messageservice "hyapp/services/activity-service/internal/service/message"
|
||||
)
|
||||
|
||||
const (
|
||||
activityTemplateTaskRewardReason = "activity template daily task reward"
|
||||
activityTemplateRankRewardReason = "activity template leaderboard reward"
|
||||
activityTemplateTaskGrantSource = "activity_template_task"
|
||||
activityTemplateRankGrantSource = "activity_template_rank"
|
||||
)
|
||||
|
||||
// RuntimeRepository is intentionally separate from the configuration Repository so legacy unit-test fakes
|
||||
// remain small. Production MySQL implements both and New binds them through a checked type assertion.
|
||||
type RuntimeRepository interface {
|
||||
FindCurrentActivityTemplateRuntime(ctx context.Context, templateCode string, regionID int64, atMS int64) (domain.Runtime, bool, error)
|
||||
FindActivityTemplateRuntimeForPeriod(ctx context.Context, templateCode string, versionNo int64, regionID int64, boardType string, periodKey string, atMS int64) (domain.Runtime, bool, error)
|
||||
FindActivityTemplateRuntimeForUserTask(ctx context.Context, templateCode string, versionNo int64, userID int64, taskDay string, taskKey string) (domain.Runtime, bool, error)
|
||||
FindActivityTemplateRuntimeForExactLeaderboard(ctx context.Context, templateCode string, versionNo int64, boardType string, periodKey string, atMS int64) (domain.Runtime, bool, error)
|
||||
GetActivityTemplateRuntimeVersion(ctx context.Context, templateID string, versionNo int64) (domain.Runtime, error)
|
||||
ConsumeActivityTemplateGiftEvent(ctx context.Context, event domain.GiftEvent, nowMS int64) (domain.EventResult, error)
|
||||
VisitActivityTemplate(ctx context.Context, command domain.VisitCommand, nowMS int64) (domain.VisitResult, error)
|
||||
ListActivityTemplateTaskProgress(ctx context.Context, runtime domain.Runtime, userID int64, taskDay string) ([]domain.TaskProgress, error)
|
||||
PrepareActivityTemplateTaskClaim(ctx context.Context, runtime domain.Runtime, command domain.ClaimCommand, nowMS int64) (domain.TaskClaim, error)
|
||||
MarkActivityTemplateTaskClaimGranted(ctx context.Context, claimID string, walletGrantID string, nowMS int64) (domain.TaskClaim, error)
|
||||
MarkActivityTemplateTaskClaimFailed(ctx context.Context, claimID string, reason string, nowMS int64) error
|
||||
ListActivityTemplateRuntimeLeaderboard(ctx context.Context, runtime domain.Runtime, query domain.LeaderboardQuery) (domain.LeaderboardResult, error)
|
||||
ClaimDueActivityTemplateRankPeriods(ctx context.Context, workerID string, nowMS int64, lockTTL time.Duration, batchSize int32) ([]domain.RankPeriod, error)
|
||||
PrepareActivityTemplateRankSettlement(ctx context.Context, period domain.RankPeriod, nowMS int64) ([]domain.RewardJob, error)
|
||||
ClaimFinalizableActivityTemplateRankPeriods(ctx context.Context, workerID string, nowMS int64, lockTTL time.Duration, batchSize int32) ([]domain.RankPeriod, error)
|
||||
ClaimActivityTemplateRewardJobs(ctx context.Context, workerID string, nowMS int64, lockTTL time.Duration, batchSize int32) ([]domain.RewardJob, error)
|
||||
ExtendActivityTemplateRewardJobLease(ctx context.Context, rewardJobID string, workerID string, nowMS int64, lockTTL time.Duration) error
|
||||
MarkActivityTemplateRewardJobGrantedWithLease(ctx context.Context, rewardJobID string, workerID string, walletGrantID string, nowMS int64) error
|
||||
MarkActivityTemplateRewardJobFailedWithLease(ctx context.Context, rewardJobID string, workerID string, reason string, nowMS int64) error
|
||||
FinishActivityTemplateRankPeriodIfComplete(ctx context.Context, period domain.RankPeriod, nowMS int64) (bool, error)
|
||||
ListActivityTemplateRewardJobs(ctx context.Context, query domain.RewardJobQuery) ([]domain.RewardJob, int64, error)
|
||||
ClaimActivityTemplateRewardJobForManualRetry(ctx context.Context, templateID string, rewardJobID string, workerID string, nowMS int64, lockTTL time.Duration) (domain.RewardJob, error)
|
||||
ListActivityTemplateTaskClaims(ctx context.Context, query domain.TaskClaimQuery) ([]domain.TaskClaim, int64, error)
|
||||
ClaimActivityTemplateTaskClaimForRetry(ctx context.Context, templateID string, claimID string, nowMS int64) (domain.TaskClaim, error)
|
||||
ClaimActivityTemplateStatsOutbox(ctx context.Context, workerID string, nowMS int64, lockTTL time.Duration, batchSize int) ([]domain.StatsOutboxRecord, error)
|
||||
MarkActivityTemplateStatsOutboxSent(ctx context.Context, outboxID string, nowMS int64) error
|
||||
MarkActivityTemplateStatsOutboxFailed(ctx context.Context, outboxID string, reason string, nowMS int64) error
|
||||
}
|
||||
|
||||
type RuntimeWallet interface {
|
||||
GrantPinnedResourceGroup(ctx context.Context, req *walletv1.GrantPinnedResourceGroupRequest, opts ...grpc.CallOption) (*walletv1.ResourceGrantResponse, error)
|
||||
}
|
||||
|
||||
type StatsPublisher interface {
|
||||
SendSync(ctx context.Context, message rocketmqx.Message) error
|
||||
}
|
||||
|
||||
type NoticeService interface {
|
||||
CreateActivityNotice(ctx context.Context, cmd messageservice.NoticeCommand) (messageservice.NoticeResult, error)
|
||||
}
|
||||
|
||||
// BindRuntime installs optional runtime dependencies without widening the stable configuration constructor.
|
||||
// Production wiring treats a missing binding as Unavailable; configuration-only tests never call these methods.
|
||||
func (s *Service) BindRuntime(repository RuntimeRepository, wallet RuntimeWallet) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.runtime = repository
|
||||
s.runtimeWallet = wallet
|
||||
}
|
||||
|
||||
func (s *Service) BindNotice(notices NoticeService) {
|
||||
if s != nil {
|
||||
s.notices = notices
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) GetCurrentRuntime(ctx context.Context, regionID int64, templateCode string) (domain.Runtime, bool, error) {
|
||||
if err := s.requireRuntime(); err != nil {
|
||||
return domain.Runtime{}, false, err
|
||||
}
|
||||
if regionID <= 0 {
|
||||
return domain.Runtime{}, false, xerr.New(xerr.InvalidArgument, "region_id is required")
|
||||
}
|
||||
return s.runtime.FindCurrentActivityTemplateRuntime(ctx, strings.TrimSpace(templateCode), regionID, s.now().UTC().UnixMilli())
|
||||
}
|
||||
|
||||
func (s *Service) Visit(ctx context.Context, command domain.VisitCommand) (domain.VisitResult, error) {
|
||||
if err := s.requireRuntime(); err != nil {
|
||||
return domain.VisitResult{}, err
|
||||
}
|
||||
command.TemplateCode = strings.TrimSpace(command.TemplateCode)
|
||||
command.CommandID = strings.TrimSpace(command.CommandID)
|
||||
if command.UserID <= 0 || command.RegionID <= 0 || command.VersionNo <= 0 || command.TemplateCode == "" || command.CommandID == "" || len(command.CommandID) > 128 {
|
||||
return domain.VisitResult{}, xerr.New(xerr.InvalidArgument, "visit command is incomplete")
|
||||
}
|
||||
return s.runtime.VisitActivityTemplate(ctx, command, s.now().UTC().UnixMilli())
|
||||
}
|
||||
|
||||
func (s *Service) ListRuntimeTasks(ctx context.Context, userID, regionID, versionNo int64, templateCode, taskDay string) (domain.Runtime, []domain.TaskProgress, int64, error) {
|
||||
if err := s.requireRuntime(); err != nil {
|
||||
return domain.Runtime{}, nil, 0, err
|
||||
}
|
||||
if userID <= 0 || versionNo < 0 || strings.TrimSpace(templateCode) == "" || (versionNo == 0 && regionID <= 0) {
|
||||
return domain.Runtime{}, nil, 0, xerr.New(xerr.InvalidArgument, "task query is incomplete")
|
||||
}
|
||||
nowMS := s.now().UTC().UnixMilli()
|
||||
var runtime domain.Runtime
|
||||
var found bool
|
||||
var err error
|
||||
if versionNo > 0 {
|
||||
if strings.TrimSpace(taskDay) == "" {
|
||||
return domain.Runtime{}, nil, 0, xerr.New(xerr.InvalidArgument, "task_day is required for historical version")
|
||||
}
|
||||
runtime, found, err = s.runtime.FindActivityTemplateRuntimeForUserTask(ctx, strings.TrimSpace(templateCode), versionNo, userID, taskDay, "")
|
||||
} else {
|
||||
runtime, found, err = s.runtime.FindCurrentActivityTemplateRuntime(ctx, strings.TrimSpace(templateCode), regionID, nowMS)
|
||||
}
|
||||
if err != nil {
|
||||
return domain.Runtime{}, nil, 0, err
|
||||
}
|
||||
if !found {
|
||||
return domain.Runtime{}, nil, 0, xerr.New(xerr.NotFound, "active activity template not found")
|
||||
}
|
||||
if strings.TrimSpace(taskDay) == "" {
|
||||
taskDay = utcDay(nowMS)
|
||||
}
|
||||
dayStart, err := parseUTCDay(taskDay)
|
||||
if err != nil || dayStart >= runtime.Template.EndMS || dayStart+int64(24*time.Hour/time.Millisecond) <= runtime.RuntimeFromMS {
|
||||
return domain.Runtime{}, nil, 0, xerr.New(xerr.InvalidArgument, "task_day is outside the activity runtime")
|
||||
}
|
||||
items, err := s.runtime.ListActivityTemplateTaskProgress(ctx, runtime, userID, taskDay)
|
||||
if err != nil {
|
||||
return domain.Runtime{}, nil, 0, err
|
||||
}
|
||||
nextRefresh := dayStart + int64(24*time.Hour/time.Millisecond)
|
||||
if runtime.Template.EndMS < nextRefresh {
|
||||
nextRefresh = runtime.Template.EndMS
|
||||
}
|
||||
return runtime, items, nextRefresh, nil
|
||||
}
|
||||
|
||||
func (s *Service) ClaimRuntimeTaskReward(ctx context.Context, command domain.ClaimCommand) (domain.TaskClaim, error) {
|
||||
if err := s.requireRuntime(); err != nil {
|
||||
return domain.TaskClaim{}, err
|
||||
}
|
||||
if s.runtimeWallet == nil {
|
||||
return domain.TaskClaim{}, xerr.New(xerr.Unavailable, "wallet client is not configured")
|
||||
}
|
||||
command.TemplateCode = strings.TrimSpace(command.TemplateCode)
|
||||
command.TaskDay = strings.TrimSpace(command.TaskDay)
|
||||
command.TaskKey = strings.TrimSpace(command.TaskKey)
|
||||
command.CommandID = strings.TrimSpace(command.CommandID)
|
||||
if command.UserID <= 0 || command.TemplateCode == "" || command.VersionNo <= 0 || command.TaskKey == "" || command.CommandID == "" || len(command.CommandID) > 128 {
|
||||
return domain.TaskClaim{}, xerr.New(xerr.InvalidArgument, "task claim command is incomplete")
|
||||
}
|
||||
nowMS := s.now().UTC().UnixMilli()
|
||||
if command.TaskDay == "" {
|
||||
command.TaskDay = utcDay(nowMS)
|
||||
}
|
||||
if _, err := parseUTCDay(command.TaskDay); err != nil {
|
||||
return domain.TaskClaim{}, err
|
||||
}
|
||||
// version_no 来自任务列表的不可变版本。即使同一 template_code 在同一个 UTC 日重发,v1 的失败
|
||||
// claim 也不会被 latest-v2 吞掉;物化 period 仍允许活动结束后的幂等 wallet 补偿。
|
||||
runtime, found, err := s.runtime.FindActivityTemplateRuntimeForUserTask(
|
||||
ctx, command.TemplateCode, command.VersionNo, command.UserID, command.TaskDay, command.TaskKey,
|
||||
)
|
||||
if err != nil {
|
||||
return domain.TaskClaim{}, err
|
||||
}
|
||||
if !found {
|
||||
return domain.TaskClaim{}, xerr.New(xerr.NotFound, "activity template task period not found")
|
||||
}
|
||||
claim, err := s.runtime.PrepareActivityTemplateTaskClaim(ctx, runtime, command, nowMS)
|
||||
if err != nil || claim.Status == domain.ClaimStatusGranted {
|
||||
return claim, err
|
||||
}
|
||||
if claim.RewardSnapshot.SnapshotID == "" {
|
||||
err := xerr.New(xerr.Conflict, "activity task reward snapshot is missing")
|
||||
_ = s.runtime.MarkActivityTemplateTaskClaimFailed(ctx, claim.ClaimID, xerr.MessageOf(err), s.now().UTC().UnixMilli())
|
||||
return domain.TaskClaim{}, err
|
||||
}
|
||||
resp, err := s.runtimeWallet.GrantPinnedResourceGroup(ctx, &walletv1.GrantPinnedResourceGroupRequest{
|
||||
CommandId: claim.WalletCommandID, AppCode: appcode.FromContext(ctx), TargetUserId: claim.UserID,
|
||||
SnapshotId: claim.RewardSnapshot.SnapshotID, Reason: activityTemplateTaskRewardReason,
|
||||
OperatorUserId: claim.UserID, GrantSource: activityTemplateTaskGrantSource,
|
||||
ExpectedSnapshotHash: claim.RewardSnapshot.SnapshotHash,
|
||||
})
|
||||
if err != nil {
|
||||
_ = s.runtime.MarkActivityTemplateTaskClaimFailed(ctx, claim.ClaimID, xerr.MessageOf(err), s.now().UTC().UnixMilli())
|
||||
return domain.TaskClaim{}, err
|
||||
}
|
||||
walletGrantID := strings.TrimSpace(resp.GetGrant().GetGrantId())
|
||||
if walletGrantID == "" {
|
||||
err := xerr.New(xerr.Internal, "wallet returned an empty resource grant")
|
||||
_ = s.runtime.MarkActivityTemplateTaskClaimFailed(ctx, claim.ClaimID, xerr.MessageOf(err), s.now().UTC().UnixMilli())
|
||||
return domain.TaskClaim{}, err
|
||||
}
|
||||
return s.runtime.MarkActivityTemplateTaskClaimGranted(ctx, claim.ClaimID, walletGrantID, s.now().UTC().UnixMilli())
|
||||
}
|
||||
|
||||
func (s *Service) ListRuntimeLeaderboard(ctx context.Context, query domain.LeaderboardQuery) (domain.LeaderboardResult, error) {
|
||||
if err := s.requireRuntime(); err != nil {
|
||||
return domain.LeaderboardResult{}, err
|
||||
}
|
||||
query.TemplateCode = strings.TrimSpace(query.TemplateCode)
|
||||
query.BoardType = strings.ToLower(strings.TrimSpace(query.BoardType))
|
||||
query.PeriodKey = strings.TrimSpace(query.PeriodKey)
|
||||
if query.UserID <= 0 || query.VersionNo < 0 || query.TemplateCode == "" || (query.VersionNo == 0 && query.RegionID <= 0) || (query.BoardType != domain.BoardTypeDaily && query.BoardType != domain.BoardTypeTotal) {
|
||||
return domain.LeaderboardResult{}, xerr.New(xerr.InvalidArgument, "leaderboard query is invalid")
|
||||
}
|
||||
query.NowMS = s.now().UTC().UnixMilli()
|
||||
var runtime domain.Runtime
|
||||
var found bool
|
||||
var err error
|
||||
// 显式 period_key 是可分享、可从获奖通知回跳的稳定榜单身份。先按物化 period 解析其不可变发布版本,
|
||||
// 因此活动结束或同一模版中途重发后仍能读取冻结榜;未显式指定时继续只暴露当前运行版本。
|
||||
if query.VersionNo > 0 && query.PeriodKey == "" {
|
||||
if query.BoardType == domain.BoardTypeTotal {
|
||||
query.PeriodKey = "total"
|
||||
} else {
|
||||
query.PeriodKey = utcDay(query.NowMS)
|
||||
}
|
||||
}
|
||||
if query.PeriodKey != "" && query.VersionNo > 0 {
|
||||
runtime, found, err = s.runtime.FindActivityTemplateRuntimeForExactLeaderboard(
|
||||
ctx, query.TemplateCode, query.VersionNo, query.BoardType, query.PeriodKey, query.NowMS,
|
||||
)
|
||||
} else if query.PeriodKey != "" {
|
||||
runtime, found, err = s.runtime.FindActivityTemplateRuntimeForPeriod(
|
||||
ctx, query.TemplateCode, query.VersionNo, query.RegionID, query.BoardType, query.PeriodKey, query.NowMS,
|
||||
)
|
||||
} else {
|
||||
runtime, found, err = s.runtime.FindCurrentActivityTemplateRuntime(ctx, query.TemplateCode, query.RegionID, query.NowMS)
|
||||
}
|
||||
if err != nil {
|
||||
return domain.LeaderboardResult{}, err
|
||||
}
|
||||
if !found {
|
||||
return domain.LeaderboardResult{}, xerr.New(xerr.NotFound, "active activity template not found")
|
||||
}
|
||||
return s.runtime.ListActivityTemplateRuntimeLeaderboard(ctx, runtime, query)
|
||||
}
|
||||
|
||||
func (s *Service) AdminListRuntimeLeaderboard(ctx context.Context, query domain.LeaderboardQuery) (domain.LeaderboardResult, error) {
|
||||
if err := s.requireRuntime(); err != nil {
|
||||
return domain.LeaderboardResult{}, err
|
||||
}
|
||||
query.TemplateID = strings.TrimSpace(query.TemplateID)
|
||||
query.BoardType = strings.ToLower(strings.TrimSpace(query.BoardType))
|
||||
if query.TemplateID == "" || query.VersionNo <= 0 || (query.BoardType != domain.BoardTypeDaily && query.BoardType != domain.BoardTypeTotal) {
|
||||
return domain.LeaderboardResult{}, xerr.New(xerr.InvalidArgument, "admin leaderboard query is invalid")
|
||||
}
|
||||
query.NowMS = s.now().UTC().UnixMilli()
|
||||
runtime, err := s.runtime.GetActivityTemplateRuntimeVersion(ctx, query.TemplateID, query.VersionNo)
|
||||
if err != nil {
|
||||
return domain.LeaderboardResult{}, err
|
||||
}
|
||||
return s.runtime.ListActivityTemplateRuntimeLeaderboard(ctx, runtime, query)
|
||||
}
|
||||
|
||||
func (s *Service) ListRewardJobs(ctx context.Context, query domain.RewardJobQuery) ([]domain.RewardJob, int64, error) {
|
||||
if err := s.requireRuntime(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
query.TemplateID = strings.TrimSpace(query.TemplateID)
|
||||
query.BoardType = strings.ToLower(strings.TrimSpace(query.BoardType))
|
||||
query.PeriodKey = strings.TrimSpace(query.PeriodKey)
|
||||
query.Status = strings.ToLower(strings.TrimSpace(query.Status))
|
||||
if query.TemplateID == "" {
|
||||
return nil, 0, xerr.New(xerr.InvalidArgument, "template_id is required")
|
||||
}
|
||||
if query.BoardType != "" && query.BoardType != domain.BoardTypeDaily && query.BoardType != domain.BoardTypeTotal {
|
||||
return nil, 0, xerr.New(xerr.InvalidArgument, "board_type is invalid")
|
||||
}
|
||||
if query.Status != "" && query.Status != domain.ClaimStatusPending && query.Status != domain.ClaimStatusRunning && query.Status != domain.ClaimStatusGranted && query.Status != domain.ClaimStatusFailed && query.Status != domain.ClaimStatusDead {
|
||||
return nil, 0, xerr.New(xerr.InvalidArgument, "reward status is invalid")
|
||||
}
|
||||
return s.runtime.ListActivityTemplateRewardJobs(ctx, query)
|
||||
}
|
||||
|
||||
func (s *Service) ListTaskClaims(ctx context.Context, query domain.TaskClaimQuery) ([]domain.TaskClaim, int64, error) {
|
||||
if err := s.requireRuntime(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
query.TemplateID = strings.TrimSpace(query.TemplateID)
|
||||
query.TaskDay = strings.TrimSpace(query.TaskDay)
|
||||
query.TaskKey = strings.TrimSpace(query.TaskKey)
|
||||
query.Status = strings.ToLower(strings.TrimSpace(query.Status))
|
||||
if query.TemplateID == "" || query.VersionNo < 0 || query.UserID < 0 {
|
||||
return nil, 0, xerr.New(xerr.InvalidArgument, "task claim query is invalid")
|
||||
}
|
||||
if query.TaskDay != "" {
|
||||
if _, err := parseUTCDay(query.TaskDay); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
}
|
||||
if query.Status != "" && query.Status != domain.ClaimStatusPending && query.Status != domain.ClaimStatusRunning && query.Status != domain.ClaimStatusGranted && query.Status != domain.ClaimStatusFailed {
|
||||
return nil, 0, xerr.New(xerr.InvalidArgument, "task claim status is invalid")
|
||||
}
|
||||
return s.runtime.ListActivityTemplateTaskClaims(ctx, query)
|
||||
}
|
||||
|
||||
func (s *Service) RetryTaskClaim(ctx context.Context, templateID, claimID string, operatorAdminID int64) (domain.TaskClaim, error) {
|
||||
if err := s.requireRuntime(); err != nil {
|
||||
return domain.TaskClaim{}, err
|
||||
}
|
||||
if s.runtimeWallet == nil {
|
||||
return domain.TaskClaim{}, xerr.New(xerr.Unavailable, "wallet client is not configured")
|
||||
}
|
||||
if strings.TrimSpace(templateID) == "" || strings.TrimSpace(claimID) == "" || operatorAdminID <= 0 {
|
||||
return domain.TaskClaim{}, xerr.New(xerr.InvalidArgument, "task claim retry command is incomplete")
|
||||
}
|
||||
claim, err := s.runtime.ClaimActivityTemplateTaskClaimForRetry(ctx, strings.TrimSpace(templateID), strings.TrimSpace(claimID), s.now().UTC().UnixMilli())
|
||||
if err != nil {
|
||||
return domain.TaskClaim{}, err
|
||||
}
|
||||
if claim.RewardSnapshot.SnapshotID == "" || !validSHA256Hex(claim.RewardSnapshot.SnapshotHash) {
|
||||
err := xerr.New(xerr.Conflict, "activity task reward snapshot is missing")
|
||||
_ = s.runtime.MarkActivityTemplateTaskClaimFailed(ctx, claim.ClaimID, xerr.MessageOf(err), s.now().UTC().UnixMilli())
|
||||
return domain.TaskClaim{}, err
|
||||
}
|
||||
resp, err := s.runtimeWallet.GrantPinnedResourceGroup(ctx, &walletv1.GrantPinnedResourceGroupRequest{
|
||||
CommandId: claim.WalletCommandID, AppCode: appcode.FromContext(ctx), TargetUserId: claim.UserID,
|
||||
SnapshotId: claim.RewardSnapshot.SnapshotID, ExpectedSnapshotHash: claim.RewardSnapshot.SnapshotHash,
|
||||
Reason: activityTemplateTaskRewardReason, OperatorUserId: claim.UserID, GrantSource: activityTemplateTaskGrantSource,
|
||||
})
|
||||
if err != nil {
|
||||
_ = s.runtime.MarkActivityTemplateTaskClaimFailed(ctx, claim.ClaimID, xerr.MessageOf(err), s.now().UTC().UnixMilli())
|
||||
return domain.TaskClaim{}, err
|
||||
}
|
||||
walletGrantID := strings.TrimSpace(resp.GetGrant().GetGrantId())
|
||||
if walletGrantID == "" {
|
||||
err := xerr.New(xerr.Internal, "wallet returned an empty resource grant")
|
||||
_ = s.runtime.MarkActivityTemplateTaskClaimFailed(ctx, claim.ClaimID, xerr.MessageOf(err), s.now().UTC().UnixMilli())
|
||||
return domain.TaskClaim{}, err
|
||||
}
|
||||
return s.runtime.MarkActivityTemplateTaskClaimGranted(ctx, claim.ClaimID, walletGrantID, s.now().UTC().UnixMilli())
|
||||
}
|
||||
|
||||
// ProcessRuntimeSettlementBatch separates bounded period materialization from bounded job delivery. A large
|
||||
// leaderboard can therefore never make one cron RPC perform unbounded wallet calls; every delivery is fenced by
|
||||
// a per-invocation lease owner and remains replay-safe through the immutable wallet command.
|
||||
func (s *Service) ProcessRuntimeSettlementBatch(ctx context.Context, runID, workerID string, batchSize int32, lockTTL time.Duration) (claimed, processed, success, failure int32, hasMore bool, err error) {
|
||||
if err := s.requireRuntime(); err != nil {
|
||||
return 0, 0, 0, 0, false, err
|
||||
}
|
||||
if s.runtimeWallet == nil {
|
||||
return 0, 0, 0, 0, false, xerr.New(xerr.Unavailable, "wallet client is not configured")
|
||||
}
|
||||
if batchSize <= 0 {
|
||||
batchSize = 20
|
||||
}
|
||||
nowMS := s.now().UTC().UnixMilli()
|
||||
workerID = strings.TrimSpace(workerID)
|
||||
if workerID == "" {
|
||||
workerID = "activity-template-settlement"
|
||||
}
|
||||
runID = strings.TrimSpace(runID)
|
||||
if runID == "" {
|
||||
runID = idgen.New("run")
|
||||
}
|
||||
leaseOwner := activityTemplateRewardLeaseOwner(workerID, runID)
|
||||
periods, err := s.runtime.ClaimDueActivityTemplateRankPeriods(ctx, leaseOwner, nowMS, lockTTL, batchSize)
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, false, err
|
||||
}
|
||||
for _, period := range periods {
|
||||
_, prepareErr := s.runtime.PrepareActivityTemplateRankSettlement(ctx, period, s.now().UTC().UnixMilli())
|
||||
if prepareErr != nil {
|
||||
logx.Error(ctx, "activity_template_prepare_settlement_failed", prepareErr, slog.String("template_id", period.TemplateID), slog.String("period_key", period.PeriodKey), slog.String("run_id", runID))
|
||||
failure++
|
||||
continue
|
||||
}
|
||||
// Empty/no-reward boards settle here; boards with jobs are released from the period lock and
|
||||
// finish after the independent job batch reaches all-granted.
|
||||
if _, finishErr := s.runtime.FinishActivityTemplateRankPeriodIfComplete(ctx, period, s.now().UTC().UnixMilli()); finishErr != nil {
|
||||
failure++
|
||||
}
|
||||
}
|
||||
|
||||
jobs, err := s.runtime.ClaimActivityTemplateRewardJobs(ctx, leaseOwner, s.now().UTC().UnixMilli(), lockTTL, batchSize)
|
||||
if err != nil {
|
||||
return 0, 0, 0, failure, len(periods) == int(batchSize), err
|
||||
}
|
||||
claimed = int32(len(jobs))
|
||||
processed = int32(len(jobs))
|
||||
var succeeded atomic.Int32
|
||||
var failed atomic.Int32
|
||||
workerCount := min(len(jobs), 8)
|
||||
if workerCount > 0 {
|
||||
queue := make(chan domain.RewardJob)
|
||||
var workers sync.WaitGroup
|
||||
workers.Add(workerCount)
|
||||
for range workerCount {
|
||||
go func() {
|
||||
defer workers.Done()
|
||||
for job := range queue {
|
||||
if _, grantErr := s.grantRuntimeRewardJob(ctx, job, leaseOwner, lockTTL); grantErr != nil {
|
||||
failed.Add(1)
|
||||
continue
|
||||
}
|
||||
succeeded.Add(1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
for _, job := range jobs {
|
||||
queue <- job
|
||||
}
|
||||
close(queue)
|
||||
workers.Wait()
|
||||
}
|
||||
success = succeeded.Load()
|
||||
failure += failed.Load()
|
||||
|
||||
// A single job batch can span periods. Finalize each identity once after every wallet worker has stopped,
|
||||
// so a period is never marked settled while an in-process job still owns a lease.
|
||||
periodsToFinish := make(map[string]domain.RankPeriod)
|
||||
for _, job := range jobs {
|
||||
key := job.TemplateID + "\x00" + strconv.FormatInt(job.VersionNo, 10) + "\x00" + job.BoardType + "\x00" + job.PeriodKey
|
||||
periodsToFinish[key] = domain.RankPeriod{
|
||||
AppCode: job.AppCode, TemplateID: job.TemplateID, TemplateCode: job.TemplateCode, VersionNo: job.VersionNo,
|
||||
BoardType: job.BoardType, PeriodKey: job.PeriodKey,
|
||||
}
|
||||
}
|
||||
for _, period := range periodsToFinish {
|
||||
if _, finishErr := s.runtime.FinishActivityTemplateRankPeriodIfComplete(ctx, period, s.now().UTC().UnixMilli()); finishErr != nil {
|
||||
failure++
|
||||
}
|
||||
}
|
||||
|
||||
// Recover the narrow crash window after the last granted-job commit but before its period transition. The
|
||||
// repository only returns `rewarding` periods with zero non-granted jobs, so failed/dead/backoff queues cannot
|
||||
// consume this bound or keep cron in a has_more hot loop.
|
||||
finalizable, finalizeClaimErr := s.runtime.ClaimFinalizableActivityTemplateRankPeriods(
|
||||
ctx, leaseOwner, s.now().UTC().UnixMilli(), lockTTL, batchSize,
|
||||
)
|
||||
if finalizeClaimErr != nil {
|
||||
return claimed, processed, success, failure, len(periods) == int(batchSize) || len(jobs) == int(batchSize), finalizeClaimErr
|
||||
}
|
||||
for _, period := range finalizable {
|
||||
if _, finishErr := s.runtime.FinishActivityTemplateRankPeriodIfComplete(ctx, period, s.now().UTC().UnixMilli()); finishErr != nil {
|
||||
failure++
|
||||
}
|
||||
}
|
||||
hasMore = len(periods) == int(batchSize) || len(jobs) == int(batchSize) || len(finalizable) == int(batchSize)
|
||||
return claimed, processed, success, failure, hasMore, nil
|
||||
}
|
||||
|
||||
func (s *Service) RetryRewardJob(ctx context.Context, templateID, rewardJobID string, operatorAdminID int64) (domain.RewardJob, error) {
|
||||
if err := s.requireRuntime(); err != nil {
|
||||
return domain.RewardJob{}, err
|
||||
}
|
||||
if s.runtimeWallet == nil {
|
||||
return domain.RewardJob{}, xerr.New(xerr.Unavailable, "wallet client is not configured")
|
||||
}
|
||||
if strings.TrimSpace(templateID) == "" || strings.TrimSpace(rewardJobID) == "" || operatorAdminID <= 0 {
|
||||
return domain.RewardJob{}, xerr.New(xerr.InvalidArgument, "reward retry command is incomplete")
|
||||
}
|
||||
nowMS := s.now().UTC().UnixMilli()
|
||||
leaseOwner := activityTemplateRewardLeaseOwner("admin-retry-"+strconv.FormatInt(operatorAdminID, 10), idgen.New("request")+":"+rewardJobID)
|
||||
const manualRetryLeaseTTL = time.Minute
|
||||
job, err := s.runtime.ClaimActivityTemplateRewardJobForManualRetry(
|
||||
ctx, strings.TrimSpace(templateID), strings.TrimSpace(rewardJobID), leaseOwner, nowMS, manualRetryLeaseTTL,
|
||||
)
|
||||
if err != nil || job.Status == domain.ClaimStatusGranted {
|
||||
return job, err
|
||||
}
|
||||
walletGrantID, err := s.grantRuntimeRewardJob(ctx, job, leaseOwner, manualRetryLeaseTTL)
|
||||
if err != nil {
|
||||
return domain.RewardJob{}, err
|
||||
}
|
||||
job.Status = domain.ClaimStatusGranted
|
||||
job.WalletGrantID = walletGrantID
|
||||
job.FailureReason = ""
|
||||
job.GrantedAtMS = s.now().UTC().UnixMilli()
|
||||
job.UpdatedAtMS = job.GrantedAtMS
|
||||
return job, nil
|
||||
}
|
||||
|
||||
func (s *Service) grantRuntimeRewardJob(ctx context.Context, job domain.RewardJob, leaseOwner string, lockTTL time.Duration) (string, error) {
|
||||
markFailed := func(cause error) error {
|
||||
if markErr := s.runtime.MarkActivityTemplateRewardJobFailedWithLease(ctx, job.RewardJobID, leaseOwner, xerr.MessageOf(cause), s.now().UTC().UnixMilli()); markErr != nil {
|
||||
logx.Error(ctx, "activity_template_reward_job_failure_fence_lost", markErr,
|
||||
slog.String("reward_job_id", job.RewardJobID), slog.String("lease_owner", leaseOwner))
|
||||
}
|
||||
return cause
|
||||
}
|
||||
if job.RewardSnapshot.SnapshotID == "" || !validSHA256Hex(job.RewardSnapshot.SnapshotHash) {
|
||||
err := xerr.New(xerr.Conflict, "activity rank reward snapshot is missing")
|
||||
return "", markFailed(err)
|
||||
}
|
||||
if err := s.runtime.ExtendActivityTemplateRewardJobLease(ctx, job.RewardJobID, leaseOwner, s.now().UTC().UnixMilli(), lockTTL); err != nil {
|
||||
return "", err
|
||||
}
|
||||
resp, err := s.runtimeWallet.GrantPinnedResourceGroup(ctx, &walletv1.GrantPinnedResourceGroupRequest{
|
||||
CommandId: job.WalletCommandID, AppCode: appcode.FromContext(ctx), TargetUserId: job.UserID,
|
||||
SnapshotId: job.RewardSnapshot.SnapshotID, Reason: activityTemplateRankRewardReason,
|
||||
OperatorUserId: job.UserID, GrantSource: activityTemplateRankGrantSource,
|
||||
ExpectedSnapshotHash: job.RewardSnapshot.SnapshotHash,
|
||||
})
|
||||
if err != nil {
|
||||
return "", markFailed(err)
|
||||
}
|
||||
walletGrantID := strings.TrimSpace(resp.GetGrant().GetGrantId())
|
||||
if walletGrantID == "" {
|
||||
err := xerr.New(xerr.Internal, "wallet returned an empty resource grant")
|
||||
return "", markFailed(err)
|
||||
}
|
||||
// The wallet call can outlive the original lease window. Re-fence before emitting the user notice; if
|
||||
// another worker took over, it will replay the same wallet command and stable notice event instead.
|
||||
if err := s.runtime.ExtendActivityTemplateRewardJobLease(ctx, job.RewardJobID, leaseOwner, s.now().UTC().UnixMilli(), lockTTL); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if s.notices != nil {
|
||||
action := url.Values{}
|
||||
action.Set("template_code", job.TemplateCode)
|
||||
action.Set("version_no", strconv.FormatInt(job.VersionNo, 10))
|
||||
action.Set("board_type", job.BoardType)
|
||||
action.Set("period_key", job.PeriodKey)
|
||||
_, noticeErr := s.notices.CreateActivityNotice(ctx, messageservice.NoticeCommand{
|
||||
TargetUserID: job.UserID, Producer: "activity-template-runtime",
|
||||
ProducerEventID: job.RewardJobID + ":granted", ProducerEventType: "ActivityTemplateRankRewardGranted",
|
||||
AggregateType: "activity_template_reward_job", AggregateID: job.RewardJobID,
|
||||
TemplateID: job.TemplateID, TemplateVersion: strconv.FormatInt(job.VersionNo, 10),
|
||||
// Inbox storage currently requires title/summary text. Persist stable content keys rather than
|
||||
// English prose; App clients localize the keys with the immutable metadata parameters below.
|
||||
Title: "activity_template.rank_reward.title", Summary: "activity_template.rank_reward.summary",
|
||||
ActionType: "activity_detail",
|
||||
ActionParam: action.Encode(), SentAtMS: s.now().UTC().UnixMilli(),
|
||||
Metadata: map[string]any{
|
||||
"content_key": "activity_template.rank_reward_granted", "template_code": job.TemplateCode,
|
||||
"version_no": job.VersionNo, "board_type": job.BoardType, "period_key": job.PeriodKey,
|
||||
"rank_no": job.RankNo, "reward_name": job.RewardSnapshot.Name,
|
||||
"resource_group_id": job.ResourceGroupID,
|
||||
},
|
||||
})
|
||||
if noticeErr != nil {
|
||||
// Wallet command is stable. Marking failed makes the next cron/admin retry replay the same wallet
|
||||
// grant (idempotent) and retry the inbox write instead of silently losing the winner notification.
|
||||
return "", markFailed(noticeErr)
|
||||
}
|
||||
}
|
||||
if err := s.runtime.MarkActivityTemplateRewardJobGrantedWithLease(ctx, job.RewardJobID, leaseOwner, walletGrantID, s.now().UTC().UnixMilli()); err != nil {
|
||||
// 钱包与站内信均已幂等成功但本地状态落库失败时,立即释放结算周期并标为 failed;下一轮会用同一
|
||||
// wallet_command_id/producer_event_id 补偿,不会重复发奖或重复通知。
|
||||
_ = s.runtime.MarkActivityTemplateRewardJobFailedWithLease(ctx, job.RewardJobID, leaseOwner, xerr.MessageOf(err), s.now().UTC().UnixMilli())
|
||||
return "", err
|
||||
}
|
||||
return walletGrantID, nil
|
||||
}
|
||||
|
||||
func activityTemplateRewardLeaseOwner(workerID, invocationID string) string {
|
||||
workerID = strings.TrimSpace(workerID)
|
||||
invocationID = strings.TrimSpace(invocationID)
|
||||
owner := workerID + ":" + invocationID
|
||||
if len(owner) <= 128 {
|
||||
return owner
|
||||
}
|
||||
// Preserve an operator-readable prefix while bounding the persisted fencing token.
|
||||
digest := sha256.Sum256([]byte(owner))
|
||||
return "activity-template-reward-lease:" + hex.EncodeToString(digest[:])
|
||||
}
|
||||
|
||||
// HandleRoomEvent accepts only wallet-committed real gift facts. coin_spent is the scoring source of truth;
|
||||
// configured gift price is a display snapshot and is never multiplied in this path.
|
||||
func (s *Service) HandleRoomEvent(ctx context.Context, envelope *roomeventsv1.EventEnvelope) (int32, error) {
|
||||
if s == nil || s.runtime == nil || envelope == nil || envelope.GetEventType() != "RoomGiftSent" {
|
||||
return 0, nil
|
||||
}
|
||||
var gift roomeventsv1.RoomGiftSent
|
||||
if err := proto.Unmarshal(envelope.GetBody(), &gift); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if gift.GetIsRobotGift() || gift.GetSyntheticLuckyGift() || gift.GetRealRoomRobotGift() {
|
||||
return 0, nil
|
||||
}
|
||||
if strings.TrimSpace(envelope.GetEventId()) == "" || gift.GetSenderUserId() <= 0 || strings.TrimSpace(gift.GetGiftId()) == "" || gift.GetGiftCount() <= 0 || gift.GetCoinSpent() <= 0 || gift.GetRegionId() <= 0 || envelope.GetOccurredAtMs() <= 0 {
|
||||
return 0, nil
|
||||
}
|
||||
result, err := s.runtime.ConsumeActivityTemplateGiftEvent(appcode.WithContext(ctx, envelope.GetAppCode()), domain.GiftEvent{
|
||||
EventID: strings.TrimSpace(envelope.GetEventId()), UserID: gift.GetSenderUserId(), GiftID: strings.TrimSpace(gift.GetGiftId()),
|
||||
// region_id is the sender's profile region captured by room-service at wallet commit. visible_region_id
|
||||
// describes the room audience and must never choose a user-targeted activity runtime.
|
||||
GiftCount: int64(gift.GetGiftCount()), CoinAmount: gift.GetCoinSpent(), RegionID: gift.GetRegionId(),
|
||||
OccurredAtMS: envelope.GetOccurredAtMs(),
|
||||
}, s.now().UTC().UnixMilli())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// Normal misses (no active runtime / gift not configured) are expected routing outcomes. Closed rank
|
||||
// watermarks are operationally different: a committed gift arrived too late for at least one projection,
|
||||
// so surface both partial acceptance and full rejection with delay metadata for alerting.
|
||||
if strings.Contains(result.SkipReason, "settlement_window_closed") || strings.Contains(result.SkipReason, "rank_period_closed") {
|
||||
logx.Warn(ctx, "activity_template_gift_rank_projection_late",
|
||||
slog.String("event_id", result.EventID), slog.String("template_id", result.TemplateID),
|
||||
slog.Int64("version_no", result.VersionNo), slog.String("task_day", result.TaskDay),
|
||||
slog.String("projection_outcome", result.SkipReason), slog.Int64("processing_delay_ms", max(int64(0), s.now().UTC().UnixMilli()-envelope.GetOccurredAtMs())))
|
||||
}
|
||||
if result.Status == domain.EventStatusConsumed {
|
||||
return 1, nil
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// ProcessStatsOutboxBatch publishes owner facts to the statistics topic. Sent is marked only after broker ack;
|
||||
// a crash after ack but before MarkSent is harmless because statistics_event_consumption deduplicates event_id.
|
||||
func (s *Service) ProcessStatsOutboxBatch(ctx context.Context, publisher StatsPublisher, topic, workerID string, batchSize int, lockTTL time.Duration) (int, int, error) {
|
||||
if err := s.requireRuntime(); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if publisher == nil || strings.TrimSpace(topic) == "" {
|
||||
return 0, 0, xerr.New(xerr.Unavailable, "activity template stats publisher is not configured")
|
||||
}
|
||||
items, err := s.runtime.ClaimActivityTemplateStatsOutbox(ctx, workerID, s.now().UTC().UnixMilli(), lockTTL, batchSize)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
success := 0
|
||||
for _, item := range items {
|
||||
tag, tagErr := activitymq.EventTypeTag(item.EventType)
|
||||
if tagErr == nil {
|
||||
tagErr = publisher.SendSync(ctx, rocketmqx.Message{Topic: topic, Tag: tag, Keys: []string{item.OutboxID}, Body: item.Payload})
|
||||
}
|
||||
if tagErr != nil {
|
||||
_ = s.runtime.MarkActivityTemplateStatsOutboxFailed(ctx, item.OutboxID, tagErr.Error(), s.now().UTC().UnixMilli())
|
||||
continue
|
||||
}
|
||||
if err := s.runtime.MarkActivityTemplateStatsOutboxSent(ctx, item.OutboxID, s.now().UTC().UnixMilli()); err != nil {
|
||||
return len(items), success, err
|
||||
}
|
||||
success++
|
||||
}
|
||||
return len(items), success, nil
|
||||
}
|
||||
|
||||
func (s *Service) requireRuntime() error {
|
||||
if s == nil || s.runtime == nil {
|
||||
return xerr.New(xerr.Unavailable, "activity template runtime repository is not configured")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func utcDay(ms int64) string { return time.UnixMilli(ms).UTC().Format("2006-01-02") }
|
||||
|
||||
func parseUTCDay(value string) (int64, error) {
|
||||
t, err := time.Parse("2006-01-02", strings.TrimSpace(value))
|
||||
if err != nil || t.Format("2006-01-02") != strings.TrimSpace(value) {
|
||||
return 0, xerr.New(xerr.InvalidArgument, "task_day must be UTC YYYY-MM-DD")
|
||||
}
|
||||
return t.UnixMilli(), nil
|
||||
}
|
||||
@ -0,0 +1,451 @@
|
||||
package activitytemplate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/proto"
|
||||
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/xerr"
|
||||
domain "hyapp/services/activity-service/internal/domain/activitytemplate"
|
||||
messageservice "hyapp/services/activity-service/internal/service/message"
|
||||
)
|
||||
|
||||
type leaderboardRuntimeRepository struct {
|
||||
RuntimeRepository
|
||||
currentRuntime domain.Runtime
|
||||
currentFound bool
|
||||
periodRuntime domain.Runtime
|
||||
periodFound bool
|
||||
currentCalls int
|
||||
periodCalls int
|
||||
periodVersionNo int64
|
||||
leaderboardCall int
|
||||
giftEvents []domain.GiftEvent
|
||||
}
|
||||
|
||||
func (f *leaderboardRuntimeRepository) FindCurrentActivityTemplateRuntime(context.Context, string, int64, int64) (domain.Runtime, bool, error) {
|
||||
f.currentCalls++
|
||||
return f.currentRuntime, f.currentFound, nil
|
||||
}
|
||||
|
||||
func (f *leaderboardRuntimeRepository) ConsumeActivityTemplateGiftEvent(_ context.Context, event domain.GiftEvent, _ int64) (domain.EventResult, error) {
|
||||
f.giftEvents = append(f.giftEvents, event)
|
||||
return domain.EventResult{EventID: event.EventID, Status: domain.EventStatusConsumed}, nil
|
||||
}
|
||||
|
||||
func (f *leaderboardRuntimeRepository) FindActivityTemplateRuntimeForPeriod(_ context.Context, _ string, versionNo int64, _ int64, _ string, _ string, _ int64) (domain.Runtime, bool, error) {
|
||||
f.periodCalls++
|
||||
f.periodVersionNo = versionNo
|
||||
return f.periodRuntime, f.periodFound, nil
|
||||
}
|
||||
|
||||
func (f *leaderboardRuntimeRepository) FindActivityTemplateRuntimeForExactLeaderboard(_ context.Context, _ string, versionNo int64, _ string, _ string, _ int64) (domain.Runtime, bool, error) {
|
||||
f.periodCalls++
|
||||
f.periodVersionNo = versionNo
|
||||
return f.periodRuntime, f.periodFound, nil
|
||||
}
|
||||
|
||||
func (f *leaderboardRuntimeRepository) ListActivityTemplateRuntimeLeaderboard(_ context.Context, runtime domain.Runtime, query domain.LeaderboardQuery) (domain.LeaderboardResult, error) {
|
||||
f.leaderboardCall++
|
||||
return domain.LeaderboardResult{
|
||||
Runtime: runtime,
|
||||
Period: domain.RankPeriod{
|
||||
TemplateID: runtime.Template.TemplateID, TemplateCode: runtime.Template.TemplateCode,
|
||||
VersionNo: runtime.VersionNo, BoardType: query.BoardType, PeriodKey: query.PeriodKey,
|
||||
},
|
||||
Entries: []domain.LeaderboardEntry{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestExactLeaderboardAllowsNonParticipantCrossRegionViewerAfterActivityEnded(t *testing.T) {
|
||||
now := time.Date(2026, 7, 20, 0, 0, 0, 0, time.UTC)
|
||||
ended := domain.Runtime{
|
||||
Template: domain.Template{TemplateID: "acttpl_1", TemplateCode: "summer_gifts", EndMS: now.Add(-time.Hour).UnixMilli()},
|
||||
VersionNo: 4, RuntimeFromMS: now.Add(-7 * 24 * time.Hour).UnixMilli(), RuntimeToMS: now.Add(-time.Hour).UnixMilli(),
|
||||
}
|
||||
repository := &leaderboardRuntimeRepository{periodRuntime: ended, periodFound: true}
|
||||
svc := New(&fakeRepository{}, nil, nil)
|
||||
svc.BindRuntime(repository, nil)
|
||||
svc.now = func() time.Time { return now }
|
||||
|
||||
got, err := svc.ListRuntimeLeaderboard(context.Background(), domain.LeaderboardQuery{
|
||||
// RegionID is deliberately zero: the gateway does not resolve the viewer's mutable current region
|
||||
// for exact historical links, and repository resolution does not require participation.
|
||||
TemplateCode: "summer_gifts", VersionNo: 4, UserID: 70001, RegionID: 0, BoardType: domain.BoardTypeTotal,
|
||||
PeriodKey: "total", Page: 1, PageSize: 20,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ListRuntimeLeaderboard() error = %v", err)
|
||||
}
|
||||
if got.Runtime.VersionNo != 4 || repository.periodVersionNo != 4 || repository.periodCalls != 1 || repository.currentCalls != 0 || repository.leaderboardCall != 1 {
|
||||
t.Fatalf("historical leaderboard resolution = %+v; repository=%+v", got.Runtime, repository)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRuntimeLeaderboardWithoutPeriodRemainsCurrentOnly(t *testing.T) {
|
||||
repository := &leaderboardRuntimeRepository{}
|
||||
svc := New(&fakeRepository{}, nil, nil)
|
||||
svc.BindRuntime(repository, nil)
|
||||
|
||||
_, err := svc.ListRuntimeLeaderboard(context.Background(), domain.LeaderboardQuery{
|
||||
TemplateCode: "summer_gifts", UserID: 70001, RegionID: 8, BoardType: domain.BoardTypeTotal,
|
||||
})
|
||||
if !xerr.IsCode(err, xerr.NotFound) {
|
||||
t.Fatalf("ListRuntimeLeaderboard() error = %v, want not found", err)
|
||||
}
|
||||
if repository.currentCalls != 1 || repository.periodCalls != 0 {
|
||||
t.Fatalf("unexpected lookup calls: current=%d period=%d", repository.currentCalls, repository.periodCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleRoomEventExcludesEveryRobotGiftClassAndUsesCoinSpent(t *testing.T) {
|
||||
repository := &leaderboardRuntimeRepository{}
|
||||
svc := New(&fakeRepository{}, nil, nil)
|
||||
svc.BindRuntime(repository, nil)
|
||||
now := time.Date(2026, 7, 14, 9, 0, 0, 0, time.UTC)
|
||||
svc.now = func() time.Time { return now }
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
edit func(*roomeventsv1.RoomGiftSent)
|
||||
want int32
|
||||
}{
|
||||
{name: "is robot gift", edit: func(item *roomeventsv1.RoomGiftSent) { item.IsRobotGift = true }},
|
||||
{name: "synthetic lucky gift", edit: func(item *roomeventsv1.RoomGiftSent) { item.SyntheticLuckyGift = true }},
|
||||
{name: "real room robot gift", edit: func(item *roomeventsv1.RoomGiftSent) { item.RealRoomRobotGift = true }},
|
||||
{name: "real user gift", edit: func(*roomeventsv1.RoomGiftSent) {}, want: 1},
|
||||
}
|
||||
for index, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
before := len(repository.giftEvents)
|
||||
gift := &roomeventsv1.RoomGiftSent{
|
||||
SenderUserId: 90001, GiftId: "rose", GiftCount: 3, GiftValue: 999_999,
|
||||
CoinSpent: 750, RegionId: 12, VisibleRegionId: 99,
|
||||
}
|
||||
tt.edit(gift)
|
||||
body, err := proto.Marshal(gift)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal gift: %v", err)
|
||||
}
|
||||
consumed, err := svc.HandleRoomEvent(context.Background(), &roomeventsv1.EventEnvelope{
|
||||
EventId: "evt-" + tt.name, EventType: "RoomGiftSent", AppCode: "hyapp",
|
||||
OccurredAtMs: now.Add(time.Duration(index) * time.Millisecond).UnixMilli(), Body: body,
|
||||
})
|
||||
if err != nil || consumed != tt.want {
|
||||
t.Fatalf("HandleRoomEvent() = (%d, %v), want (%d, nil)", consumed, err, tt.want)
|
||||
}
|
||||
if tt.want == 0 && len(repository.giftEvents) != before {
|
||||
t.Fatalf("robot gift reached score repository: %+v", repository.giftEvents[before:])
|
||||
}
|
||||
})
|
||||
}
|
||||
if len(repository.giftEvents) != 1 || repository.giftEvents[0].CoinAmount != 750 || repository.giftEvents[0].GiftCount != 3 || repository.giftEvents[0].RegionID != 12 {
|
||||
t.Fatalf("real gift score fact = %+v; coin_spent must be source of truth", repository.giftEvents)
|
||||
}
|
||||
}
|
||||
|
||||
type retryRewardRepository struct {
|
||||
RuntimeRepository
|
||||
job domain.RewardJob
|
||||
taskRuntime domain.Runtime
|
||||
taskClaim domain.TaskClaim
|
||||
claimCalls int
|
||||
taskClaimCalls int
|
||||
taskVersionNo int64
|
||||
markedGranted []string
|
||||
markedFailed []string
|
||||
taskGranted []string
|
||||
taskFailed []string
|
||||
walletGrantIDs []string
|
||||
leaseOwners []string
|
||||
}
|
||||
|
||||
func (f *retryRewardRepository) FindCurrentActivityTemplateRuntime(context.Context, string, int64, int64) (domain.Runtime, bool, error) {
|
||||
return domain.Runtime{}, false, nil
|
||||
}
|
||||
|
||||
func (f *retryRewardRepository) FindActivityTemplateRuntimeForUserTask(_ context.Context, _ string, versionNo int64, _ int64, _ string, _ string) (domain.Runtime, bool, error) {
|
||||
f.taskVersionNo = versionNo
|
||||
return f.taskRuntime, f.taskRuntime.VersionNo > 0, nil
|
||||
}
|
||||
|
||||
func (f *retryRewardRepository) PrepareActivityTemplateTaskClaim(context.Context, domain.Runtime, domain.ClaimCommand, int64) (domain.TaskClaim, error) {
|
||||
f.taskClaimCalls++
|
||||
claim := f.taskClaim
|
||||
claim.Status = domain.ClaimStatusRunning
|
||||
claim.AttemptCount = int32(f.taskClaimCalls)
|
||||
return claim, nil
|
||||
}
|
||||
|
||||
func (f *retryRewardRepository) MarkActivityTemplateTaskClaimGranted(_ context.Context, claimID, walletGrantID string, nowMS int64) (domain.TaskClaim, error) {
|
||||
f.taskGranted = append(f.taskGranted, claimID)
|
||||
claim := f.taskClaim
|
||||
claim.Status = domain.ClaimStatusGranted
|
||||
claim.WalletGrantID = walletGrantID
|
||||
claim.GrantedAtMS = nowMS
|
||||
return claim, nil
|
||||
}
|
||||
|
||||
func (f *retryRewardRepository) MarkActivityTemplateTaskClaimFailed(_ context.Context, claimID, _ string, _ int64) error {
|
||||
f.taskFailed = append(f.taskFailed, claimID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *retryRewardRepository) ClaimActivityTemplateRewardJobForManualRetry(_ context.Context, _, _ string, workerID string, _ int64, _ time.Duration) (domain.RewardJob, error) {
|
||||
f.claimCalls++
|
||||
f.leaseOwners = append(f.leaseOwners, workerID)
|
||||
job := f.job
|
||||
job.Status = domain.ClaimStatusRunning
|
||||
job.AttemptCount = int32(f.claimCalls)
|
||||
return job, nil
|
||||
}
|
||||
|
||||
func (f *retryRewardRepository) ExtendActivityTemplateRewardJobLease(_ context.Context, _, workerID string, _ int64, _ time.Duration) error {
|
||||
f.leaseOwners = append(f.leaseOwners, workerID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *retryRewardRepository) MarkActivityTemplateRewardJobGrantedWithLease(_ context.Context, jobID, workerID, walletGrantID string, _ int64) error {
|
||||
f.markedGranted = append(f.markedGranted, jobID)
|
||||
f.leaseOwners = append(f.leaseOwners, workerID)
|
||||
f.walletGrantIDs = append(f.walletGrantIDs, walletGrantID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *retryRewardRepository) MarkActivityTemplateRewardJobFailedWithLease(_ context.Context, jobID, workerID, _ string, _ int64) error {
|
||||
f.markedFailed = append(f.markedFailed, jobID)
|
||||
f.leaseOwners = append(f.leaseOwners, workerID)
|
||||
return nil
|
||||
}
|
||||
|
||||
type sequenceRuntimeWallet struct {
|
||||
requests []*walletv1.GrantPinnedResourceGroupRequest
|
||||
errors []error
|
||||
}
|
||||
|
||||
func (f *sequenceRuntimeWallet) GrantPinnedResourceGroup(_ context.Context, req *walletv1.GrantPinnedResourceGroupRequest, _ ...grpc.CallOption) (*walletv1.ResourceGrantResponse, error) {
|
||||
f.requests = append(f.requests, req)
|
||||
index := len(f.requests) - 1
|
||||
if index < len(f.errors) && f.errors[index] != nil {
|
||||
return nil, f.errors[index]
|
||||
}
|
||||
return &walletv1.ResourceGrantResponse{Grant: &walletv1.ResourceGrant{GrantId: "wallet-grant-1"}}, nil
|
||||
}
|
||||
|
||||
type captureRuntimeNotice struct {
|
||||
commands []messageservice.NoticeCommand
|
||||
}
|
||||
|
||||
type settlementLeaseRepository struct {
|
||||
RuntimeRepository
|
||||
periods []domain.RankPeriod
|
||||
finalizable []domain.RankPeriod
|
||||
jobs []domain.RewardJob
|
||||
periodOwner string
|
||||
jobOwner string
|
||||
leaseOwners []string
|
||||
prepared []string
|
||||
finished []string
|
||||
granted []string
|
||||
failed []string
|
||||
}
|
||||
|
||||
func (f *settlementLeaseRepository) ClaimDueActivityTemplateRankPeriods(_ context.Context, workerID string, _ int64, _ time.Duration, _ int32) ([]domain.RankPeriod, error) {
|
||||
f.periodOwner = workerID
|
||||
return append([]domain.RankPeriod(nil), f.periods...), nil
|
||||
}
|
||||
|
||||
func (f *settlementLeaseRepository) PrepareActivityTemplateRankSettlement(_ context.Context, period domain.RankPeriod, _ int64) ([]domain.RewardJob, error) {
|
||||
f.prepared = append(f.prepared, period.BoardType+":"+period.PeriodKey)
|
||||
return []domain.RewardJob{}, nil
|
||||
}
|
||||
|
||||
func (f *settlementLeaseRepository) ClaimFinalizableActivityTemplateRankPeriods(_ context.Context, _ string, _ int64, _ time.Duration, _ int32) ([]domain.RankPeriod, error) {
|
||||
return append([]domain.RankPeriod(nil), f.finalizable...), nil
|
||||
}
|
||||
|
||||
func (f *settlementLeaseRepository) FinishActivityTemplateRankPeriodIfComplete(_ context.Context, period domain.RankPeriod, _ int64) (bool, error) {
|
||||
f.finished = append(f.finished, period.BoardType+":"+period.PeriodKey)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (f *settlementLeaseRepository) ClaimActivityTemplateRewardJobs(_ context.Context, workerID string, _ int64, _ time.Duration, _ int32) ([]domain.RewardJob, error) {
|
||||
f.jobOwner = workerID
|
||||
return append([]domain.RewardJob(nil), f.jobs...), nil
|
||||
}
|
||||
|
||||
func (f *settlementLeaseRepository) ExtendActivityTemplateRewardJobLease(_ context.Context, _ string, workerID string, _ int64, _ time.Duration) error {
|
||||
f.leaseOwners = append(f.leaseOwners, workerID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *settlementLeaseRepository) MarkActivityTemplateRewardJobGrantedWithLease(_ context.Context, jobID, workerID, _ string, _ int64) error {
|
||||
f.leaseOwners = append(f.leaseOwners, workerID)
|
||||
f.granted = append(f.granted, jobID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *settlementLeaseRepository) MarkActivityTemplateRewardJobFailedWithLease(_ context.Context, jobID, workerID, _ string, _ int64) error {
|
||||
f.leaseOwners = append(f.leaseOwners, workerID)
|
||||
f.failed = append(f.failed, jobID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestSettlementSeparatesPeriodMaterializationFromBoundedLeasedJobDelivery(t *testing.T) {
|
||||
periodDaily := domain.RankPeriod{AppCode: "hyapp", TemplateID: "tpl-1", TemplateCode: "summer", VersionNo: 3, BoardType: domain.BoardTypeDaily, PeriodKey: "2026-07-14"}
|
||||
periodTotal := domain.RankPeriod{AppCode: "hyapp", TemplateID: "tpl-1", TemplateCode: "summer", VersionNo: 3, BoardType: domain.BoardTypeTotal, PeriodKey: "total"}
|
||||
job := domain.RewardJob{
|
||||
AppCode: "hyapp", RewardJobID: "job-1", TemplateID: "tpl-1", TemplateCode: "summer", VersionNo: 3,
|
||||
BoardType: domain.BoardTypeDaily, PeriodKey: "2026-07-14", UserID: 1001, ResourceGroupID: 81,
|
||||
WalletCommandID: "wallet-stable", RewardSnapshot: domain.RewardSnapshot{SnapshotID: "rgs-1", SnapshotHash: strings.Repeat("d", 64)},
|
||||
}
|
||||
repository := &settlementLeaseRepository{periods: []domain.RankPeriod{periodDaily, periodTotal}, jobs: []domain.RewardJob{job}}
|
||||
wallet := &sequenceRuntimeWallet{}
|
||||
svc := New(&fakeRepository{}, nil, nil)
|
||||
svc.BindRuntime(repository, wallet)
|
||||
ctx := appcode.WithContext(context.Background(), "hyapp")
|
||||
|
||||
claimed, processed, success, failure, hasMore, err := svc.ProcessRuntimeSettlementBatch(ctx, "run-77", "node-a", 2, 30*time.Second)
|
||||
if err != nil || claimed != 1 || processed != 1 || success != 1 || failure != 0 || !hasMore {
|
||||
t.Fatalf("settlement counters = (%d,%d,%d,%d,%v,%v)", claimed, processed, success, failure, hasMore, err)
|
||||
}
|
||||
if repository.periodOwner != "node-a:run-77" || repository.jobOwner != repository.periodOwner || len(repository.prepared) != 2 ||
|
||||
len(repository.granted) != 1 || len(repository.failed) != 0 || len(wallet.requests) != 1 {
|
||||
t.Fatalf("bounded settlement state mismatch: repository=%+v wallet=%+v", repository, wallet.requests)
|
||||
}
|
||||
for _, owner := range repository.leaseOwners {
|
||||
if owner != repository.jobOwner {
|
||||
t.Fatalf("job completion escaped lease owner: owners=%v job_owner=%q", repository.leaseOwners, repository.jobOwner)
|
||||
}
|
||||
}
|
||||
if len(repository.finished) != 3 { // two post-materialization checks plus one post-job finalization
|
||||
t.Fatalf("period finalization checks = %v", repository.finished)
|
||||
}
|
||||
if activityTemplateRewardLeaseOwner("node-a", "old-run") == activityTemplateRewardLeaseOwner("node-a", "new-run") {
|
||||
t.Fatal("settlement invocations on one node must have distinct fencing owners")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettlementHasMoreIgnoresFiftyBlockedPeriodsAndDoesNotStarvePending(t *testing.T) {
|
||||
// Repository discovery represents the production SQL result after fifty older settling periods with
|
||||
// dead/backoff jobs were filtered out: only the later pending period is actionable.
|
||||
pending := domain.RankPeriod{
|
||||
AppCode: "hyapp", TemplateID: "pending-after-50-blocked", TemplateCode: "summer", VersionNo: 3,
|
||||
BoardType: domain.BoardTypeTotal, PeriodKey: "total",
|
||||
}
|
||||
repository := &settlementLeaseRepository{periods: []domain.RankPeriod{pending}}
|
||||
svc := New(&fakeRepository{}, nil, nil)
|
||||
svc.BindRuntime(repository, &sequenceRuntimeWallet{})
|
||||
ctx := appcode.WithContext(context.Background(), "hyapp")
|
||||
|
||||
claimed, processed, success, failure, hasMore, err := svc.ProcessRuntimeSettlementBatch(
|
||||
ctx, "run-no-hot-loop", "node-a", 50, time.Minute,
|
||||
)
|
||||
if err != nil || claimed != 0 || processed != 0 || success != 0 || failure != 0 {
|
||||
t.Fatalf("settlement counters = (%d,%d,%d,%d,%v): %v", claimed, processed, success, failure, hasMore, err)
|
||||
}
|
||||
if hasMore {
|
||||
t.Fatal("blocked settling periods must not keep has_more true after the sole pending period is processed")
|
||||
}
|
||||
if len(repository.prepared) != 1 || repository.prepared[0] != "total:total" {
|
||||
t.Fatalf("later pending period was starved: prepared=%v", repository.prepared)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *captureRuntimeNotice) CreateActivityNotice(_ context.Context, cmd messageservice.NoticeCommand) (messageservice.NoticeResult, error) {
|
||||
f.commands = append(f.commands, cmd)
|
||||
return messageservice.NoticeResult{Created: true}, nil
|
||||
}
|
||||
|
||||
func TestRetryRewardJobReplaysStableWalletCommandAndCreatesIdempotentWinnerNotice(t *testing.T) {
|
||||
job := domain.RewardJob{
|
||||
RewardJobID: "reward-job-1", TemplateID: "acttpl_1", TemplateCode: "summer", VersionNo: 3,
|
||||
BoardType: domain.BoardTypeTotal, PeriodKey: "total", RankNo: 2, UserID: 88001, ResourceGroupID: 77,
|
||||
WalletCommandID: "activity-template-rank:stable-command",
|
||||
RewardSnapshot: domain.RewardSnapshot{SnapshotID: "rgs_rank", SnapshotHash: strings.Repeat("a", 64), Name: "Champion reward"},
|
||||
}
|
||||
repository := &retryRewardRepository{job: job}
|
||||
wallet := &sequenceRuntimeWallet{errors: []error{errors.New("wallet timeout"), nil}}
|
||||
notices := &captureRuntimeNotice{}
|
||||
svc := New(&fakeRepository{}, nil, nil)
|
||||
svc.BindRuntime(repository, wallet)
|
||||
svc.BindNotice(notices)
|
||||
ctx := appcode.WithContext(context.Background(), "hyapp")
|
||||
|
||||
if _, err := svc.RetryRewardJob(ctx, job.TemplateID, job.RewardJobID, 90001); err == nil {
|
||||
t.Fatal("first retry must expose wallet failure")
|
||||
}
|
||||
got, err := svc.RetryRewardJob(ctx, job.TemplateID, job.RewardJobID, 90001)
|
||||
if err != nil {
|
||||
t.Fatalf("second RetryRewardJob() error = %v", err)
|
||||
}
|
||||
if len(wallet.requests) != 2 || wallet.requests[0].GetCommandId() != job.WalletCommandID || wallet.requests[1].GetCommandId() != job.WalletCommandID {
|
||||
t.Fatalf("wallet commands are not stable: %+v", wallet.requests)
|
||||
}
|
||||
if got.Status != domain.ClaimStatusGranted || got.WalletGrantID != "wallet-grant-1" || len(repository.markedFailed) != 1 || len(repository.markedGranted) != 1 {
|
||||
t.Fatalf("retry result=%+v repository failed=%v granted=%v", got, repository.markedFailed, repository.markedGranted)
|
||||
}
|
||||
if len(repository.leaseOwners) != 7 || repository.leaseOwners[0] == "" ||
|
||||
repository.leaseOwners[0] != repository.leaseOwners[1] || repository.leaseOwners[1] != repository.leaseOwners[2] ||
|
||||
repository.leaseOwners[3] == repository.leaseOwners[0] || repository.leaseOwners[3] != repository.leaseOwners[4] ||
|
||||
repository.leaseOwners[4] != repository.leaseOwners[5] || repository.leaseOwners[5] != repository.leaseOwners[6] {
|
||||
t.Fatalf("manual retry lease fencing mismatch: %+v", repository.leaseOwners)
|
||||
}
|
||||
if len(notices.commands) != 1 || notices.commands[0].ProducerEventID != job.RewardJobID+":granted" ||
|
||||
notices.commands[0].ActionType != "activity_detail" || notices.commands[0].ActionParam != "board_type=total&period_key=total&template_code=summer&version_no=3" {
|
||||
t.Fatalf("winner notice is not stable/readable: %+v", notices.commands)
|
||||
}
|
||||
notice := notices.commands[0]
|
||||
if notice.Title != "activity_template.rank_reward.title" || notice.Summary != "activity_template.rank_reward.summary" ||
|
||||
strings.Contains(notice.Title, "Daily leaderboard") || notice.Metadata["content_key"] != "activity_template.rank_reward_granted" ||
|
||||
notice.Metadata["template_code"] != "summer" || notice.Metadata["board_type"] != domain.BoardTypeTotal ||
|
||||
notice.Metadata["period_key"] != "total" || notice.Metadata["reward_name"] != "Champion reward" {
|
||||
t.Fatalf("winner notice must use localization keys and immutable metadata: %+v", notice)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskClaimWalletFailureRemainsRetryableAfterActivityEnds(t *testing.T) {
|
||||
taskDay := "2026-07-14"
|
||||
endedAt := time.Date(2026, 7, 14, 23, 59, 0, 0, time.UTC)
|
||||
runtime := domain.Runtime{
|
||||
Template: domain.Template{TemplateID: "acttpl_1", TemplateCode: "summer", EndMS: endedAt.UnixMilli()},
|
||||
VersionNo: 3, RuntimeFromMS: endedAt.Add(-24 * time.Hour).UnixMilli(), RuntimeToMS: endedAt.UnixMilli(),
|
||||
}
|
||||
claim := domain.TaskClaim{
|
||||
ClaimID: "task-claim-1", TemplateID: runtime.Template.TemplateID, TemplateCode: runtime.Template.TemplateCode,
|
||||
VersionNo: runtime.VersionNo, TaskDay: taskDay, TaskKey: "gift-10", UserID: 77001,
|
||||
RewardResourceGroupID: 88, WalletCommandID: "activity-template-task:stable-command",
|
||||
RewardSnapshot: domain.RewardSnapshot{SnapshotID: "rgs_task", SnapshotHash: strings.Repeat("b", 64)},
|
||||
}
|
||||
repository := &retryRewardRepository{taskRuntime: runtime, taskClaim: claim}
|
||||
wallet := &sequenceRuntimeWallet{errors: []error{errors.New("wallet timeout"), nil}}
|
||||
svc := New(&fakeRepository{}, nil, nil)
|
||||
svc.BindRuntime(repository, wallet)
|
||||
svc.now = func() time.Time { return endedAt.Add(2 * time.Hour) }
|
||||
command := domain.ClaimCommand{
|
||||
UserID: claim.UserID, RegionID: 6, TemplateCode: claim.TemplateCode, VersionNo: claim.VersionNo, TaskDay: taskDay,
|
||||
TaskKey: claim.TaskKey, CommandID: "client-command-1",
|
||||
}
|
||||
ctx := appcode.WithContext(context.Background(), "hyapp")
|
||||
|
||||
if _, err := svc.ClaimRuntimeTaskReward(ctx, command); err == nil {
|
||||
t.Fatal("first claim must expose wallet failure")
|
||||
}
|
||||
got, err := svc.ClaimRuntimeTaskReward(ctx, command)
|
||||
if err != nil {
|
||||
t.Fatalf("retry after activity end error = %v", err)
|
||||
}
|
||||
if repository.taskVersionNo != claim.VersionNo || repository.taskClaimCalls != 2 || len(repository.taskFailed) != 1 || len(repository.taskGranted) != 1 || got.Status != domain.ClaimStatusGranted {
|
||||
t.Fatalf("task compensation result=%+v repository=%+v", got, repository)
|
||||
}
|
||||
if len(wallet.requests) != 2 || wallet.requests[0].GetCommandId() != claim.WalletCommandID || wallet.requests[1].GetCommandId() != claim.WalletCommandID {
|
||||
t.Fatalf("task wallet command changed across retries: %+v", wallet.requests)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,524 @@
|
||||
package activitytemplate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/proto"
|
||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/xerr"
|
||||
domain "hyapp/services/activity-service/internal/domain/activitytemplate"
|
||||
)
|
||||
|
||||
type fakeRepository struct {
|
||||
current domain.Template
|
||||
versions map[int64]domain.Version
|
||||
listedVersions []domain.Version
|
||||
updateCalls int
|
||||
statusCalls int
|
||||
createCalls int
|
||||
}
|
||||
|
||||
func (f *fakeRepository) ListActivityTemplates(context.Context, domain.ListQuery) ([]domain.Summary, int64, error) {
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
func (f *fakeRepository) GetActivityTemplate(_ context.Context, templateID string) (domain.Template, error) {
|
||||
if f.current.TemplateID != templateID {
|
||||
return domain.Template{}, xerr.New(xerr.NotFound, "activity template not found")
|
||||
}
|
||||
return f.current, nil
|
||||
}
|
||||
|
||||
func (f *fakeRepository) CreateActivityTemplate(_ context.Context, item domain.Template, operatorAdminID int64, nowMS int64) (domain.Template, error) {
|
||||
f.createCalls++
|
||||
item.TemplateID = "acttpl_created"
|
||||
item.Status = domain.StatusDraft
|
||||
item.Revision = 1
|
||||
item.CreatedByAdminID = operatorAdminID
|
||||
item.UpdatedByAdminID = operatorAdminID
|
||||
item.CreatedAtMS = nowMS
|
||||
item.UpdatedAtMS = nowMS
|
||||
f.current = item
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (f *fakeRepository) UpdateActivityTemplate(_ context.Context, command domain.UpdateCommand, nowMS int64) (domain.Template, error) {
|
||||
f.updateCalls++
|
||||
item := command.Template
|
||||
item.Revision = command.ExpectedRevision + 1
|
||||
item.UpdatedByAdminID = command.OperatorAdminID
|
||||
item.UpdatedAtMS = nowMS
|
||||
f.current = item
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (f *fakeRepository) SetActivityTemplateStatus(_ context.Context, command domain.StatusCommand, nowMS int64) (domain.Template, error) {
|
||||
f.statusCalls++
|
||||
item := f.current
|
||||
item.Status = command.Status
|
||||
item.Revision++
|
||||
item.UpdatedByAdminID = command.OperatorAdminID
|
||||
item.UpdatedAtMS = nowMS
|
||||
if command.Status == domain.StatusPublished {
|
||||
item.PublishedVersion++
|
||||
item.PublishedByAdminID = command.OperatorAdminID
|
||||
item.PublishedAtMS = nowMS
|
||||
}
|
||||
f.current = item
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (f *fakeRepository) ListActivityTemplateVersions(context.Context, string, int32, int32) ([]domain.Version, int64, error) {
|
||||
return append([]domain.Version(nil), f.listedVersions...), int64(len(f.listedVersions)), nil
|
||||
}
|
||||
|
||||
func (f *fakeRepository) GetActivityTemplateVersion(_ context.Context, templateID string, versionNo int64) (domain.Version, error) {
|
||||
item, ok := f.versions[versionNo]
|
||||
if !ok || item.TemplateID != templateID {
|
||||
return domain.Version{}, xerr.New(xerr.NotFound, "activity template version not found")
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
type fakeCatalog struct {
|
||||
gift *walletv1.GiftConfig
|
||||
groups map[int64]*walletv1.ResourceGroup
|
||||
lastGift *walletv1.ListGiftConfigsRequest
|
||||
pins []*walletv1.PinResourceGroupSnapshotRequest
|
||||
}
|
||||
|
||||
type fakeRegionSource struct {
|
||||
regionIDs []int64
|
||||
err error
|
||||
}
|
||||
|
||||
func (f fakeRegionSource) ListActiveRegionIDs(context.Context) ([]int64, error) {
|
||||
return append([]int64(nil), f.regionIDs...), f.err
|
||||
}
|
||||
|
||||
func (f *fakeCatalog) ListGiftConfigs(_ context.Context, req *walletv1.ListGiftConfigsRequest, _ ...grpc.CallOption) (*walletv1.ListGiftConfigsResponse, error) {
|
||||
f.lastGift = req
|
||||
if f.gift == nil || f.gift.GetGiftId() != req.GetKeyword() {
|
||||
return &walletv1.ListGiftConfigsResponse{}, nil
|
||||
}
|
||||
return &walletv1.ListGiftConfigsResponse{Gifts: []*walletv1.GiftConfig{f.gift}, Total: 1}, nil
|
||||
}
|
||||
|
||||
func (f *fakeCatalog) GetResourceGroup(_ context.Context, req *walletv1.GetResourceGroupRequest, _ ...grpc.CallOption) (*walletv1.GetResourceGroupResponse, error) {
|
||||
group := f.groups[req.GetGroupId()]
|
||||
if group == nil {
|
||||
return nil, xerr.New(xerr.NotFound, "resource group not found")
|
||||
}
|
||||
return &walletv1.GetResourceGroupResponse{Group: group, SourceContentHash: fakeResourceGroupHash(group)}, nil
|
||||
}
|
||||
|
||||
func (f *fakeCatalog) PinResourceGroupSnapshot(_ context.Context, req *walletv1.PinResourceGroupSnapshotRequest, _ ...grpc.CallOption) (*walletv1.PinResourceGroupSnapshotResponse, error) {
|
||||
f.pins = append(f.pins, req)
|
||||
group := f.groups[req.GetGroupId()]
|
||||
if group == nil {
|
||||
return nil, xerr.New(xerr.NotFound, "resource group not found")
|
||||
}
|
||||
hash := fakeResourceGroupHash(group)
|
||||
if req.GetExpectedSourceContentHash() != hash || req.GetExpectedGroupUpdatedAtMs() != group.GetUpdatedAtMs() {
|
||||
return nil, xerr.New(xerr.Conflict, "resource group content changed")
|
||||
}
|
||||
return &walletv1.PinResourceGroupSnapshotResponse{Snapshot: &walletv1.ResourceGroupSnapshot{
|
||||
AppCode: req.GetAppCode(), SnapshotId: "rgs_" + hash[:24], PinKey: req.GetPinKey(), SourceGroupId: group.GetGroupId(),
|
||||
VersionNo: 1, SnapshotHash: hash, Group: group, CreatedByUserId: req.GetOperatorUserId(), CreatedAtMs: 1,
|
||||
SourceGroupUpdatedAtMs: group.GetUpdatedAtMs(), SourceContentHash: hash,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func fakeResourceGroupHash(group *walletv1.ResourceGroup) string {
|
||||
body, _ := proto.MarshalOptions{Deterministic: true}.Marshal(group)
|
||||
sum := sha256.Sum256(body)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func TestLifecycleStatusUsesUTCLeftClosedRightOpenBoundary(t *testing.T) {
|
||||
const startMS = int64(10_000)
|
||||
const endMS = int64(20_000)
|
||||
tests := []struct {
|
||||
name string
|
||||
now int64
|
||||
want string
|
||||
}{
|
||||
{name: "before start", now: startMS - 1, want: domain.LifecycleScheduled},
|
||||
{name: "at start", now: startMS, want: domain.LifecycleOngoing},
|
||||
{name: "before end", now: endMS - 1, want: domain.LifecycleOngoing},
|
||||
{name: "at end", now: endMS, want: domain.LifecycleEnded},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := lifecycleStatus(domain.StatusPublished, startMS, endMS, tt.now); got != tt.want {
|
||||
t.Fatalf("lifecycleStatus() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListVersionsUsesActualRuntimeWindowAfterDisableAndRepublish(t *testing.T) {
|
||||
const (
|
||||
startMS = int64(100)
|
||||
endMS = int64(1_000)
|
||||
nowMS = int64(700)
|
||||
)
|
||||
snapshot := domain.Template{
|
||||
TemplateID: "acttpl_republished", TemplateCode: "summer", Status: domain.StatusPublished,
|
||||
StartMS: startMS, EndMS: endMS,
|
||||
}
|
||||
repository := &fakeRepository{listedVersions: []domain.Version{
|
||||
{TemplateID: snapshot.TemplateID, VersionNo: 2, Snapshot: snapshot, RuntimeFromMS: 600},
|
||||
{TemplateID: snapshot.TemplateID, VersionNo: 1, Snapshot: snapshot, RuntimeFromMS: startMS, RuntimeToMS: 500},
|
||||
}}
|
||||
svc := New(repository, nil, nil)
|
||||
svc.now = func() time.Time { return time.UnixMilli(nowMS) }
|
||||
|
||||
versions, total, err := svc.ListVersions(context.Background(), snapshot.TemplateID, 1, 20)
|
||||
if err != nil {
|
||||
t.Fatalf("ListVersions() error = %v", err)
|
||||
}
|
||||
if total != 2 || len(versions) != 2 {
|
||||
t.Fatalf("ListVersions() total/items = %d/%d", total, len(versions))
|
||||
}
|
||||
if versions[0].VersionNo != 2 || versions[0].Snapshot.LifecycleStatus != domain.LifecycleOngoing || versions[0].RuntimeFromMS != 600 || versions[0].RuntimeToMS != 0 {
|
||||
t.Fatalf("republished v2 lifecycle/window = %+v", versions[0])
|
||||
}
|
||||
if versions[1].VersionNo != 1 || versions[1].Snapshot.LifecycleStatus != domain.LifecycleDisabled || versions[1].RuntimeFromMS != startMS || versions[1].RuntimeToMS != 500 {
|
||||
t.Fatalf("disabled v1 lifecycle/window = %+v", versions[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeLifecycleStatusUsesActualWindowBoundaries(t *testing.T) {
|
||||
item := domain.Template{Status: domain.StatusPublished, StartMS: 100, EndMS: 1_000}
|
||||
tests := []struct {
|
||||
name string
|
||||
runtimeFromMS int64
|
||||
runtimeToMS int64
|
||||
nowMS int64
|
||||
want string
|
||||
}{
|
||||
{name: "scheduled", runtimeFromMS: 200, nowMS: 199, want: domain.LifecycleScheduled},
|
||||
{name: "ongoing at runtime start", runtimeFromMS: 200, nowMS: 200, want: domain.LifecycleOngoing},
|
||||
{name: "disabled at exclusive runtime end", runtimeFromMS: 200, runtimeToMS: 500, nowMS: 500, want: domain.LifecycleDisabled},
|
||||
{name: "disabled remains historical state", runtimeFromMS: 200, runtimeToMS: 500, nowMS: 1_500, want: domain.LifecycleDisabled},
|
||||
{name: "naturally ended", runtimeFromMS: 200, nowMS: 1_000, want: domain.LifecycleEnded},
|
||||
{name: "disable after planned end stays ended", runtimeFromMS: 200, runtimeToMS: 1_200, nowMS: 1_200, want: domain.LifecycleEnded},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := RuntimeLifecycleStatus(item, tt.runtimeFromMS, tt.runtimeToMS, tt.nowMS); got != tt.want {
|
||||
t.Fatalf("RuntimeLifecycleStatus() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRejectsNegativeTimeFilter(t *testing.T) {
|
||||
svc := New(&fakeRepository{}, nil, nil)
|
||||
_, _, err := svc.List(context.Background(), domain.ListQuery{StartMS: -1})
|
||||
if !xerr.IsCode(err, xerr.InvalidArgument) {
|
||||
t.Fatalf("List() error = %v, want invalid argument", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePublishRejectsOverlappingRankRangesAndNonObjectDisplayConfig(t *testing.T) {
|
||||
nowMS := int64(1_800_000_000_000)
|
||||
item := publishableTemplate(nowMS)
|
||||
item.RankRewards = append(item.RankRewards, domain.RankReward{
|
||||
BoardType: domain.BoardTypeDaily, RankFrom: 3, RankTo: 5, ResourceGroupID: 102, SortOrder: 2,
|
||||
})
|
||||
item.DisplayConfigJSON = "null"
|
||||
svc := New(&fakeRepository{}, nil, nil)
|
||||
svc.now = func() time.Time { return time.UnixMilli(nowMS) }
|
||||
|
||||
issues, err := svc.Validate(context.Background(), item, true)
|
||||
if err != nil {
|
||||
t.Fatalf("Validate() error = %v", err)
|
||||
}
|
||||
if !hasIssue(issues, "rank_rewards", "overlap") {
|
||||
t.Fatalf("expected rank overlap issue, got %+v", issues)
|
||||
}
|
||||
if !hasIssue(issues, "display_config_json", "invalid_json") {
|
||||
t.Fatalf("expected JSON object issue, got %+v", issues)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRejectsMultipleDailyVisitTasks(t *testing.T) {
|
||||
nowMS := int64(1_800_000_000_000)
|
||||
item := publishableTemplate(nowMS)
|
||||
item.Tasks = append(item.Tasks, domain.Task{
|
||||
TaskKey: "daily_visit_bonus", TaskType: domain.TaskTypeDailyVisit,
|
||||
TargetValue: 1, RewardResourceGroupID: 101, SortOrder: 2,
|
||||
})
|
||||
svc := New(&fakeRepository{}, nil, nil)
|
||||
svc.now = func() time.Time { return time.UnixMilli(nowMS) }
|
||||
|
||||
issues, err := svc.Validate(context.Background(), item, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Validate() error = %v", err)
|
||||
}
|
||||
if !hasIssue(issues, "tasks", "duplicate_daily_visit") {
|
||||
t.Fatalf("expected duplicate daily_visit issue, got %+v", issues)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetStatusPublishesOnlyAfterOwnerReferencesValidate(t *testing.T) {
|
||||
nowMS := int64(1_800_000_000_000)
|
||||
item := publishableTemplate(nowMS)
|
||||
repository := &fakeRepository{current: item}
|
||||
catalog := publishableCatalog(item)
|
||||
svc := New(repository, catalog, fakeRegionSource{regionIDs: []int64{1}})
|
||||
svc.now = func() time.Time { return time.UnixMilli(nowMS) }
|
||||
ctx := appcode.WithContext(context.Background(), "hyapp")
|
||||
|
||||
got, err := svc.SetStatus(ctx, domain.StatusCommand{
|
||||
TemplateID: item.TemplateID, Status: domain.StatusPublished,
|
||||
ExpectedRevision: item.Revision, OperatorAdminID: 90001,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SetStatus() error = %v", err)
|
||||
}
|
||||
if got.Status != domain.StatusPublished || got.PublishedVersion != 1 || got.Revision != item.Revision+1 {
|
||||
t.Fatalf("published template metadata = %+v", got)
|
||||
}
|
||||
if repository.statusCalls != 1 {
|
||||
t.Fatalf("status repository calls = %d, want 1", repository.statusCalls)
|
||||
}
|
||||
if catalog.lastGift == nil || catalog.lastGift.GetStatus() != "active" || catalog.lastGift.GetActiveOnly() {
|
||||
t.Fatalf("gift lookup must load active owner config without current-time-only filtering: %+v", catalog.lastGift)
|
||||
}
|
||||
if len(catalog.pins) != 3 {
|
||||
t.Fatalf("publish pin calls = %d, want 3", len(catalog.pins))
|
||||
}
|
||||
for _, pin := range catalog.pins {
|
||||
if pin.GetRequiredAllRegions() || len(pin.GetRequiredRegionIds()) != 1 || pin.GetRequiredRegionIds()[0] != 1 {
|
||||
t.Fatalf("targeted activity pin omitted required region coverage: %+v", pin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetStatusAllRegionsRequiresGlobalRewardGiftCoverageAtWalletPin(t *testing.T) {
|
||||
nowMS := int64(1_800_000_000_000)
|
||||
item := publishableTemplate(nowMS)
|
||||
item.AllRegions = true
|
||||
item.RegionIDs = nil
|
||||
repository := &fakeRepository{current: item}
|
||||
catalog := publishableCatalog(item)
|
||||
svc := New(repository, catalog, fakeRegionSource{regionIDs: []int64{1, 2}})
|
||||
svc.now = func() time.Time { return time.UnixMilli(nowMS) }
|
||||
|
||||
if _, err := svc.SetStatus(appcode.WithContext(context.Background(), "hyapp"), domain.StatusCommand{
|
||||
TemplateID: item.TemplateID, Status: domain.StatusPublished,
|
||||
ExpectedRevision: item.Revision, OperatorAdminID: 90001,
|
||||
}); err != nil {
|
||||
t.Fatalf("publish all-regions template: %v", err)
|
||||
}
|
||||
if len(catalog.pins) != 3 {
|
||||
t.Fatalf("all-regions publish pin calls = %d, want 3", len(catalog.pins))
|
||||
}
|
||||
for _, pin := range catalog.pins {
|
||||
if !pin.GetRequiredAllRegions() || len(pin.GetRequiredRegionIds()) != 0 {
|
||||
t.Fatalf("all-regions activity must require global reward gift coverage: %+v", pin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePublishRejectsGiftThatExpiresDuringActivity(t *testing.T) {
|
||||
nowMS := int64(1_800_000_000_000)
|
||||
item := publishableTemplate(nowMS)
|
||||
catalog := publishableCatalog(item)
|
||||
catalog.gift.EffectiveToMs = item.EndMS - 1
|
||||
svc := New(&fakeRepository{}, catalog, fakeRegionSource{regionIDs: []int64{1}})
|
||||
svc.now = func() time.Time { return time.UnixMilli(nowMS) }
|
||||
|
||||
issues, err := svc.Validate(context.Background(), item, true)
|
||||
if err != nil {
|
||||
t.Fatalf("Validate() error = %v", err)
|
||||
}
|
||||
if !hasIssue(issues, "gifts", "gift_period_mismatch") {
|
||||
t.Fatalf("expected gift period issue, got %+v", issues)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePublishRejectsInactiveTargetRegion(t *testing.T) {
|
||||
nowMS := int64(1_800_000_000_000)
|
||||
item := publishableTemplate(nowMS)
|
||||
svc := New(&fakeRepository{}, publishableCatalog(item), fakeRegionSource{regionIDs: []int64{2}})
|
||||
svc.now = func() time.Time { return time.UnixMilli(nowMS) }
|
||||
|
||||
issues, err := svc.Validate(context.Background(), item, true)
|
||||
if err != nil {
|
||||
t.Fatalf("Validate() error = %v", err)
|
||||
}
|
||||
if !hasIssue(issues, "region_ids", "region_not_active") {
|
||||
t.Fatalf("expected inactive region issue, got %+v", issues)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateRejectsStaleRevisionBeforeWriting(t *testing.T) {
|
||||
nowMS := int64(1_800_000_000_000)
|
||||
current := publishableTemplate(nowMS)
|
||||
current.Status = domain.StatusDisabled
|
||||
repository := &fakeRepository{current: current}
|
||||
svc := New(repository, nil, nil)
|
||||
svc.now = func() time.Time { return time.UnixMilli(nowMS) }
|
||||
|
||||
_, err := svc.Update(context.Background(), domain.UpdateCommand{
|
||||
Template: current, ExpectedRevision: current.Revision - 1, OperatorAdminID: 90001,
|
||||
})
|
||||
if !xerr.IsCode(err, xerr.Conflict) {
|
||||
t.Fatalf("Update() error = %v, want conflict", err)
|
||||
}
|
||||
if repository.updateCalls != 0 {
|
||||
t.Fatalf("stale update wrote repository %d times", repository.updateCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateRejectsTemplateCodeChangeAfterCreate(t *testing.T) {
|
||||
nowMS := int64(1_800_000_000_000)
|
||||
current := publishableTemplate(nowMS)
|
||||
current.Status = domain.StatusDisabled
|
||||
repository := &fakeRepository{current: current}
|
||||
svc := New(repository, nil, nil)
|
||||
changed := current
|
||||
changed.TemplateCode = "replacement_code"
|
||||
|
||||
_, err := svc.Update(context.Background(), domain.UpdateCommand{
|
||||
Template: changed, ExpectedRevision: current.Revision, OperatorAdminID: 90001,
|
||||
})
|
||||
if !xerr.IsCode(err, xerr.Conflict) || repository.updateCalls != 0 {
|
||||
t.Fatalf("code mutation error=%v update_calls=%d", err, repository.updateCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteRequiresScheduledOrOngoingPublishedTemplateToBeDisabledFirst(t *testing.T) {
|
||||
nowMS := int64(1_800_000_000_000)
|
||||
current := publishableTemplate(nowMS)
|
||||
current.Status = domain.StatusPublished
|
||||
repository := &fakeRepository{current: current}
|
||||
svc := New(repository, nil, nil)
|
||||
svc.now = func() time.Time { return time.UnixMilli(nowMS) }
|
||||
|
||||
_, _, err := svc.Delete(context.Background(), domain.StatusCommand{
|
||||
TemplateID: current.TemplateID, ExpectedRevision: current.Revision, OperatorAdminID: 90001,
|
||||
})
|
||||
if !xerr.IsCode(err, xerr.Conflict) {
|
||||
t.Fatalf("Delete() error = %v, want conflict", err)
|
||||
}
|
||||
if repository.statusCalls != 0 {
|
||||
t.Fatalf("scheduled template was archived %d times", repository.statusCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteAllowsEndedPublishedTemplateToArchive(t *testing.T) {
|
||||
nowMS := int64(1_800_000_000_000)
|
||||
current := publishableTemplate(nowMS)
|
||||
current.Status = domain.StatusPublished
|
||||
current.StartMS = nowMS - 2_000
|
||||
current.EndMS = nowMS - 1_000
|
||||
repository := &fakeRepository{current: current}
|
||||
svc := New(repository, nil, nil)
|
||||
svc.now = func() time.Time { return time.UnixMilli(nowMS) }
|
||||
|
||||
got, archived, err := svc.Delete(context.Background(), domain.StatusCommand{
|
||||
TemplateID: current.TemplateID, ExpectedRevision: current.Revision, OperatorAdminID: 90001,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Delete() error = %v", err)
|
||||
}
|
||||
if !archived || got.Status != domain.StatusArchived || got.LifecycleStatus != domain.LifecycleArchived {
|
||||
t.Fatalf("Delete() = archived:%v template:%+v", archived, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClonePreservesConfigurationButClearsRuntimeIdentityAndSchedule(t *testing.T) {
|
||||
nowMS := int64(1_800_000_000_000)
|
||||
source := publishableTemplate(nowMS)
|
||||
source.Status = domain.StatusDisabled
|
||||
source.PublishedVersion = 3
|
||||
source.PublishedAtMS = nowMS - 10_000
|
||||
repository := &fakeRepository{current: source}
|
||||
svc := New(repository, nil, nil)
|
||||
svc.now = func() time.Time { return time.UnixMilli(nowMS) }
|
||||
|
||||
got, err := svc.Clone(context.Background(), domain.CloneCommand{
|
||||
SourceTemplateID: source.TemplateID, TemplateCode: "gift_challenge_copy", Name: "Gift Challenge Copy", OperatorAdminID: 90002,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Clone() error = %v", err)
|
||||
}
|
||||
if got.TemplateID == source.TemplateID || got.TemplateCode != "gift_challenge_copy" || got.Status != domain.StatusDraft {
|
||||
t.Fatalf("clone identity/status = %+v", got)
|
||||
}
|
||||
if got.StartMS != 0 || got.EndMS != 0 || got.PublishedVersion != 0 || got.PublishedAtMS != 0 {
|
||||
t.Fatalf("clone leaked runtime metadata = %+v", got)
|
||||
}
|
||||
if len(got.RegionIDs) != 1 || got.RegionIDs[0] != source.RegionIDs[0] ||
|
||||
len(got.Tasks) != 1 || got.Tasks[0].RewardResourceGroupID != source.Tasks[0].RewardResourceGroupID ||
|
||||
len(got.RankRewards) != len(source.RankRewards) {
|
||||
t.Fatalf("clone did not preserve configuration: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func publishableTemplate(nowMS int64) domain.Template {
|
||||
return domain.Template{
|
||||
TemplateID: "acttpl_source", TemplateCode: "gift_challenge_2026", Name: "Gift Challenge 2026",
|
||||
ActivityType: domain.ActivityTypeGiftChallenge, Status: domain.StatusDraft, Revision: 7,
|
||||
StartMS: nowMS + 60_000, EndMS: nowMS + int64((7*24*time.Hour)/time.Millisecond),
|
||||
AllRegions: false, RegionIDs: []int64{1},
|
||||
Locales: []domain.Locale{{Locale: "en", Title: "Gift Challenge", Rules: "Send gifts and complete tasks."}},
|
||||
Gifts: []domain.Gift{{
|
||||
GiftID: "gift_rose", SortOrder: 1, Name: "Rose", IconURL: "https://cdn.example.com/gifts/rose.png", CoinPrice: 100, PriceVersion: "v3",
|
||||
}},
|
||||
Tasks: []domain.Task{{
|
||||
TaskKey: "daily_visit", TaskType: domain.TaskTypeDailyVisit, TargetValue: 1, RewardResourceGroupID: 101, SortOrder: 1,
|
||||
}},
|
||||
DailyLeaderboardSize: 100,
|
||||
TotalLeaderboardSize: 100,
|
||||
RankRewards: []domain.RankReward{
|
||||
{BoardType: domain.BoardTypeDaily, RankFrom: 1, RankTo: 3, ResourceGroupID: 102, SortOrder: 1},
|
||||
{BoardType: domain.BoardTypeTotal, RankFrom: 1, RankTo: 3, ResourceGroupID: 103, SortOrder: 1},
|
||||
},
|
||||
DisplayConfigJSON: `{}`,
|
||||
}
|
||||
}
|
||||
|
||||
func publishableCatalog(item domain.Template) *fakeCatalog {
|
||||
groups := make(map[int64]*walletv1.ResourceGroup)
|
||||
for _, groupID := range []int64{101, 102, 103} {
|
||||
groups[groupID] = &walletv1.ResourceGroup{
|
||||
GroupId: groupID, Status: "active", Name: "Reward", UpdatedAtMs: 1_800_000_000_001,
|
||||
Items: []*walletv1.ResourceGroupItem{{
|
||||
GroupId: groupID, ResourceId: groupID + 1000, Quantity: 1, ItemType: "resource",
|
||||
Resource: &walletv1.Resource{ResourceId: groupID + 1000, ResourceType: "badge", Name: "Badge", Status: "active", Grantable: true, GrantStrategy: "new_entitlement"},
|
||||
}},
|
||||
}
|
||||
}
|
||||
return &fakeCatalog{
|
||||
gift: &walletv1.GiftConfig{
|
||||
GiftId: item.Gifts[0].GiftID, Status: "active", Name: item.Gifts[0].Name,
|
||||
CoinPrice: item.Gifts[0].CoinPrice, PriceVersion: item.Gifts[0].PriceVersion,
|
||||
RegionIds: []int64{0},
|
||||
Resource: &walletv1.Resource{
|
||||
Status: "active", PreviewUrl: item.Gifts[0].IconURL, AssetUrl: "https://cdn.example.com/gifts/rose-animation.svga",
|
||||
},
|
||||
},
|
||||
groups: groups,
|
||||
}
|
||||
}
|
||||
|
||||
func hasIssue(issues []domain.ValidationIssue, field string, code string) bool {
|
||||
for _, issue := range issues {
|
||||
if issue.Field == field && issue.Code == code {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@ -0,0 +1,787 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
mysqlDriver "github.com/go-sql-driver/mysql"
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/idgen"
|
||||
"hyapp/pkg/xerr"
|
||||
domain "hyapp/services/activity-service/internal/domain/activitytemplate"
|
||||
)
|
||||
|
||||
const activityTemplateSelect = `
|
||||
SELECT app_code, template_id, template_code, name, activity_type, status, start_ms, end_ms, all_regions,
|
||||
daily_leaderboard_size, total_leaderboard_size, CAST(display_config_json AS CHAR), revision, published_version,
|
||||
created_by_admin_id, updated_by_admin_id, published_by_admin_id, created_at_ms, updated_at_ms,
|
||||
published_at_ms, archived_at_ms
|
||||
FROM activity_templates`
|
||||
|
||||
type activityTemplateQuerier interface {
|
||||
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
|
||||
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
|
||||
}
|
||||
|
||||
type activityTemplateScanner interface {
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
||||
// ListActivityTemplates 查询轻量列表投影,只额外附加区域 ID;大段规则、任务和素材只在详情接口读取。
|
||||
func (r *Repository) ListActivityTemplates(ctx context.Context, query domain.ListQuery) ([]domain.Summary, int64, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
}
|
||||
where, args := activityTemplateListWhere(ctx, query)
|
||||
var total int64
|
||||
if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM activity_templates t `+where, args...).Scan(&total); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
pageSize := normalizePageSize(query.PageSize)
|
||||
page := query.Page
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT t.app_code, t.template_id, t.template_code, t.name, t.activity_type, t.status, t.start_ms, t.end_ms,
|
||||
t.all_regions, t.revision, t.published_version, t.created_by_admin_id, t.updated_by_admin_id,
|
||||
t.created_at_ms, t.updated_at_ms
|
||||
FROM activity_templates t `+where+`
|
||||
ORDER BY t.updated_at_ms DESC, t.template_id DESC
|
||||
LIMIT ? OFFSET ?`, append(args, int(pageSize), int((page-1)*pageSize))...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := make([]domain.Summary, 0)
|
||||
for rows.Next() {
|
||||
var item domain.Summary
|
||||
if err := rows.Scan(
|
||||
&item.AppCode, &item.TemplateID, &item.TemplateCode, &item.Name, &item.ActivityType, &item.Status,
|
||||
&item.StartMS, &item.EndMS, &item.AllRegions, &item.Revision, &item.PublishedVersion,
|
||||
&item.CreatedByAdminID, &item.UpdatedByAdminID, &item.CreatedAtMS, &item.UpdatedAtMS,
|
||||
); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := attachActivityTemplateSummaryRegions(ctx, r.db, items); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetActivityTemplate(ctx context.Context, templateID string) (domain.Template, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return domain.Template{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
}
|
||||
return getActivityTemplateAggregate(ctx, r.db, appcode.FromContext(ctx), strings.TrimSpace(templateID), false)
|
||||
}
|
||||
|
||||
func (r *Repository) CreateActivityTemplate(ctx context.Context, item domain.Template, operatorAdminID int64, nowMS int64) (domain.Template, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return domain.Template{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
}
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return domain.Template{}, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
item.AppCode = appcode.FromContext(ctx)
|
||||
item.TemplateID = idgen.New("acttpl")
|
||||
item.Status = domain.StatusDraft
|
||||
item.Revision = 1
|
||||
item.PublishedVersion = 0
|
||||
item.CreatedByAdminID = operatorAdminID
|
||||
item.UpdatedByAdminID = operatorAdminID
|
||||
item.PublishedByAdminID = 0
|
||||
item.CreatedAtMS = nowMS
|
||||
item.UpdatedAtMS = nowMS
|
||||
item.PublishedAtMS = 0
|
||||
item.ArchivedAtMS = 0
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO activity_templates (
|
||||
app_code, template_id, template_code, name, activity_type, status, start_ms, end_ms, all_regions,
|
||||
daily_leaderboard_size, total_leaderboard_size, display_config_json, revision, published_version,
|
||||
created_by_admin_id, updated_by_admin_id, published_by_admin_id, created_at_ms, updated_at_ms, published_at_ms, archived_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS JSON), ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
item.AppCode, item.TemplateID, item.TemplateCode, item.Name, item.ActivityType, item.Status, item.StartMS, item.EndMS, item.AllRegions,
|
||||
item.DailyLeaderboardSize, item.TotalLeaderboardSize, item.DisplayConfigJSON, item.Revision, item.PublishedVersion,
|
||||
item.CreatedByAdminID, item.UpdatedByAdminID, item.PublishedByAdminID, item.CreatedAtMS, item.UpdatedAtMS, item.PublishedAtMS, item.ArchivedAtMS,
|
||||
); err != nil {
|
||||
return domain.Template{}, activityTemplateWriteError(err)
|
||||
}
|
||||
if err := replaceActivityTemplateChildren(ctx, tx, item, nowMS); err != nil {
|
||||
return domain.Template{}, activityTemplateWriteError(err)
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return domain.Template{}, activityTemplateWriteError(err)
|
||||
}
|
||||
return r.GetActivityTemplate(ctx, item.TemplateID)
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateActivityTemplate(ctx context.Context, command domain.UpdateCommand, nowMS int64) (domain.Template, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return domain.Template{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
}
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return domain.Template{}, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
appCode := appcode.FromContext(ctx)
|
||||
current, err := getActivityTemplateAggregate(ctx, tx, appCode, command.Template.TemplateID, true)
|
||||
if err != nil {
|
||||
return domain.Template{}, err
|
||||
}
|
||||
if current.Revision != command.ExpectedRevision {
|
||||
return domain.Template{}, xerr.New(xerr.Conflict, "activity template revision changed")
|
||||
}
|
||||
// Repository 再做一次身份防线,避免未来新增调用方绕过 service 直接改写 deep-link/runtime code。
|
||||
if command.Template.TemplateCode != current.TemplateCode {
|
||||
return domain.Template{}, xerr.New(xerr.Conflict, "activity template_code is immutable")
|
||||
}
|
||||
if current.Status == domain.StatusArchived {
|
||||
return domain.Template{}, xerr.New(xerr.Conflict, "archived activity template is read-only")
|
||||
}
|
||||
item := command.Template
|
||||
item.AppCode = appCode
|
||||
item.TemplateID = current.TemplateID
|
||||
item.Status = current.Status
|
||||
item.Revision = current.Revision + 1
|
||||
item.PublishedVersion = current.PublishedVersion
|
||||
item.CreatedByAdminID = current.CreatedByAdminID
|
||||
item.CreatedAtMS = current.CreatedAtMS
|
||||
item.UpdatedByAdminID = command.OperatorAdminID
|
||||
item.UpdatedAtMS = nowMS
|
||||
item.PublishedByAdminID = current.PublishedByAdminID
|
||||
item.PublishedAtMS = current.PublishedAtMS
|
||||
item.ArchivedAtMS = current.ArchivedAtMS
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
UPDATE activity_templates
|
||||
SET name = ?, activity_type = ?, start_ms = ?, end_ms = ?, all_regions = ?,
|
||||
daily_leaderboard_size = ?, total_leaderboard_size = ?, display_config_json = CAST(? AS JSON),
|
||||
revision = ?, updated_by_admin_id = ?, updated_at_ms = ?
|
||||
WHERE app_code = ? AND template_id = ? AND revision = ?`,
|
||||
item.Name, item.ActivityType, item.StartMS, item.EndMS, item.AllRegions,
|
||||
item.DailyLeaderboardSize, item.TotalLeaderboardSize, item.DisplayConfigJSON,
|
||||
item.Revision, item.UpdatedByAdminID, item.UpdatedAtMS,
|
||||
appCode, item.TemplateID, command.ExpectedRevision)
|
||||
if err != nil {
|
||||
return domain.Template{}, activityTemplateWriteError(err)
|
||||
}
|
||||
affected, _ := result.RowsAffected()
|
||||
if affected != 1 {
|
||||
return domain.Template{}, xerr.New(xerr.Conflict, "activity template revision changed")
|
||||
}
|
||||
if err := replaceActivityTemplateChildren(ctx, tx, item, nowMS); err != nil {
|
||||
return domain.Template{}, activityTemplateWriteError(err)
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return domain.Template{}, activityTemplateWriteError(err)
|
||||
}
|
||||
return r.GetActivityTemplate(ctx, item.TemplateID)
|
||||
}
|
||||
|
||||
func (r *Repository) SetActivityTemplateStatus(ctx context.Context, command domain.StatusCommand, nowMS int64) (domain.Template, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return domain.Template{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
}
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return domain.Template{}, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
appCode := appcode.FromContext(ctx)
|
||||
if command.Status == domain.StatusPublished {
|
||||
// 同一 App 的发布事务先争用一条稳定互斥行,再读取当前草稿与已发布区间。
|
||||
// 否则两个互不相同的草稿可以在各自事务快照中都看不到对方,造成区域排期写偏差。
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO activity_template_publish_locks (app_code, updated_at_ms)
|
||||
VALUES (?, ?)
|
||||
ON DUPLICATE KEY UPDATE updated_at_ms = ?`, appCode, nowMS, nowMS); err != nil {
|
||||
return domain.Template{}, err
|
||||
}
|
||||
}
|
||||
current, err := getActivityTemplateAggregate(ctx, tx, appCode, command.TemplateID, true)
|
||||
if err != nil {
|
||||
return domain.Template{}, err
|
||||
}
|
||||
if current.Revision != command.ExpectedRevision {
|
||||
return domain.Template{}, xerr.New(xerr.Conflict, "activity template revision changed")
|
||||
}
|
||||
if current.Status == domain.StatusArchived {
|
||||
return domain.Template{}, xerr.New(xerr.Conflict, "archived activity template is read-only")
|
||||
}
|
||||
if command.Status == domain.StatusPublished {
|
||||
// 发布冲突检查与状态写入必须在同一个事务内。all_regions 与任何区域活动冲突;
|
||||
// 定向活动只在区域集合有交集且 UTC [start_ms,end_ms) 相交时冲突。
|
||||
if err := ensureNoPublishedActivityTemplateOverlap(ctx, tx, current); err != nil {
|
||||
return domain.Template{}, err
|
||||
}
|
||||
}
|
||||
nextRevision := current.Revision + 1
|
||||
nextPublishedVersion := current.PublishedVersion
|
||||
nextPublishedBy := current.PublishedByAdminID
|
||||
nextPublishedAt := current.PublishedAtMS
|
||||
nextArchivedAt := current.ArchivedAtMS
|
||||
if command.Status == domain.StatusPublished {
|
||||
nextPublishedVersion++
|
||||
nextPublishedBy = command.OperatorAdminID
|
||||
nextPublishedAt = nowMS
|
||||
}
|
||||
if command.Status == domain.StatusArchived {
|
||||
nextArchivedAt = nowMS
|
||||
}
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
UPDATE activity_templates
|
||||
SET status = ?, revision = ?, published_version = ?, updated_by_admin_id = ?, updated_at_ms = ?,
|
||||
published_by_admin_id = ?, published_at_ms = ?, archived_at_ms = ?
|
||||
WHERE app_code = ? AND template_id = ? AND revision = ?`,
|
||||
command.Status, nextRevision, nextPublishedVersion, command.OperatorAdminID, nowMS,
|
||||
nextPublishedBy, nextPublishedAt, nextArchivedAt,
|
||||
appCode, current.TemplateID, command.ExpectedRevision)
|
||||
if err != nil {
|
||||
return domain.Template{}, activityTemplateWriteError(err)
|
||||
}
|
||||
affected, _ := result.RowsAffected()
|
||||
if affected != 1 {
|
||||
return domain.Template{}, xerr.New(xerr.Conflict, "activity template revision changed")
|
||||
}
|
||||
if command.Status == domain.StatusPublished {
|
||||
published, err := getActivityTemplateAggregate(ctx, tx, appCode, current.TemplateID, false)
|
||||
if err != nil {
|
||||
return domain.Template{}, err
|
||||
}
|
||||
// wallet-owner snapshot IDs are produced immediately before this transaction and keyed by the next
|
||||
// published version. Only these trusted refs are embedded; mutable edit tables continue storing group IDs.
|
||||
for index := range published.Tasks {
|
||||
if published.Tasks[index].RewardResourceGroupID <= 0 {
|
||||
continue
|
||||
}
|
||||
snapshot, ok := command.RewardSnapshots[published.Tasks[index].RewardResourceGroupID]
|
||||
if !ok || snapshot.SnapshotID == "" {
|
||||
return domain.Template{}, xerr.New(xerr.Conflict, "activity task reward snapshot is missing")
|
||||
}
|
||||
published.Tasks[index].RewardSnapshot = snapshot
|
||||
}
|
||||
for index := range published.RankRewards {
|
||||
snapshot, ok := command.RewardSnapshots[published.RankRewards[index].ResourceGroupID]
|
||||
if !ok || snapshot.SnapshotID == "" {
|
||||
return domain.Template{}, xerr.New(xerr.Conflict, "activity rank reward snapshot is missing")
|
||||
}
|
||||
published.RankRewards[index].RewardSnapshot = snapshot
|
||||
}
|
||||
published.LifecycleStatus = ""
|
||||
snapshotJSON, err := json.Marshal(published)
|
||||
if err != nil {
|
||||
return domain.Template{}, err
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO activity_template_versions (
|
||||
app_code, template_id, version_no, snapshot_json, published_by_admin_id, published_at_ms
|
||||
) VALUES (?, ?, ?, CAST(? AS JSON), ?, ?)`,
|
||||
appCode, current.TemplateID, nextPublishedVersion, string(snapshotJSON), command.OperatorAdminID, nowMS); err != nil {
|
||||
return domain.Template{}, activityTemplateWriteError(err)
|
||||
}
|
||||
// 运行侧只绑定刚写入的不可变快照;这里和状态/版本在同一事务建启用窗口及 UTC 结算周期,
|
||||
// 避免发布已成功但 MQ 消费或 cron 永远找不到运行版本的半发布状态。
|
||||
if err := insertActivityTemplateRuntimeVersion(ctx, tx, published, nextPublishedVersion, nowMS, r.activityTemplateSettlementLatenessMS()); err != nil {
|
||||
return domain.Template{}, activityTemplateWriteError(err)
|
||||
}
|
||||
} else if command.Status == domain.StatusDisabled {
|
||||
// 停用只关闭当前发布版本的真实事件窗口,并把未来周期取消、进行中的周期截断为立即可结算。
|
||||
// 已冻结快照和历史发奖任务不删除,后续重试仍按原 published_version 审计。
|
||||
if err := closeActivityTemplateRuntimeVersion(ctx, tx, current.TemplateID, current.PublishedVersion, nowMS, r.activityTemplateSettlementLatenessMS()); err != nil {
|
||||
return domain.Template{}, activityTemplateWriteError(err)
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return domain.Template{}, activityTemplateWriteError(err)
|
||||
}
|
||||
return r.GetActivityTemplate(ctx, current.TemplateID)
|
||||
}
|
||||
|
||||
func (r *Repository) ListActivityTemplateVersions(ctx context.Context, templateID string, page int32, pageSize int32) ([]domain.Version, int64, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
}
|
||||
appCode := appcode.FromContext(ctx)
|
||||
if _, err := getActivityTemplateBase(ctx, r.db, appCode, templateID, false); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var total int64
|
||||
if err := r.db.QueryRowContext(ctx, `
|
||||
SELECT COUNT(*) FROM activity_template_versions WHERE app_code = ? AND template_id = ?`,
|
||||
appCode, templateID).Scan(&total); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
pageSize = normalizePageSize(pageSize)
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT v.version_no, CAST(v.snapshot_json AS CHAR), v.published_by_admin_id, v.published_at_ms,
|
||||
COALESCE(rv.runtime_from_ms, 0), COALESCE(rv.runtime_to_ms, 0)
|
||||
FROM activity_template_versions v
|
||||
LEFT JOIN activity_template_runtime_versions rv
|
||||
ON rv.app_code = v.app_code AND rv.template_id = v.template_id AND rv.version_no = v.version_no
|
||||
WHERE v.app_code = ? AND v.template_id = ?
|
||||
ORDER BY v.version_no DESC
|
||||
LIMIT ? OFFSET ?`, appCode, templateID, int(pageSize), int((page-1)*pageSize))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := make([]domain.Version, 0)
|
||||
for rows.Next() {
|
||||
var item domain.Version
|
||||
var snapshotJSON string
|
||||
item.TemplateID = templateID
|
||||
if err := rows.Scan(
|
||||
&item.VersionNo, &snapshotJSON, &item.PublishedByAdminID, &item.PublishedAtMS,
|
||||
&item.RuntimeFromMS, &item.RuntimeToMS,
|
||||
); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := json.Unmarshal([]byte(snapshotJSON), &item.Snapshot); err != nil {
|
||||
return nil, 0, fmt.Errorf("decode activity template version %d: %w", item.VersionNo, err)
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, total, rows.Err()
|
||||
}
|
||||
|
||||
func (r *Repository) GetActivityTemplateVersion(ctx context.Context, templateID string, versionNo int64) (domain.Version, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return domain.Version{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
}
|
||||
var item domain.Version
|
||||
var snapshotJSON string
|
||||
item.TemplateID = strings.TrimSpace(templateID)
|
||||
err := r.db.QueryRowContext(ctx, `
|
||||
SELECT v.version_no, CAST(v.snapshot_json AS CHAR), v.published_by_admin_id, v.published_at_ms,
|
||||
COALESCE(rv.runtime_from_ms, 0), COALESCE(rv.runtime_to_ms, 0)
|
||||
FROM activity_template_versions v
|
||||
LEFT JOIN activity_template_runtime_versions rv
|
||||
ON rv.app_code = v.app_code AND rv.template_id = v.template_id AND rv.version_no = v.version_no
|
||||
WHERE v.app_code = ? AND v.template_id = ? AND v.version_no = ?`,
|
||||
appcode.FromContext(ctx), item.TemplateID, versionNo).Scan(
|
||||
&item.VersionNo, &snapshotJSON, &item.PublishedByAdminID, &item.PublishedAtMS,
|
||||
&item.RuntimeFromMS, &item.RuntimeToMS)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return domain.Version{}, xerr.New(xerr.NotFound, "activity template version not found")
|
||||
}
|
||||
if err != nil {
|
||||
return domain.Version{}, err
|
||||
}
|
||||
if err := json.Unmarshal([]byte(snapshotJSON), &item.Snapshot); err != nil {
|
||||
return domain.Version{}, fmt.Errorf("decode activity template version %d: %w", item.VersionNo, err)
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func activityTemplateListWhere(ctx context.Context, query domain.ListQuery) (string, []any) {
|
||||
parts := []string{"t.app_code = ?"}
|
||||
args := []any{appcode.FromContext(ctx)}
|
||||
if query.Status == "" {
|
||||
parts = append(parts, "t.status <> ?")
|
||||
args = append(args, domain.StatusArchived)
|
||||
} else {
|
||||
switch query.Status {
|
||||
case domain.LifecycleScheduled:
|
||||
parts = append(parts, "t.status = ? AND t.start_ms > ?")
|
||||
args = append(args, domain.StatusPublished, query.NowMS)
|
||||
case domain.LifecycleOngoing, "active":
|
||||
parts = append(parts, "t.status = ? AND t.start_ms <= ? AND t.end_ms > ?")
|
||||
args = append(args, domain.StatusPublished, query.NowMS, query.NowMS)
|
||||
case domain.LifecycleEnded:
|
||||
parts = append(parts, "t.status = ? AND t.end_ms <= ?")
|
||||
args = append(args, domain.StatusPublished, query.NowMS)
|
||||
default:
|
||||
parts = append(parts, "t.status = ?")
|
||||
args = append(args, query.Status)
|
||||
}
|
||||
}
|
||||
if query.Keyword != "" {
|
||||
parts = append(parts, "(t.template_id LIKE ? OR t.template_code LIKE ? OR t.name LIKE ?)")
|
||||
keyword := "%" + query.Keyword + "%"
|
||||
args = append(args, keyword, keyword, keyword)
|
||||
}
|
||||
if query.StartMS > 0 || query.EndMS > 0 {
|
||||
// 未排期草稿的 start_ms/end_ms 均为 0,不属于任何时间区间;尤其只有 end_ms 时不能把它们误算为相交。
|
||||
parts = append(parts, "t.start_ms > 0 AND t.end_ms > t.start_ms")
|
||||
}
|
||||
if query.StartMS > 0 {
|
||||
// 区间筛选使用相交语义,不要求活动开始时间落在查询区间内。
|
||||
parts = append(parts, "t.end_ms > ?")
|
||||
args = append(args, query.StartMS)
|
||||
}
|
||||
if query.EndMS > 0 {
|
||||
parts = append(parts, "t.start_ms < ?")
|
||||
args = append(args, query.EndMS)
|
||||
}
|
||||
if query.AllRegions != nil {
|
||||
parts = append(parts, "t.all_regions = ?")
|
||||
args = append(args, *query.AllRegions)
|
||||
}
|
||||
if query.RegionID != nil {
|
||||
if query.AllRegions != nil && !*query.AllRegions {
|
||||
parts = append(parts, "EXISTS (SELECT 1 FROM activity_template_regions tr WHERE tr.app_code = t.app_code AND tr.template_id = t.template_id AND tr.region_id = ?)")
|
||||
} else {
|
||||
// 未显式限定 all_regions 时,区域筛选返回所有对该区域生效的活动,包括全区活动。
|
||||
parts = append(parts, "(t.all_regions = TRUE OR EXISTS (SELECT 1 FROM activity_template_regions tr WHERE tr.app_code = t.app_code AND tr.template_id = t.template_id AND tr.region_id = ?))")
|
||||
}
|
||||
args = append(args, *query.RegionID)
|
||||
}
|
||||
return "WHERE " + strings.Join(parts, " AND "), args
|
||||
}
|
||||
|
||||
func getActivityTemplateAggregate(ctx context.Context, q activityTemplateQuerier, appCode string, templateID string, forUpdate bool) (domain.Template, error) {
|
||||
item, err := getActivityTemplateBase(ctx, q, appCode, templateID, forUpdate)
|
||||
if err != nil {
|
||||
return domain.Template{}, err
|
||||
}
|
||||
if err := attachActivityTemplateChildren(ctx, q, &item); err != nil {
|
||||
return domain.Template{}, err
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func getActivityTemplateBase(ctx context.Context, q activityTemplateQuerier, appCode string, templateID string, forUpdate bool) (domain.Template, error) {
|
||||
query := activityTemplateSelect + ` WHERE app_code = ? AND template_id = ?`
|
||||
if forUpdate {
|
||||
query += ` FOR UPDATE`
|
||||
}
|
||||
item, err := scanActivityTemplate(q.QueryRowContext(ctx, query, appCode, strings.TrimSpace(templateID)))
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return domain.Template{}, xerr.New(xerr.NotFound, "activity template not found")
|
||||
}
|
||||
return item, err
|
||||
}
|
||||
|
||||
func scanActivityTemplate(scanner activityTemplateScanner) (domain.Template, error) {
|
||||
var item domain.Template
|
||||
err := scanner.Scan(
|
||||
&item.AppCode, &item.TemplateID, &item.TemplateCode, &item.Name, &item.ActivityType, &item.Status,
|
||||
&item.StartMS, &item.EndMS, &item.AllRegions, &item.DailyLeaderboardSize, &item.TotalLeaderboardSize,
|
||||
&item.DisplayConfigJSON, &item.Revision, &item.PublishedVersion,
|
||||
&item.CreatedByAdminID, &item.UpdatedByAdminID, &item.PublishedByAdminID,
|
||||
&item.CreatedAtMS, &item.UpdatedAtMS, &item.PublishedAtMS, &item.ArchivedAtMS,
|
||||
)
|
||||
return item, err
|
||||
}
|
||||
|
||||
func attachActivityTemplateChildren(ctx context.Context, q activityTemplateQuerier, item *domain.Template) error {
|
||||
item.RegionIDs = []int64{}
|
||||
item.Locales = []domain.Locale{}
|
||||
item.Gifts = []domain.Gift{}
|
||||
item.Tasks = []domain.Task{}
|
||||
item.RankRewards = []domain.RankReward{}
|
||||
item.Assets = []domain.Asset{}
|
||||
rows, err := q.QueryContext(ctx, `SELECT region_id FROM activity_template_regions WHERE app_code = ? AND template_id = ? ORDER BY region_id`, item.AppCode, item.TemplateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for rows.Next() {
|
||||
var regionID int64
|
||||
if err := rows.Scan(®ionID); err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
item.RegionIDs = append(item.RegionIDs, regionID)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err = q.QueryContext(ctx, `SELECT locale, title, rules FROM activity_template_locales WHERE app_code = ? AND template_id = ? ORDER BY locale`, item.AppCode, item.TemplateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for rows.Next() {
|
||||
var locale domain.Locale
|
||||
if err := rows.Scan(&locale.Locale, &locale.Title, &locale.Rules); err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
item.Locales = append(item.Locales, locale)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err = q.QueryContext(ctx, `
|
||||
SELECT gift_id, sort_order, name, icon_url, coin_price, price_version
|
||||
FROM activity_template_gifts WHERE app_code = ? AND template_id = ? ORDER BY sort_order, gift_id`, item.AppCode, item.TemplateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for rows.Next() {
|
||||
var gift domain.Gift
|
||||
if err := rows.Scan(&gift.GiftID, &gift.SortOrder, &gift.Name, &gift.IconURL, &gift.CoinPrice, &gift.PriceVersion); err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
item.Gifts = append(item.Gifts, gift)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err = q.QueryContext(ctx, `
|
||||
SELECT task_key, task_type, target_value, reward_resource_group_id, sort_order
|
||||
FROM activity_template_tasks WHERE app_code = ? AND template_id = ? ORDER BY sort_order, task_key`, item.AppCode, item.TemplateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for rows.Next() {
|
||||
var task domain.Task
|
||||
if err := rows.Scan(&task.TaskKey, &task.TaskType, &task.TargetValue, &task.RewardResourceGroupID, &task.SortOrder); err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
item.Tasks = append(item.Tasks, task)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err = q.QueryContext(ctx, `
|
||||
SELECT board_type, rank_from, rank_to, resource_group_id, sort_order
|
||||
FROM activity_template_rank_rewards WHERE app_code = ? AND template_id = ? ORDER BY board_type, rank_from, rank_to`, item.AppCode, item.TemplateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for rows.Next() {
|
||||
var reward domain.RankReward
|
||||
if err := rows.Scan(&reward.BoardType, &reward.RankFrom, &reward.RankTo, &reward.ResourceGroupID, &reward.SortOrder); err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
item.RankRewards = append(item.RankRewards, reward)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err = q.QueryContext(ctx, `
|
||||
SELECT asset_key, locale, url, media_type, sort_order
|
||||
FROM activity_template_assets WHERE app_code = ? AND template_id = ? ORDER BY asset_key, locale, sort_order`, item.AppCode, item.TemplateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for rows.Next() {
|
||||
var asset domain.Asset
|
||||
if err := rows.Scan(&asset.AssetKey, &asset.Locale, &asset.URL, &asset.MediaType, &asset.SortOrder); err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
item.Assets = append(item.Assets, asset)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func replaceActivityTemplateChildren(ctx context.Context, tx *sql.Tx, item domain.Template, nowMS int64) error {
|
||||
for _, table := range []string{
|
||||
"activity_template_regions",
|
||||
"activity_template_locales",
|
||||
"activity_template_gifts",
|
||||
"activity_template_tasks",
|
||||
"activity_template_rank_rewards",
|
||||
"activity_template_assets",
|
||||
} {
|
||||
if _, err := tx.ExecContext(ctx, `DELETE FROM `+table+` WHERE app_code = ? AND template_id = ?`, item.AppCode, item.TemplateID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, regionID := range item.RegionIDs {
|
||||
if _, err := tx.ExecContext(ctx, `INSERT INTO activity_template_regions (app_code, template_id, region_id, created_at_ms) VALUES (?, ?, ?, ?)`, item.AppCode, item.TemplateID, regionID, nowMS); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, locale := range item.Locales {
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO activity_template_locales (app_code, template_id, locale, title, rules, created_at_ms, updated_at_ms)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`, item.AppCode, item.TemplateID, locale.Locale, locale.Title, locale.Rules, nowMS, nowMS); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, gift := range item.Gifts {
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO activity_template_gifts (app_code, template_id, gift_id, sort_order, name, icon_url, coin_price, price_version, created_at_ms, updated_at_ms)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
item.AppCode, item.TemplateID, gift.GiftID, gift.SortOrder, gift.Name, gift.IconURL, gift.CoinPrice, gift.PriceVersion, nowMS, nowMS); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, task := range item.Tasks {
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO activity_template_tasks (app_code, template_id, task_key, task_type, target_value, reward_resource_group_id, sort_order, created_at_ms, updated_at_ms)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
item.AppCode, item.TemplateID, task.TaskKey, task.TaskType, task.TargetValue, task.RewardResourceGroupID, task.SortOrder, nowMS, nowMS); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, reward := range item.RankRewards {
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO activity_template_rank_rewards (app_code, template_id, board_type, rank_from, rank_to, resource_group_id, sort_order, created_at_ms, updated_at_ms)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
item.AppCode, item.TemplateID, reward.BoardType, reward.RankFrom, reward.RankTo, reward.ResourceGroupID, reward.SortOrder, nowMS, nowMS); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, asset := range item.Assets {
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO activity_template_assets (app_code, template_id, asset_key, locale, url, media_type, sort_order, created_at_ms, updated_at_ms)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
item.AppCode, item.TemplateID, asset.AssetKey, asset.Locale, asset.URL, asset.MediaType, asset.SortOrder, nowMS, nowMS); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureNoPublishedActivityTemplateOverlap(ctx context.Context, tx *sql.Tx, item domain.Template) error {
|
||||
rows, err := tx.QueryContext(ctx, activityTemplateSelect+`
|
||||
WHERE app_code = ? AND template_id <> ? AND status = ? AND start_ms < ? AND end_ms > ?
|
||||
ORDER BY start_ms, template_id
|
||||
FOR UPDATE`, item.AppCode, item.TemplateID, domain.StatusPublished, item.EndMS, item.StartMS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
candidates := make([]domain.Template, 0)
|
||||
for rows.Next() {
|
||||
candidate, err := scanActivityTemplate(rows)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
candidates = append(candidates, candidate)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
for index := range candidates {
|
||||
if err := attachActivityTemplateRegions(ctx, tx, &candidates[index]); err != nil {
|
||||
return err
|
||||
}
|
||||
if activityTemplateScopesOverlap(item, candidates[index]) {
|
||||
return xerr.New(xerr.Conflict, "published activity template overlaps another activity in the same region scope")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func activityTemplateScopesOverlap(left domain.Template, right domain.Template) bool {
|
||||
if left.AllRegions || right.AllRegions {
|
||||
return true
|
||||
}
|
||||
seen := make(map[int64]struct{}, len(left.RegionIDs))
|
||||
for _, regionID := range left.RegionIDs {
|
||||
seen[regionID] = struct{}{}
|
||||
}
|
||||
for _, regionID := range right.RegionIDs {
|
||||
if _, exists := seen[regionID]; exists {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func attachActivityTemplateRegions(ctx context.Context, q activityTemplateQuerier, item *domain.Template) error {
|
||||
rows, err := q.QueryContext(ctx, `SELECT region_id FROM activity_template_regions WHERE app_code = ? AND template_id = ? ORDER BY region_id`, item.AppCode, item.TemplateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
item.RegionIDs = []int64{}
|
||||
for rows.Next() {
|
||||
var regionID int64
|
||||
if err := rows.Scan(®ionID); err != nil {
|
||||
return err
|
||||
}
|
||||
item.RegionIDs = append(item.RegionIDs, regionID)
|
||||
}
|
||||
return rows.Err()
|
||||
}
|
||||
|
||||
func attachActivityTemplateSummaryRegions(ctx context.Context, q activityTemplateQuerier, items []domain.Summary) error {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
placeholders := make([]string, 0, len(items))
|
||||
args := make([]any, 0, len(items)+1)
|
||||
args = append(args, items[0].AppCode)
|
||||
indexes := make(map[string]int, len(items))
|
||||
for index := range items {
|
||||
items[index].RegionIDs = []int64{}
|
||||
placeholders = append(placeholders, "?")
|
||||
args = append(args, items[index].TemplateID)
|
||||
indexes[items[index].TemplateID] = index
|
||||
}
|
||||
rows, err := q.QueryContext(ctx, `
|
||||
SELECT template_id, region_id
|
||||
FROM activity_template_regions
|
||||
WHERE app_code = ? AND template_id IN (`+strings.Join(placeholders, ",")+`)
|
||||
ORDER BY template_id, region_id`, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var templateID string
|
||||
var regionID int64
|
||||
if err := rows.Scan(&templateID, ®ionID); err != nil {
|
||||
return err
|
||||
}
|
||||
if index, exists := indexes[templateID]; exists {
|
||||
items[index].RegionIDs = append(items[index].RegionIDs, regionID)
|
||||
}
|
||||
}
|
||||
return rows.Err()
|
||||
}
|
||||
|
||||
func activityTemplateWriteError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
var mysqlErr *mysqlDriver.MySQLError
|
||||
if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 {
|
||||
return xerr.New(xerr.Conflict, "activity template code or child key already exists")
|
||||
}
|
||||
return err
|
||||
}
|
||||
@ -0,0 +1,480 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hyapp/internal/testutil/mysqlschema"
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/xerr"
|
||||
domain "hyapp/services/activity-service/internal/domain/activitytemplate"
|
||||
)
|
||||
|
||||
func TestActivityTemplateListKeywordSearchesIDCodeAndName(t *testing.T) {
|
||||
ctx := appcode.WithContext(context.Background(), "hyapp")
|
||||
where, args := activityTemplateListWhere(ctx, domain.ListQuery{Keyword: "acttpl_123"})
|
||||
for _, field := range []string{"t.template_id LIKE ?", "t.template_code LIKE ?", "t.name LIKE ?"} {
|
||||
if !strings.Contains(where, field) {
|
||||
t.Fatalf("keyword where clause missing %s: %s", field, where)
|
||||
}
|
||||
}
|
||||
if len(args) != 5 {
|
||||
t.Fatalf("keyword args = %v, want app_code, default archived exclusion and three LIKE args", args)
|
||||
}
|
||||
for index := 2; index < len(args); index++ {
|
||||
if args[index] != "%acttpl_123%" {
|
||||
t.Fatalf("keyword arg %d = %v", index, args[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareActivityTemplateRankSettlementSeparatesMaterializationFromExpiredJobRecovery(t *testing.T) {
|
||||
schema := mysqlschema.New(t, mysqlschema.Config{
|
||||
EnvVar: "ACTIVITY_SERVICE_MYSQL_TEST_DSN",
|
||||
InitDBPath: mysqlschema.InitDBPath(t, mysqlschema.CallerFile(t, 1), "..", "..", "..", "deploy", "mysql", "initdb", "001_activity_service.sql"),
|
||||
DatabasePrefix: "hyapp_activity_reward_recovery_test",
|
||||
})
|
||||
repository, err := Open(context.Background(), schema.DSN)
|
||||
if err != nil {
|
||||
t.Fatalf("open repository: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = repository.Close() })
|
||||
ctx := appcode.WithContext(context.Background(), "hyapp")
|
||||
|
||||
item := createRepositoryTemplate(t, ctx, repository, "reward_crash_recovery", false, []int64{1}, 1_000, 2_000)
|
||||
item, err = repository.SetActivityTemplateStatus(ctx, domain.StatusCommand{
|
||||
TemplateID: item.TemplateID, Status: domain.StatusPublished, ExpectedRevision: item.Revision, OperatorAdminID: 90001,
|
||||
}, 100)
|
||||
if err != nil {
|
||||
t.Fatalf("publish template: %v", err)
|
||||
}
|
||||
period := domain.RankPeriod{
|
||||
AppCode: "hyapp", TemplateID: item.TemplateID, TemplateCode: item.TemplateCode, VersionNo: item.PublishedVersion,
|
||||
BoardType: domain.BoardTypeDaily, PeriodKey: time.UnixMilli(1_000).UTC().Format("2006-01-02"),
|
||||
}
|
||||
if _, err := repository.db.ExecContext(ctx, `
|
||||
INSERT INTO activity_template_rank_snapshots (
|
||||
app_code, template_id, template_code, version_no, board_type, period_key, rank_no,
|
||||
user_id, score, gift_count, coin_amount, first_scored_at_ms, snapshot_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, 1, 70001, 500, 5, 500, 1100, 2100)`,
|
||||
period.AppCode, period.TemplateID, period.TemplateCode, period.VersionNo, period.BoardType, period.PeriodKey); err != nil {
|
||||
t.Fatalf("insert frozen rank: %v", err)
|
||||
}
|
||||
if _, err := repository.db.ExecContext(ctx, `
|
||||
INSERT INTO activity_template_reward_jobs (
|
||||
app_code, reward_job_id, template_id, template_code, version_no, board_type, period_key,
|
||||
rank_no, user_id, score, resource_group_id, wallet_command_id, status, attempt_count,
|
||||
created_at_ms, updated_at_ms
|
||||
) VALUES (?, 'rewardjob_crashed', ?, ?, ?, ?, ?, 1, 70001, 500, 99,
|
||||
'activity-template-rank:crashed', ?, 1, 2100, 2100)`,
|
||||
period.AppCode, period.TemplateID, period.TemplateCode, period.VersionNo, period.BoardType, period.PeriodKey,
|
||||
domain.ClaimStatusRunning); err != nil {
|
||||
t.Fatalf("insert crashed running job: %v", err)
|
||||
}
|
||||
periods, err := repository.ClaimDueActivityTemplateRankPeriods(ctx, "materializer", 200_000, time.Minute, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("ClaimDueActivityTemplateRankPeriods() error = %v", err)
|
||||
}
|
||||
claimed := false
|
||||
for _, candidate := range periods {
|
||||
if candidate.TemplateID == period.TemplateID && candidate.BoardType == period.BoardType && candidate.PeriodKey == period.PeriodKey {
|
||||
claimed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !claimed {
|
||||
t.Fatalf("daily period was not claimed for materialization: %+v", periods)
|
||||
}
|
||||
|
||||
jobs, err := repository.PrepareActivityTemplateRankSettlement(ctx, period, 200_001)
|
||||
if err != nil {
|
||||
t.Fatalf("PrepareActivityTemplateRankSettlement() error = %v", err)
|
||||
}
|
||||
if len(jobs) != 0 {
|
||||
t.Fatalf("PrepareActivityTemplateRankSettlement() jobs = %+v, want materialization only", jobs)
|
||||
}
|
||||
var periodStatus string
|
||||
if err := repository.db.QueryRowContext(ctx, `
|
||||
SELECT status FROM activity_template_rank_periods
|
||||
WHERE app_code = ? AND template_id = ? AND version_no = ? AND board_type = ? AND period_key = ?`,
|
||||
period.AppCode, period.TemplateID, period.VersionNo, period.BoardType, period.PeriodKey).Scan(&periodStatus); err != nil {
|
||||
t.Fatalf("read materialized period status: %v", err)
|
||||
}
|
||||
if periodStatus != domain.PeriodStatusRewarding {
|
||||
t.Fatalf("materialized period status = %q, want rewarding", periodStatus)
|
||||
}
|
||||
jobs, err = repository.ClaimActivityTemplateRewardJobs(ctx, "recovery-worker", 200_002, time.Minute, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("ClaimActivityTemplateRewardJobs() error = %v", err)
|
||||
}
|
||||
if len(jobs) != 1 || jobs[0].RewardJobID != "rewardjob_crashed" || jobs[0].Status != domain.ClaimStatusRunning || jobs[0].AttemptCount != 2 {
|
||||
t.Fatalf("expired job recovery = %+v", jobs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityTemplateSettlementDiscoverySkipsFiftyBlockedPeriodsBeforePending(t *testing.T) {
|
||||
schema := mysqlschema.New(t, mysqlschema.Config{
|
||||
EnvVar: "ACTIVITY_SERVICE_MYSQL_TEST_DSN",
|
||||
InitDBPath: mysqlschema.InitDBPath(t, mysqlschema.CallerFile(t, 1), "..", "..", "..", "deploy", "mysql", "initdb", "001_activity_service.sql"),
|
||||
DatabasePrefix: "hyapp_activity_period_starvation_test",
|
||||
})
|
||||
repository, err := Open(context.Background(), schema.DSN)
|
||||
if err != nil {
|
||||
t.Fatalf("open repository: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = repository.Close() })
|
||||
ctx := appcode.WithContext(context.Background(), "hyapp")
|
||||
const nowMS = int64(100_000)
|
||||
|
||||
for index := 0; index < 50; index++ {
|
||||
templateID := "blocked-" + fmt.Sprintf("%02d", index)
|
||||
if _, err := repository.db.ExecContext(ctx, `
|
||||
INSERT INTO activity_template_rank_periods (
|
||||
app_code, template_id, template_code, version_no, board_type, period_key,
|
||||
period_start_ms, period_end_ms, settle_after_ms, leaderboard_size, status,
|
||||
locked_by, lock_until_ms, settled_at_ms, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, 1, 'total', 'total', 1, 2, ?, 100, ?, '', 0, 0, 1, 1)`,
|
||||
"hyapp", templateID, templateID, int64(index+1), domain.PeriodStatusSettling); err != nil {
|
||||
t.Fatalf("insert blocked period %d: %v", index, err)
|
||||
}
|
||||
status := domain.ClaimStatusFailed
|
||||
nextRetryAtMS := nowMS + int64(time.Hour/time.Millisecond)
|
||||
if index%2 == 0 {
|
||||
status = domain.ClaimStatusDead
|
||||
nextRetryAtMS = 0
|
||||
}
|
||||
if _, err := repository.db.ExecContext(ctx, `
|
||||
INSERT INTO activity_template_reward_jobs (
|
||||
app_code, reward_job_id, template_id, template_code, version_no, board_type, period_key,
|
||||
rank_no, user_id, score, resource_group_id, wallet_command_id, status, attempt_count,
|
||||
max_attempts, next_retry_at_ms, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, 1, 'total', 'total', 1, ?, 1, 1, ?, ?, 1, 8, ?, 1, 1)`,
|
||||
"hyapp", "job-"+templateID, templateID, templateID, int64(index+1), "wallet-"+templateID,
|
||||
status, nextRetryAtMS); err != nil {
|
||||
t.Fatalf("insert blocked job %d: %v", index, err)
|
||||
}
|
||||
}
|
||||
if _, err := repository.db.ExecContext(ctx, `
|
||||
INSERT INTO activity_template_rank_periods (
|
||||
app_code, template_id, template_code, version_no, board_type, period_key,
|
||||
period_start_ms, period_end_ms, settle_after_ms, leaderboard_size, status,
|
||||
locked_by, lock_until_ms, settled_at_ms, created_at_ms, updated_at_ms
|
||||
) VALUES ('hyapp', 'pending-after-blocked', 'pending-after-blocked', 1, 'total', 'total',
|
||||
1, 2, 1000, 100, ?, '', 0, 0, 1, 1)`, domain.PeriodStatusPending); err != nil {
|
||||
t.Fatalf("insert pending period: %v", err)
|
||||
}
|
||||
|
||||
periods, err := repository.ClaimDueActivityTemplateRankPeriods(ctx, "settler", nowMS, time.Minute, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("ClaimDueActivityTemplateRankPeriods() error = %v", err)
|
||||
}
|
||||
if len(periods) != 1 || periods[0].TemplateID != "pending-after-blocked" {
|
||||
t.Fatalf("discovered periods = %+v, want only pending-after-blocked", periods)
|
||||
}
|
||||
if len(periods) == 50 {
|
||||
t.Fatal("blocked settling periods must not keep settlement has_more true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityTemplateListTimeFilterExcludesUnscheduledDrafts(t *testing.T) {
|
||||
ctx := appcode.WithContext(context.Background(), "hyapp")
|
||||
where, _ := activityTemplateListWhere(ctx, domain.ListQuery{EndMS: 2_000})
|
||||
if !strings.Contains(where, "t.start_ms > 0 AND t.end_ms > t.start_ms") || !strings.Contains(where, "t.start_ms < ?") {
|
||||
t.Fatalf("end-only time filter does not require a valid scheduled period: %s", where)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityTemplateScopesOverlap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
left domain.Template
|
||||
right domain.Template
|
||||
want bool
|
||||
}{
|
||||
{name: "left global", left: domain.Template{AllRegions: true}, right: domain.Template{RegionIDs: []int64{1}}, want: true},
|
||||
{name: "right global", left: domain.Template{RegionIDs: []int64{1}}, right: domain.Template{AllRegions: true}, want: true},
|
||||
{name: "shared region", left: domain.Template{RegionIDs: []int64{1, 2}}, right: domain.Template{RegionIDs: []int64{2, 3}}, want: true},
|
||||
{name: "disjoint regions", left: domain.Template{RegionIDs: []int64{1}}, right: domain.Template{RegionIDs: []int64{2}}, want: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := activityTemplateScopesOverlap(tt.left, tt.right); got != tt.want {
|
||||
t.Fatalf("activityTemplateScopesOverlap() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityTemplateRankPeriodWatermarkIsExclusive(t *testing.T) {
|
||||
const settleAfterMS = int64(122_000)
|
||||
if accepted, reason := activityTemplateRankPeriodAcceptsEvent(domain.PeriodStatusPending, settleAfterMS, settleAfterMS-1); !accepted || reason != "" {
|
||||
t.Fatalf("event before watermark = (%v, %q), want accepted", accepted, reason)
|
||||
}
|
||||
if accepted, reason := activityTemplateRankPeriodAcceptsEvent(domain.PeriodStatusPending, settleAfterMS, settleAfterMS); accepted || reason != "settlement_window_closed" {
|
||||
t.Fatalf("event at watermark = (%v, %q), want settlement_window_closed", accepted, reason)
|
||||
}
|
||||
if accepted, reason := activityTemplateRankPeriodAcceptsEvent(domain.PeriodStatusSettling, settleAfterMS, settleAfterMS-1); accepted || reason != "rank_period_closed" {
|
||||
t.Fatalf("event for settling period = (%v, %q), want rank_period_closed", accepted, reason)
|
||||
}
|
||||
if accepted, reason := activityTemplateRankPeriodAcceptsEvent(domain.PeriodStatusRewarding, settleAfterMS, settleAfterMS-1); accepted || reason != "rank_period_closed" {
|
||||
t.Fatalf("event for rewarding period = (%v, %q), want rank_period_closed", accepted, reason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityTemplateLateGiftIsAcceptedBeforeWatermarkAndRejectedAtSettlement(t *testing.T) {
|
||||
schema := mysqlschema.New(t, mysqlschema.Config{
|
||||
EnvVar: "ACTIVITY_SERVICE_MYSQL_TEST_DSN",
|
||||
InitDBPath: mysqlschema.InitDBPath(t, mysqlschema.CallerFile(t, 1), "..", "..", "..", "deploy", "mysql", "initdb", "001_activity_service.sql"),
|
||||
DatabasePrefix: "hyapp_activity_late_gift_test",
|
||||
})
|
||||
repository, err := Open(context.Background(), schema.DSN)
|
||||
if err != nil {
|
||||
t.Fatalf("open repository: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = repository.Close() })
|
||||
repository.SetActivityTemplateSettlementLateness(2 * time.Minute)
|
||||
ctx := appcode.WithContext(context.Background(), "hyapp")
|
||||
|
||||
item := createRepositoryTemplate(t, ctx, repository, "late_gift_watermark", false, []int64{1}, 1_000, 2_000)
|
||||
item, err = repository.SetActivityTemplateStatus(ctx, domain.StatusCommand{
|
||||
TemplateID: item.TemplateID, Status: domain.StatusPublished, ExpectedRevision: item.Revision, OperatorAdminID: 90001,
|
||||
}, 100)
|
||||
if err != nil {
|
||||
t.Fatalf("publish template: %v", err)
|
||||
}
|
||||
periodKey := time.UnixMilli(1_999).UTC().Format("2006-01-02")
|
||||
var settleAfterMS int64
|
||||
if err := repository.db.QueryRowContext(ctx, `
|
||||
SELECT settle_after_ms FROM activity_template_rank_periods
|
||||
WHERE app_code = ? AND template_id = ? AND version_no = ? AND board_type = ? AND period_key = ?`,
|
||||
"hyapp", item.TemplateID, item.PublishedVersion, domain.BoardTypeDaily, periodKey).Scan(&settleAfterMS); err != nil {
|
||||
t.Fatalf("read settlement watermark: %v", err)
|
||||
}
|
||||
if settleAfterMS != 2_000+(2*time.Minute).Milliseconds() {
|
||||
t.Fatalf("settle_after_ms = %d", settleAfterMS)
|
||||
}
|
||||
periods, err := repository.ClaimDueActivityTemplateRankPeriods(ctx, "worker-before", settleAfterMS-1, time.Minute, 10)
|
||||
if err != nil || len(periods) != 0 {
|
||||
t.Fatalf("claim before watermark = %+v, err=%v", periods, err)
|
||||
}
|
||||
|
||||
accepted, err := repository.ConsumeActivityTemplateGiftEvent(ctx, domain.GiftEvent{
|
||||
EventID: "event-before-watermark", UserID: 70001, GiftID: "gift_rose", GiftCount: 1,
|
||||
CoinAmount: 100, RegionID: 1, OccurredAtMS: 1_999,
|
||||
}, settleAfterMS-1)
|
||||
if err != nil || accepted.Status != domain.EventStatusConsumed {
|
||||
t.Fatalf("consume before watermark = %+v, err=%v", accepted, err)
|
||||
}
|
||||
periods, err = repository.ClaimDueActivityTemplateRankPeriods(ctx, "worker-at", settleAfterMS, time.Minute, 10)
|
||||
if err != nil || len(periods) != 2 {
|
||||
t.Fatalf("claim at watermark = %+v, err=%v", periods, err)
|
||||
}
|
||||
late, err := repository.ConsumeActivityTemplateGiftEvent(ctx, domain.GiftEvent{
|
||||
EventID: "event-at-watermark", UserID: 70001, GiftID: "gift_rose", GiftCount: 1,
|
||||
CoinAmount: 100, RegionID: 1, OccurredAtMS: 1_999,
|
||||
}, settleAfterMS)
|
||||
if err != nil || late.Status != domain.EventStatusSkipped || late.SkipReason != "daily:settlement_window_closed;total:settlement_window_closed" {
|
||||
t.Fatalf("consume at watermark = %+v, err=%v", late, err)
|
||||
}
|
||||
var score int64
|
||||
if err := repository.db.QueryRowContext(ctx, `
|
||||
SELECT score FROM activity_template_user_total_scores
|
||||
WHERE app_code = ? AND template_id = ? AND version_no = ? AND user_id = ?`,
|
||||
"hyapp", item.TemplateID, item.PublishedVersion, 70001).Scan(&score); err != nil {
|
||||
t.Fatalf("read total score: %v", err)
|
||||
}
|
||||
if score != 100 {
|
||||
t.Fatalf("total score = %d, want only the accepted event", score)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityTemplateLateGiftStillUpdatesOpenTotalAfterDailySettlement(t *testing.T) {
|
||||
schema := mysqlschema.New(t, mysqlschema.Config{
|
||||
EnvVar: "ACTIVITY_SERVICE_MYSQL_TEST_DSN",
|
||||
InitDBPath: mysqlschema.InitDBPath(t, mysqlschema.CallerFile(t, 1), "..", "..", "..", "deploy", "mysql", "initdb", "001_activity_service.sql"),
|
||||
DatabasePrefix: "hyapp_activity_split_watermark_test",
|
||||
})
|
||||
repository, err := Open(context.Background(), schema.DSN)
|
||||
if err != nil {
|
||||
t.Fatalf("open repository: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = repository.Close() })
|
||||
repository.SetActivityTemplateSettlementLateness(2 * time.Minute)
|
||||
ctx := appcode.WithContext(context.Background(), "hyapp")
|
||||
dayStart := time.Date(2026, 7, 14, 0, 0, 0, 0, time.UTC).UnixMilli()
|
||||
dayEnd := dayStart + int64(24*time.Hour/time.Millisecond)
|
||||
item := createRepositoryTemplate(t, ctx, repository, "split_daily_total_watermark", false, []int64{1}, dayStart, dayEnd+int64(24*time.Hour/time.Millisecond))
|
||||
item, err = repository.SetActivityTemplateStatus(ctx, domain.StatusCommand{
|
||||
TemplateID: item.TemplateID, Status: domain.StatusPublished, ExpectedRevision: item.Revision, OperatorAdminID: 90001,
|
||||
}, dayStart-1_000)
|
||||
if err != nil {
|
||||
t.Fatalf("publish template: %v", err)
|
||||
}
|
||||
processingMS := dayEnd + (2 * time.Minute).Milliseconds()
|
||||
periods, err := repository.ClaimDueActivityTemplateRankPeriods(ctx, "daily-settler", processingMS, time.Minute, 10)
|
||||
if err != nil || len(periods) != 1 || periods[0].BoardType != domain.BoardTypeDaily {
|
||||
t.Fatalf("claim first daily period = %+v, err=%v", periods, err)
|
||||
}
|
||||
result, err := repository.ConsumeActivityTemplateGiftEvent(ctx, domain.GiftEvent{
|
||||
EventID: "late-daily-open-total", UserID: 70002, GiftID: "gift_rose", GiftCount: 2,
|
||||
CoinAmount: 250, RegionID: 1, OccurredAtMS: dayEnd - 1,
|
||||
}, processingMS)
|
||||
if err != nil || result.Status != domain.EventStatusConsumed || result.SkipReason != "daily:settlement_window_closed" {
|
||||
t.Fatalf("consume split-watermark gift = %+v, err=%v", result, err)
|
||||
}
|
||||
var totalScore int64
|
||||
if err := repository.db.QueryRowContext(ctx, `
|
||||
SELECT score FROM activity_template_user_total_scores
|
||||
WHERE app_code = ? AND template_id = ? AND version_no = ? AND user_id = ?`,
|
||||
"hyapp", item.TemplateID, item.PublishedVersion, 70002).Scan(&totalScore); err != nil {
|
||||
t.Fatalf("read total score: %v", err)
|
||||
}
|
||||
if totalScore != 250 {
|
||||
t.Fatalf("total score = %d, want 250", totalScore)
|
||||
}
|
||||
var dailyRows int
|
||||
if err := repository.db.QueryRowContext(ctx, `
|
||||
SELECT COUNT(*) FROM activity_template_user_day_scores
|
||||
WHERE app_code = ? AND template_id = ? AND version_no = ? AND user_id = ?`,
|
||||
"hyapp", item.TemplateID, item.PublishedVersion, 70002).Scan(&dailyRows); err != nil || dailyRows != 0 {
|
||||
t.Fatalf("closed daily score rows = %d, err=%v", dailyRows, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityTemplateRepositoryPublishSerializesScopeOverlapAndStoresVersion(t *testing.T) {
|
||||
schema := mysqlschema.New(t, mysqlschema.Config{
|
||||
EnvVar: "ACTIVITY_SERVICE_MYSQL_TEST_DSN",
|
||||
InitDBPath: mysqlschema.InitDBPath(t, mysqlschema.CallerFile(t, 1), "..", "..", "..", "deploy", "mysql", "initdb", "001_activity_service.sql"),
|
||||
DatabasePrefix: "hyapp_activity_template_test",
|
||||
})
|
||||
repository, err := Open(context.Background(), schema.DSN)
|
||||
if err != nil {
|
||||
t.Fatalf("open repository: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = repository.Close() })
|
||||
ctx := appcode.WithContext(context.Background(), "hyapp")
|
||||
|
||||
first := createRepositoryTemplate(t, ctx, repository, "scope_first", false, []int64{1}, 1_000, 2_000)
|
||||
first, err = repository.SetActivityTemplateStatus(ctx, domain.StatusCommand{
|
||||
TemplateID: first.TemplateID, Status: domain.StatusPublished, ExpectedRevision: first.Revision, OperatorAdminID: 90001,
|
||||
}, 100)
|
||||
if err != nil {
|
||||
t.Fatalf("publish first template: %v", err)
|
||||
}
|
||||
if first.PublishedVersion != 1 || first.Revision != 2 {
|
||||
t.Fatalf("first published metadata = %+v", first)
|
||||
}
|
||||
version, err := repository.GetActivityTemplateVersion(ctx, first.TemplateID, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("get first version: %v", err)
|
||||
}
|
||||
if version.Snapshot.Status != domain.StatusPublished || version.Snapshot.TemplateCode != first.TemplateCode || version.Snapshot.Revision != first.Revision {
|
||||
t.Fatalf("published snapshot = %+v", version.Snapshot)
|
||||
}
|
||||
|
||||
overlap := createRepositoryTemplate(t, ctx, repository, "scope_overlap", false, []int64{1}, 1_500, 2_500)
|
||||
_, err = repository.SetActivityTemplateStatus(ctx, domain.StatusCommand{
|
||||
TemplateID: overlap.TemplateID, Status: domain.StatusPublished, ExpectedRevision: overlap.Revision, OperatorAdminID: 90001,
|
||||
}, 110)
|
||||
if !xerr.IsCode(err, xerr.Conflict) {
|
||||
t.Fatalf("overlapping same-region publish error = %v, want conflict", err)
|
||||
}
|
||||
|
||||
disjoint := createRepositoryTemplate(t, ctx, repository, "scope_disjoint", false, []int64{2}, 1_500, 2_500)
|
||||
if _, err := repository.SetActivityTemplateStatus(ctx, domain.StatusCommand{
|
||||
TemplateID: disjoint.TemplateID, Status: domain.StatusPublished, ExpectedRevision: disjoint.Revision, OperatorAdminID: 90001,
|
||||
}, 120); err != nil {
|
||||
t.Fatalf("publish disjoint-region template: %v", err)
|
||||
}
|
||||
|
||||
global := createRepositoryTemplate(t, ctx, repository, "scope_global", true, nil, 1_700, 1_800)
|
||||
_, err = repository.SetActivityTemplateStatus(ctx, domain.StatusCommand{
|
||||
TemplateID: global.TemplateID, Status: domain.StatusPublished, ExpectedRevision: global.Revision, OperatorAdminID: 90001,
|
||||
}, 130)
|
||||
if !xerr.IsCode(err, xerr.Conflict) {
|
||||
t.Fatalf("global overlapping publish error = %v, want conflict", err)
|
||||
}
|
||||
|
||||
// [start_ms,end_ms) 允许一个活动在另一个活动结束的同一毫秒开始。
|
||||
adjacent := createRepositoryTemplate(t, ctx, repository, "scope_adjacent", false, []int64{1}, 2_000, 3_000)
|
||||
if _, err := repository.SetActivityTemplateStatus(ctx, domain.StatusCommand{
|
||||
TemplateID: adjacent.TemplateID, Status: domain.StatusPublished, ExpectedRevision: adjacent.Revision, OperatorAdminID: 90001,
|
||||
}, 140); err != nil {
|
||||
t.Fatalf("publish adjacent template: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityTemplateVersionQueryJoinsDisableAndRepublishRuntimeWindows(t *testing.T) {
|
||||
schema := mysqlschema.New(t, mysqlschema.Config{
|
||||
EnvVar: "ACTIVITY_SERVICE_MYSQL_TEST_DSN",
|
||||
InitDBPath: mysqlschema.InitDBPath(t, mysqlschema.CallerFile(t, 1), "..", "..", "..", "deploy", "mysql", "initdb", "001_activity_service.sql"),
|
||||
DatabasePrefix: "hyapp_activity_version_runtime_test",
|
||||
})
|
||||
repository, err := Open(context.Background(), schema.DSN)
|
||||
if err != nil {
|
||||
t.Fatalf("open repository: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = repository.Close() })
|
||||
ctx := appcode.WithContext(context.Background(), "hyapp")
|
||||
|
||||
item := createRepositoryTemplate(t, ctx, repository, "version_runtime_window", false, []int64{1}, 100, 2_000)
|
||||
item, err = repository.SetActivityTemplateStatus(ctx, domain.StatusCommand{
|
||||
TemplateID: item.TemplateID, Status: domain.StatusPublished, ExpectedRevision: item.Revision, OperatorAdminID: 90001,
|
||||
}, 100)
|
||||
if err != nil {
|
||||
t.Fatalf("publish v1: %v", err)
|
||||
}
|
||||
item, err = repository.SetActivityTemplateStatus(ctx, domain.StatusCommand{
|
||||
TemplateID: item.TemplateID, Status: domain.StatusDisabled, ExpectedRevision: item.Revision, OperatorAdminID: 90001,
|
||||
}, 500)
|
||||
if err != nil {
|
||||
t.Fatalf("disable v1: %v", err)
|
||||
}
|
||||
item, err = repository.SetActivityTemplateStatus(ctx, domain.StatusCommand{
|
||||
TemplateID: item.TemplateID, Status: domain.StatusPublished, ExpectedRevision: item.Revision, OperatorAdminID: 90002,
|
||||
}, 600)
|
||||
if err != nil {
|
||||
t.Fatalf("republish v2: %v", err)
|
||||
}
|
||||
|
||||
versions, total, err := repository.ListActivityTemplateVersions(ctx, item.TemplateID, 1, 20)
|
||||
if err != nil {
|
||||
t.Fatalf("list versions: %v", err)
|
||||
}
|
||||
if total != 2 || len(versions) != 2 {
|
||||
t.Fatalf("version total/items = %d/%d", total, len(versions))
|
||||
}
|
||||
if versions[0].VersionNo != 2 || versions[0].RuntimeFromMS != 600 || versions[0].RuntimeToMS != 0 {
|
||||
t.Fatalf("v2 runtime window = %+v", versions[0])
|
||||
}
|
||||
if versions[1].VersionNo != 1 || versions[1].RuntimeFromMS != 100 || versions[1].RuntimeToMS != 500 {
|
||||
t.Fatalf("v1 runtime window = %+v", versions[1])
|
||||
}
|
||||
v1, err := repository.GetActivityTemplateVersion(ctx, item.TemplateID, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("get v1: %v", err)
|
||||
}
|
||||
if v1.RuntimeFromMS != 100 || v1.RuntimeToMS != 500 {
|
||||
t.Fatalf("get v1 runtime window = %+v", v1)
|
||||
}
|
||||
}
|
||||
|
||||
func createRepositoryTemplate(t *testing.T, ctx context.Context, repository *Repository, code string, allRegions bool, regionIDs []int64, startMS int64, endMS int64) domain.Template {
|
||||
t.Helper()
|
||||
item, err := repository.CreateActivityTemplate(ctx, domain.Template{
|
||||
TemplateCode: code, Name: code, ActivityType: domain.ActivityTypeGiftChallenge,
|
||||
StartMS: startMS, EndMS: endMS, AllRegions: allRegions, RegionIDs: regionIDs,
|
||||
Gifts: []domain.Gift{{
|
||||
GiftID: "gift_rose", SortOrder: 1, Name: "Rose", IconURL: "https://cdn.example.com/gift_rose.png",
|
||||
CoinPrice: 100, PriceVersion: "v1",
|
||||
}},
|
||||
DailyLeaderboardSize: 100, TotalLeaderboardSize: 100, DisplayConfigJSON: `{}`,
|
||||
}, 90001, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("create template %s: %v", code, err)
|
||||
}
|
||||
return item
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,329 @@
|
||||
package mysql
|
||||
|
||||
import "context"
|
||||
|
||||
// ensureActivityTemplateRuntimeTables mirrors migration 011 for explicitly enabled local/test auto-migrate.
|
||||
func (r *Repository) ensureActivityTemplateRuntimeTables(ctx context.Context) error {
|
||||
statements := []string{
|
||||
`CREATE TABLE IF NOT EXISTS activity_template_runtime_versions (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
start_ms BIGINT NOT NULL,
|
||||
end_ms BIGINT NOT NULL,
|
||||
all_regions BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
runtime_from_ms BIGINT NOT NULL,
|
||||
runtime_to_ms BIGINT NOT NULL DEFAULT 0,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, template_id, version_no),
|
||||
KEY idx_activity_template_runtime_current (app_code, template_code, start_ms, end_ms, runtime_from_ms, runtime_to_ms),
|
||||
KEY idx_activity_template_runtime_due (app_code, end_ms, runtime_to_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版不可变运行版本启停窗口'`,
|
||||
`CREATE TABLE IF NOT EXISTS activity_template_runtime_regions (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
region_id BIGINT NOT NULL,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, template_id, version_no, region_id),
|
||||
KEY idx_activity_template_runtime_region (app_code, region_id, template_id, version_no)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版运行版本区域快照'`,
|
||||
`CREATE TABLE IF NOT EXISTS activity_template_event_consumption (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
source_event_id VARCHAR(128) NOT NULL,
|
||||
event_type VARCHAR(64) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL DEFAULT '',
|
||||
template_code VARCHAR(64) NOT NULL DEFAULT '',
|
||||
version_no BIGINT NOT NULL DEFAULT 0,
|
||||
user_id BIGINT NOT NULL DEFAULT 0,
|
||||
region_id BIGINT NOT NULL DEFAULT 0,
|
||||
task_day VARCHAR(10) NOT NULL DEFAULT '',
|
||||
status VARCHAR(24) NOT NULL,
|
||||
skip_reason VARCHAR(128) NOT NULL DEFAULT '',
|
||||
occurred_at_ms BIGINT NOT NULL,
|
||||
consumed_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, source_event_id),
|
||||
KEY idx_activity_template_event_template (app_code, template_id, version_no, occurred_at_ms),
|
||||
KEY idx_activity_template_event_status (app_code, status, consumed_at_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版运行事件幂等事实'`,
|
||||
`CREATE TABLE IF NOT EXISTS activity_template_user_day_scores (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
task_day VARCHAR(10) NOT NULL,
|
||||
day_start_ms BIGINT NOT NULL,
|
||||
day_end_ms BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
score BIGINT NOT NULL DEFAULT 0,
|
||||
gift_count BIGINT NOT NULL DEFAULT 0,
|
||||
coin_amount BIGINT NOT NULL DEFAULT 0,
|
||||
first_scored_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, template_id, version_no, task_day, user_id),
|
||||
KEY idx_activity_template_day_rank (app_code, template_id, version_no, task_day, score DESC, first_scored_at_ms, user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版 UTC 日榜实时分数'`,
|
||||
`CREATE TABLE IF NOT EXISTS activity_template_user_total_scores (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
score BIGINT NOT NULL DEFAULT 0,
|
||||
gift_count BIGINT NOT NULL DEFAULT 0,
|
||||
coin_amount BIGINT NOT NULL DEFAULT 0,
|
||||
first_scored_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, template_id, version_no, user_id),
|
||||
KEY idx_activity_template_total_rank (app_code, template_id, version_no, score DESC, first_scored_at_ms, user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版活动总榜实时分数'`,
|
||||
`CREATE TABLE IF NOT EXISTS activity_template_daily_visits (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
task_day VARCHAR(10) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
region_id BIGINT NOT NULL DEFAULT 0,
|
||||
command_id VARCHAR(128) NOT NULL,
|
||||
visited_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, template_id, version_no, task_day, user_id),
|
||||
UNIQUE KEY uk_activity_template_visit_command (app_code, command_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版 daily_visit 自然幂等事实'`,
|
||||
`CREATE TABLE IF NOT EXISTS activity_template_task_progress (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
task_day VARCHAR(10) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
first_region_id BIGINT NOT NULL DEFAULT 0,
|
||||
task_key VARCHAR(64) NOT NULL,
|
||||
task_type VARCHAR(32) NOT NULL,
|
||||
target_value BIGINT NOT NULL,
|
||||
progress_value BIGINT NOT NULL DEFAULT 0,
|
||||
reward_resource_group_id BIGINT NOT NULL,
|
||||
reward_snapshot_id VARCHAR(96) NOT NULL DEFAULT '',
|
||||
reward_snapshot_json JSON NULL,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
status VARCHAR(24) NOT NULL,
|
||||
completed_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||
earning_region_id BIGINT NOT NULL DEFAULT 0,
|
||||
attribution_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||
claimed_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, template_id, version_no, task_day, user_id, task_key),
|
||||
KEY idx_activity_template_task_data (app_code, template_id, version_no, task_day, task_key, status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版用户每日任务进度'`,
|
||||
`CREATE TABLE IF NOT EXISTS activity_template_task_claims (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
claim_id VARCHAR(96) NOT NULL,
|
||||
command_id VARCHAR(128) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
task_day VARCHAR(10) NOT NULL,
|
||||
task_key VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
earning_region_id BIGINT NOT NULL DEFAULT 0,
|
||||
attribution_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||
reward_resource_group_id BIGINT NOT NULL,
|
||||
reward_snapshot_id VARCHAR(96) NOT NULL DEFAULT '',
|
||||
reward_snapshot_json JSON NULL,
|
||||
wallet_command_id VARCHAR(160) NOT NULL,
|
||||
wallet_grant_id VARCHAR(96) NOT NULL DEFAULT '',
|
||||
status VARCHAR(24) NOT NULL,
|
||||
failure_reason VARCHAR(512) NOT NULL DEFAULT '',
|
||||
attempt_count INT NOT NULL DEFAULT 0,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
granted_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (app_code, claim_id),
|
||||
UNIQUE KEY uk_activity_template_claim_command (app_code, command_id),
|
||||
UNIQUE KEY uk_activity_template_claim_task (app_code, template_id, version_no, task_day, user_id, task_key),
|
||||
UNIQUE KEY uk_activity_template_claim_wallet (app_code, wallet_command_id),
|
||||
KEY idx_activity_template_claim_status (app_code, status, updated_at_ms),
|
||||
KEY idx_activity_template_claim_admin (app_code, template_id, version_no, task_day, task_key, user_id, status, updated_at_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版任务资源组领奖事实'`,
|
||||
`CREATE TABLE IF NOT EXISTS activity_template_rank_periods (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
board_type VARCHAR(16) NOT NULL,
|
||||
period_key VARCHAR(24) NOT NULL,
|
||||
period_start_ms BIGINT NOT NULL,
|
||||
period_end_ms BIGINT NOT NULL,
|
||||
settle_after_ms BIGINT NOT NULL COMMENT '结算水位:周期结束加允许迟到窗口,UTC epoch ms',
|
||||
leaderboard_size INT NOT NULL,
|
||||
status VARCHAR(24) NOT NULL,
|
||||
locked_by VARCHAR(128) NOT NULL DEFAULT '',
|
||||
lock_until_ms BIGINT NOT NULL DEFAULT 0,
|
||||
settled_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, template_id, version_no, board_type, period_key),
|
||||
KEY idx_activity_template_period_settle_due (app_code, status, settle_after_ms, lock_until_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版榜单结算周期'`,
|
||||
`CREATE TABLE IF NOT EXISTS activity_template_rank_snapshots (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
board_type VARCHAR(16) NOT NULL,
|
||||
period_key VARCHAR(24) NOT NULL,
|
||||
rank_no INT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
score BIGINT NOT NULL,
|
||||
gift_count BIGINT NOT NULL,
|
||||
coin_amount BIGINT NOT NULL,
|
||||
first_scored_at_ms BIGINT NOT NULL,
|
||||
snapshot_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, template_id, version_no, board_type, period_key, rank_no),
|
||||
UNIQUE KEY uk_activity_template_rank_user (app_code, template_id, version_no, board_type, period_key, user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版冻结榜单快照'`,
|
||||
`CREATE TABLE IF NOT EXISTS activity_template_reward_jobs (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
reward_job_id VARCHAR(96) NOT NULL,
|
||||
template_id VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(64) NOT NULL,
|
||||
version_no BIGINT NOT NULL,
|
||||
board_type VARCHAR(16) NOT NULL,
|
||||
period_key VARCHAR(24) NOT NULL,
|
||||
rank_no INT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
score BIGINT NOT NULL,
|
||||
resource_group_id BIGINT NOT NULL,
|
||||
reward_snapshot_id VARCHAR(96) NOT NULL DEFAULT '',
|
||||
reward_snapshot_json JSON NULL,
|
||||
wallet_command_id VARCHAR(192) NOT NULL,
|
||||
wallet_grant_id VARCHAR(96) NOT NULL DEFAULT '',
|
||||
status VARCHAR(24) NOT NULL,
|
||||
failure_reason VARCHAR(512) NOT NULL DEFAULT '',
|
||||
attempt_count INT NOT NULL DEFAULT 0,
|
||||
max_attempts INT NOT NULL DEFAULT 8 COMMENT '自动投递总尝试上限;dead 手工复活时只扩一轮预算',
|
||||
next_retry_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '下一次可领取时间,UTC epoch ms',
|
||||
locked_by VARCHAR(128) NOT NULL DEFAULT '' COMMENT '当前 job lease owner',
|
||||
lock_until_ms BIGINT NOT NULL DEFAULT 0 COMMENT 'job lease 过期时间,UTC epoch ms',
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
granted_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (app_code, reward_job_id),
|
||||
UNIQUE KEY uk_activity_template_reward_rank (app_code, template_id, version_no, board_type, period_key, rank_no),
|
||||
UNIQUE KEY uk_activity_template_reward_wallet (app_code, wallet_command_id),
|
||||
KEY idx_activity_template_reward_query (app_code, template_id, version_no, board_type, period_key, status),
|
||||
KEY idx_activity_template_reward_retry (app_code, status, updated_at_ms),
|
||||
KEY idx_activity_template_reward_claim (app_code, status, next_retry_at_ms, lock_until_ms, created_at_ms, reward_job_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版榜单奖励发放任务'`,
|
||||
`CREATE TABLE IF NOT EXISTS activity_template_stats_outbox (
|
||||
outbox_id VARCHAR(96) NOT NULL,
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
event_type VARCHAR(64) NOT NULL,
|
||||
payload BLOB NOT NULL,
|
||||
status VARCHAR(24) NOT NULL,
|
||||
retry_count INT NOT NULL DEFAULT 0,
|
||||
next_retry_at_ms BIGINT NOT NULL,
|
||||
locked_by VARCHAR(128) NOT NULL DEFAULT '',
|
||||
lock_until_ms BIGINT NOT NULL DEFAULT 0,
|
||||
last_error VARCHAR(512) NOT NULL DEFAULT '',
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (outbox_id),
|
||||
KEY idx_activity_template_stats_outbox_claim (status, next_retry_at_ms, lock_until_ms, created_at_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版统计事实 RocketMQ transactional outbox'`,
|
||||
}
|
||||
for _, statement := range statements {
|
||||
if _, err := r.db.ExecContext(ctx, statement); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
rewardSnapshotColumns := []struct {
|
||||
table string
|
||||
column string
|
||||
statement string
|
||||
}{
|
||||
{"activity_template_task_progress", "reward_snapshot_id", `ALTER TABLE activity_template_task_progress ADD COLUMN reward_snapshot_id VARCHAR(96) NOT NULL DEFAULT '' AFTER reward_resource_group_id`},
|
||||
{"activity_template_task_progress", "reward_snapshot_json", `ALTER TABLE activity_template_task_progress ADD COLUMN reward_snapshot_json JSON NULL AFTER reward_snapshot_id`},
|
||||
{"activity_template_task_claims", "reward_snapshot_id", `ALTER TABLE activity_template_task_claims ADD COLUMN reward_snapshot_id VARCHAR(96) NOT NULL DEFAULT '' AFTER reward_resource_group_id`},
|
||||
{"activity_template_task_claims", "reward_snapshot_json", `ALTER TABLE activity_template_task_claims ADD COLUMN reward_snapshot_json JSON NULL AFTER reward_snapshot_id`},
|
||||
{"activity_template_daily_visits", "region_id", `ALTER TABLE activity_template_daily_visits ADD COLUMN region_id BIGINT NOT NULL DEFAULT 0 AFTER user_id`},
|
||||
{"activity_template_task_progress", "first_region_id", `ALTER TABLE activity_template_task_progress ADD COLUMN first_region_id BIGINT NOT NULL DEFAULT 0 AFTER user_id`},
|
||||
{"activity_template_task_progress", "earning_region_id", `ALTER TABLE activity_template_task_progress ADD COLUMN earning_region_id BIGINT NOT NULL DEFAULT 0 AFTER completed_at_ms`},
|
||||
{"activity_template_task_progress", "attribution_at_ms", `ALTER TABLE activity_template_task_progress ADD COLUMN attribution_at_ms BIGINT NOT NULL DEFAULT 0 AFTER earning_region_id`},
|
||||
{"activity_template_task_claims", "earning_region_id", `ALTER TABLE activity_template_task_claims ADD COLUMN earning_region_id BIGINT NOT NULL DEFAULT 0 AFTER user_id`},
|
||||
{"activity_template_task_claims", "attribution_at_ms", `ALTER TABLE activity_template_task_claims ADD COLUMN attribution_at_ms BIGINT NOT NULL DEFAULT 0 AFTER earning_region_id`},
|
||||
{"activity_template_reward_jobs", "reward_snapshot_id", `ALTER TABLE activity_template_reward_jobs ADD COLUMN reward_snapshot_id VARCHAR(96) NOT NULL DEFAULT '' AFTER resource_group_id`},
|
||||
{"activity_template_reward_jobs", "reward_snapshot_json", `ALTER TABLE activity_template_reward_jobs ADD COLUMN reward_snapshot_json JSON NULL AFTER reward_snapshot_id`},
|
||||
{"activity_template_reward_jobs", "max_attempts", `ALTER TABLE activity_template_reward_jobs ADD COLUMN max_attempts INT NOT NULL DEFAULT 8 COMMENT '自动投递总尝试上限;dead 手工复活时只扩一轮预算' AFTER attempt_count`},
|
||||
{"activity_template_reward_jobs", "next_retry_at_ms", `ALTER TABLE activity_template_reward_jobs ADD COLUMN next_retry_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '下一次可领取时间,UTC epoch ms' AFTER max_attempts`},
|
||||
{"activity_template_reward_jobs", "locked_by", `ALTER TABLE activity_template_reward_jobs ADD COLUMN locked_by VARCHAR(128) NOT NULL DEFAULT '' COMMENT '当前 job lease owner' AFTER next_retry_at_ms`},
|
||||
{"activity_template_reward_jobs", "lock_until_ms", `ALTER TABLE activity_template_reward_jobs ADD COLUMN lock_until_ms BIGINT NOT NULL DEFAULT 0 COMMENT 'job lease 过期时间,UTC epoch ms' AFTER locked_by`},
|
||||
}
|
||||
for _, addition := range rewardSnapshotColumns {
|
||||
exists, err := r.columnExists(ctx, addition.table, addition.column)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
if _, err := r.db.ExecContext(ctx, addition.statement); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
// 旧环境可能已经执行过 011。新增水位列先以 0 落地,再只回填缺失值;历史周期一旦
|
||||
// 物化便不随之后的配置变化漂移,保证结算审计可以复现当时的迟到容忍策略。
|
||||
settleAfterExists, err := r.columnExists(ctx, "activity_template_rank_periods", "settle_after_ms")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !settleAfterExists {
|
||||
if _, err := r.db.ExecContext(ctx, `
|
||||
ALTER TABLE activity_template_rank_periods
|
||||
ADD COLUMN settle_after_ms BIGINT NOT NULL DEFAULT 0
|
||||
COMMENT '结算水位:周期结束加允许迟到窗口,UTC epoch ms' AFTER period_end_ms`); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := r.db.ExecContext(ctx, `
|
||||
UPDATE activity_template_rank_periods
|
||||
SET settle_after_ms = period_end_ms + ?
|
||||
WHERE settle_after_ms <= 0`, r.activityTemplateSettlementLatenessMS()); err != nil {
|
||||
return err
|
||||
}
|
||||
settleDueIndexExists, err := r.indexExists(ctx, "activity_template_rank_periods", "idx_activity_template_period_settle_due")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !settleDueIndexExists {
|
||||
if _, err := r.db.ExecContext(ctx, `
|
||||
CREATE INDEX idx_activity_template_period_settle_due
|
||||
ON activity_template_rank_periods (app_code, status, settle_after_ms, lock_until_ms)`); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
rewardClaimIndexExists, err := r.indexExists(ctx, "activity_template_reward_jobs", "idx_activity_template_reward_claim")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !rewardClaimIndexExists {
|
||||
if _, err := r.db.ExecContext(ctx, `
|
||||
CREATE INDEX idx_activity_template_reward_claim
|
||||
ON activity_template_reward_jobs (app_code, status, next_retry_at_ms, lock_until_ms, created_at_ms, reward_job_id)`); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
taskClaimAdminIndexExists, err := r.indexExists(ctx, "activity_template_task_claims", "idx_activity_template_claim_admin")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !taskClaimAdminIndexExists {
|
||||
if _, err := r.db.ExecContext(ctx, `
|
||||
CREATE INDEX idx_activity_template_claim_admin
|
||||
ON activity_template_task_claims (app_code, template_id, version_no, task_day, task_key, user_id, status, updated_at_ms)`); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,126 @@
|
||||
package mysql
|
||||
|
||||
import "context"
|
||||
|
||||
// ensureActivityTemplateTables mirrors deploy/mysql/migrations/010_activity_templates.sql for explicitly enabled local auto-migrate.
|
||||
// Production still applies the numbered SQL migration so schema history remains reviewable and deterministic.
|
||||
func (r *Repository) ensureActivityTemplateTables(ctx context.Context) error {
|
||||
statements := []string{
|
||||
`CREATE TABLE IF NOT EXISTS activity_templates (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
template_id VARCHAR(96) NOT NULL COMMENT '活动模版 ID',
|
||||
template_code VARCHAR(64) NOT NULL COMMENT 'App 内唯一业务编码',
|
||||
name VARCHAR(128) NOT NULL COMMENT '后台内部名称',
|
||||
activity_type VARCHAR(32) NOT NULL DEFAULT 'gift_challenge' COMMENT '活动类型',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'draft' COMMENT 'draft/published/disabled/archived',
|
||||
start_ms BIGINT NOT NULL DEFAULT 0 COMMENT '活动开始,UTC epoch ms,包含',
|
||||
end_ms BIGINT NOT NULL DEFAULT 0 COMMENT '活动结束,UTC epoch ms,不包含',
|
||||
all_regions BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否覆盖全部区域',
|
||||
daily_leaderboard_size INT NOT NULL DEFAULT 100 COMMENT '日榜最多展示人数',
|
||||
total_leaderboard_size INT NOT NULL DEFAULT 100 COMMENT '总榜最多展示人数',
|
||||
display_config_json JSON NOT NULL COMMENT '客户端布局和样式配置',
|
||||
revision BIGINT NOT NULL DEFAULT 1 COMMENT '后台编辑乐观锁版本',
|
||||
published_version BIGINT NOT NULL DEFAULT 0 COMMENT '最近发布快照版本',
|
||||
created_by_admin_id BIGINT NOT NULL COMMENT '创建管理员',
|
||||
updated_by_admin_id BIGINT NOT NULL COMMENT '最后更新管理员',
|
||||
published_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '最近发布管理员',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
published_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '最近发布时间,UTC epoch ms',
|
||||
archived_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '软归档时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, template_id),
|
||||
UNIQUE KEY uk_activity_template_code (app_code, template_code),
|
||||
KEY idx_activity_template_list (app_code, status, updated_at_ms, template_id),
|
||||
KEY idx_activity_template_period (app_code, status, start_ms, end_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='可排期运营活动模版实例'`,
|
||||
`CREATE TABLE IF NOT EXISTS activity_template_publish_locks (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '最近获取发布锁时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版发布事务互斥行'`,
|
||||
`CREATE TABLE IF NOT EXISTS activity_template_regions (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
template_id VARCHAR(96) NOT NULL COMMENT '活动模版 ID',
|
||||
region_id BIGINT NOT NULL COMMENT '投放区域 ID',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, template_id, region_id),
|
||||
KEY idx_activity_template_region_match (app_code, region_id, template_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版投放区域'`,
|
||||
`CREATE TABLE IF NOT EXISTS activity_template_locales (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
template_id VARCHAR(96) NOT NULL COMMENT '活动模版 ID',
|
||||
locale VARCHAR(24) NOT NULL COMMENT 'BCP-47 基础语言码',
|
||||
title VARCHAR(160) NOT NULL DEFAULT '' COMMENT '客户端活动标题',
|
||||
rules MEDIUMTEXT NOT NULL COMMENT '客户端活动规则正文',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, template_id, locale)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版多语言文案'`,
|
||||
`CREATE TABLE IF NOT EXISTS activity_template_gifts (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
template_id VARCHAR(96) NOT NULL COMMENT '活动模版 ID',
|
||||
gift_id VARCHAR(96) NOT NULL COMMENT 'wallet 礼物 ID',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '展示顺序',
|
||||
name VARCHAR(128) NOT NULL DEFAULT '' COMMENT '发布展示名称快照',
|
||||
icon_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '发布展示图标快照',
|
||||
coin_price BIGINT NOT NULL DEFAULT 0 COMMENT '发布时金币价格快照,仅供展示',
|
||||
price_version VARCHAR(64) NOT NULL DEFAULT '' COMMENT '发布时价格版本快照',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, template_id, gift_id),
|
||||
KEY idx_activity_template_gift_match (app_code, gift_id, template_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版指定礼物和展示快照'`,
|
||||
`CREATE TABLE IF NOT EXISTS activity_template_tasks (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
template_id VARCHAR(96) NOT NULL COMMENT '活动模版 ID',
|
||||
task_key VARCHAR(64) NOT NULL COMMENT '模版内稳定任务键',
|
||||
task_type VARCHAR(32) NOT NULL COMMENT 'daily_visit/gift_count/gift_coin_amount',
|
||||
target_value BIGINT NOT NULL DEFAULT 0 COMMENT '任务达成阈值',
|
||||
reward_resource_group_id BIGINT NOT NULL DEFAULT 0 COMMENT 'wallet 奖励资源组 ID',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '展示顺序',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, template_id, task_key)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版每日任务配置'`,
|
||||
`CREATE TABLE IF NOT EXISTS activity_template_rank_rewards (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
template_id VARCHAR(96) NOT NULL COMMENT '活动模版 ID',
|
||||
board_type VARCHAR(16) NOT NULL COMMENT 'daily/total',
|
||||
rank_from INT NOT NULL COMMENT '奖励起始名次,闭区间',
|
||||
rank_to INT NOT NULL COMMENT '奖励结束名次,闭区间',
|
||||
resource_group_id BIGINT NOT NULL DEFAULT 0 COMMENT 'wallet 奖励资源组 ID',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '展示顺序',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, template_id, board_type, rank_from, rank_to)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版日榜和总榜名次奖励'`,
|
||||
`CREATE TABLE IF NOT EXISTS activity_template_assets (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
template_id VARCHAR(96) NOT NULL COMMENT '活动模版 ID',
|
||||
asset_key VARCHAR(64) NOT NULL COMMENT '素材位键',
|
||||
locale VARCHAR(24) NOT NULL DEFAULT '*' COMMENT '* 表示共享素材,否则为语言码',
|
||||
url VARCHAR(1024) NOT NULL COMMENT 'COS/CDN 素材地址',
|
||||
media_type VARCHAR(16) NOT NULL COMMENT 'image/webp/svga/pag',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '展示顺序',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, template_id, asset_key, locale)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版客户端素材'`,
|
||||
`CREATE TABLE IF NOT EXISTS activity_template_versions (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
template_id VARCHAR(96) NOT NULL COMMENT '活动模版 ID',
|
||||
version_no BIGINT NOT NULL COMMENT '单模版递增发布版本',
|
||||
snapshot_json JSON NOT NULL COMMENT '发布时完整不可变配置快照',
|
||||
published_by_admin_id BIGINT NOT NULL COMMENT '发布管理员',
|
||||
published_at_ms BIGINT NOT NULL COMMENT '发布时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, template_id, version_no),
|
||||
KEY idx_activity_template_version_list (app_code, template_id, published_at_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动模版不可变发布版本'`,
|
||||
}
|
||||
for _, statement := range statements {
|
||||
if _, err := r.db.ExecContext(ctx, statement); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,245 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/xerr"
|
||||
domain "hyapp/services/activity-service/internal/domain/activitytemplate"
|
||||
)
|
||||
|
||||
var activityTemplateRewardLeaseTestColumns = []string{
|
||||
"app_code", "reward_job_id", "template_id", "template_code", "version_no", "board_type", "period_key",
|
||||
"rank_no", "user_id", "score", "resource_group_id", "reward_snapshot_id", "reward_snapshot_json",
|
||||
"wallet_command_id", "wallet_grant_id", "status", "failure_reason", "attempt_count", "created_at_ms",
|
||||
"updated_at_ms", "granted_at_ms", "max_attempts", "next_retry_at_ms", "locked_by", "lock_until_ms",
|
||||
}
|
||||
|
||||
var activityTemplateRankPeriodLeaseTestColumns = []string{
|
||||
"app_code", "template_id", "template_code", "version_no", "board_type", "period_key",
|
||||
"period_start_ms", "period_end_ms", "settle_after_ms", "leaderboard_size", "status", "settled_at_ms",
|
||||
}
|
||||
|
||||
func TestClaimDueActivityTemplateRankPeriodsExcludesMaterializedBlockedJobsBeforePending(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("sqlmock.New(): %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = db.Close() })
|
||||
repository := &Repository{db: db}
|
||||
ctx := appcode.WithContext(context.Background(), "hyapp")
|
||||
const nowMS = int64(100_000)
|
||||
lockUntilMS := nowMS + time.Minute.Milliseconds()
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectQuery(`(?s)FROM activity_template_rank_periods p.+p.status = \?.+p.status = \?.+NOT EXISTS \(.+FROM activity_template_reward_jobs j.+LIMIT \? FOR UPDATE SKIP LOCKED`).
|
||||
WithArgs("hyapp", nowMS, domain.PeriodStatusPending, domain.PeriodStatusSettling, nowMS, 50).
|
||||
WillReturnRows(sqlmock.NewRows(activityTemplateRankPeriodLeaseTestColumns).
|
||||
AddRow("hyapp", "pending-after-50-blocked", "summer", 3, domain.BoardTypeTotal, "total", 1, 2, 3, 100, domain.PeriodStatusPending, 0))
|
||||
mock.ExpectExec(`(?s)UPDATE activity_template_rank_periods.+SET status = \?, locked_by = \?, lock_until_ms = \?.+WHERE app_code = \?`).
|
||||
WithArgs(domain.PeriodStatusSettling, "settler", lockUntilMS, nowMS, "hyapp", "pending-after-50-blocked", int64(3), domain.BoardTypeTotal, "total").
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectCommit()
|
||||
|
||||
periods, err := repository.ClaimDueActivityTemplateRankPeriods(ctx, "settler", nowMS, time.Minute, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("ClaimDueActivityTemplateRankPeriods(): %v", err)
|
||||
}
|
||||
if len(periods) != 1 || periods[0].TemplateID != "pending-after-50-blocked" {
|
||||
t.Fatalf("claimed periods = %+v, want only actionable pending period", periods)
|
||||
}
|
||||
if len(periods) == 50 {
|
||||
t.Fatal("dead/backoff periods must not fill the discovery batch")
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sql expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaimFinalizableActivityTemplateRankPeriodsOnlySelectsAllGrantedRewardingPeriods(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("sqlmock.New(): %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = db.Close() })
|
||||
repository := &Repository{db: db}
|
||||
ctx := appcode.WithContext(context.Background(), "hyapp")
|
||||
const nowMS = int64(200_000)
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectQuery(`(?s)FROM activity_template_rank_periods p.+p.status = \?.+p.status = \? AND EXISTS \(.+FROM activity_template_reward_jobs materialized.+NOT EXISTS \(.+FROM activity_template_reward_jobs j.+j.status <> \?.+LIMIT \? FOR UPDATE SKIP LOCKED`).
|
||||
WithArgs("hyapp", nowMS, domain.PeriodStatusRewarding, domain.PeriodStatusSettling, domain.ClaimStatusGranted, 50).
|
||||
WillReturnRows(sqlmock.NewRows(activityTemplateRankPeriodLeaseTestColumns))
|
||||
mock.ExpectCommit()
|
||||
|
||||
periods, err := repository.ClaimFinalizableActivityTemplateRankPeriods(ctx, "finalizer", nowMS, time.Minute, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("ClaimFinalizableActivityTemplateRankPeriods(): %v", err)
|
||||
}
|
||||
if len(periods) != 0 {
|
||||
t.Fatalf("finalizable periods = %+v, want none while jobs are blocked", periods)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sql expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaimActivityTemplateRewardJobsUsesFiniteLeasesAndRecoversExpiredRunning(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("sqlmock.New(): %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = db.Close() })
|
||||
repository := &Repository{db: db}
|
||||
ctx := appcode.WithContext(context.Background(), "hyapp")
|
||||
nowMS := int64(1_000)
|
||||
lockUntilMS := nowMS + (30 * time.Second).Milliseconds()
|
||||
|
||||
mock.ExpectBegin()
|
||||
rows := sqlmock.NewRows(activityTemplateRewardLeaseTestColumns).
|
||||
AddRow("hyapp", "job_failed", "tpl_1", "summer", 2, "daily", "2026-07-15", 1, 1001, 900, 81, "", "null", "wallet-stable-failed", "", domain.ClaimStatusFailed, "wallet unavailable", 1, 100, 900, 0, 8, 900, "", 0).
|
||||
AddRow("hyapp", "job_crashed", "tpl_1", "summer", 2, "daily", "2026-07-15", 2, 1002, 800, 82, "", "null", "wallet-stable-crashed", "", domain.ClaimStatusRunning, "", 2, 100, 900, 0, 8, 0, "dead-worker", 999).
|
||||
AddRow("hyapp", "job_exhausted", "tpl_1", "summer", 2, "daily", "2026-07-15", 3, 1003, 700, 83, "", "null", "wallet-stable-exhausted", "", domain.ClaimStatusRunning, "", 8, 100, 900, 0, 8, 0, "dead-worker", 999)
|
||||
mock.ExpectQuery(`(?s)SELECT app_code, reward_job_id.+FROM activity_template_reward_jobs.+FOR UPDATE SKIP LOCKED`).
|
||||
WithArgs("hyapp", domain.ClaimStatusPending, domain.ClaimStatusFailed, nowMS, domain.ClaimStatusRunning, nowMS, 3).
|
||||
WillReturnRows(rows)
|
||||
mock.ExpectExec(`(?s)UPDATE activity_template_reward_jobs.+attempt_count = attempt_count \+ 1.+WHERE app_code = \? AND reward_job_id = \?`).
|
||||
WithArgs(domain.ClaimStatusRunning, "settler-a", lockUntilMS, nowMS, "hyapp", "job_failed").
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectExec(`(?s)UPDATE activity_template_reward_jobs.+attempt_count = attempt_count \+ 1.+WHERE app_code = \? AND reward_job_id = \?`).
|
||||
WithArgs(domain.ClaimStatusRunning, "settler-a", lockUntilMS, nowMS, "hyapp", "job_crashed").
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectExec(`(?s)UPDATE activity_template_reward_jobs.+failure_reason = CASE WHEN failure_reason = ''.+WHERE app_code = \? AND reward_job_id = \?`).
|
||||
WithArgs(activityTemplateRewardJobStatusDead, "maximum delivery attempts exhausted", nowMS, "hyapp", "job_exhausted").
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectCommit()
|
||||
|
||||
jobs, err := repository.ClaimActivityTemplateRewardJobs(ctx, "settler-a", nowMS, 30*time.Second, 3)
|
||||
if err != nil {
|
||||
t.Fatalf("ClaimActivityTemplateRewardJobs(): %v", err)
|
||||
}
|
||||
if len(jobs) != 2 {
|
||||
t.Fatalf("claimed jobs = %+v, want failed and expired-running jobs only", jobs)
|
||||
}
|
||||
if jobs[0].RewardJobID != "job_failed" || jobs[0].AttemptCount != 2 || jobs[0].WalletCommandID != "wallet-stable-failed" {
|
||||
t.Fatalf("failed retry claim = %+v", jobs[0])
|
||||
}
|
||||
if jobs[1].RewardJobID != "job_crashed" || jobs[1].AttemptCount != 3 || jobs[1].WalletCommandID != "wallet-stable-crashed" {
|
||||
t.Fatalf("expired running recovery = %+v", jobs[1])
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sql expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkActivityTemplateRewardJobFailedWithLeaseSchedulesStableBackoff(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("sqlmock.New(): %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = db.Close() })
|
||||
repository := &Repository{db: db}
|
||||
ctx := appcode.WithContext(context.Background(), "hyapp")
|
||||
nowMS := int64(20_000)
|
||||
nextRetryAtMS := activityTemplateRewardRetryAtMS("job_retry", 2, nowMS)
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectQuery(`(?s)SELECT attempt_count, max_attempts FROM activity_template_reward_jobs.+FOR UPDATE`).
|
||||
WithArgs("hyapp", "job_retry", domain.ClaimStatusRunning, "settler-b", nowMS).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"attempt_count", "max_attempts"}).AddRow(2, 8))
|
||||
mock.ExpectExec(`(?s)UPDATE activity_template_reward_jobs.+next_retry_at_ms = \?.+locked_by = ''.+lock_until_ms = 0.+locked_by = \?.+lock_until_ms > \?`).
|
||||
WithArgs(domain.ClaimStatusFailed, "wallet timeout", nextRetryAtMS, nowMS, "hyapp", "job_retry", domain.ClaimStatusRunning, "settler-b", nowMS).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectCommit()
|
||||
|
||||
if err := repository.MarkActivityTemplateRewardJobFailedWithLease(ctx, "job_retry", "settler-b", "wallet timeout", nowMS); err != nil {
|
||||
t.Fatalf("MarkActivityTemplateRewardJobFailedWithLease(): %v", err)
|
||||
}
|
||||
if nextRetryAtMS <= nowMS+activityTemplateRewardJobRetryBase.Milliseconds() {
|
||||
t.Fatalf("next retry = %d, want exponential delay plus jitter after %d", nextRetryAtMS, nowMS)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sql expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkActivityTemplateRewardJobGrantedRejectsLostLeaseOwner(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("sqlmock.New(): %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = db.Close() })
|
||||
repository := &Repository{db: db}
|
||||
ctx := appcode.WithContext(context.Background(), "hyapp")
|
||||
nowMS := int64(50_000)
|
||||
|
||||
mock.ExpectExec(`(?s)UPDATE activity_template_reward_jobs.+status = \?.+locked_by = ''.+WHERE app_code = \?.+locked_by = \?.+lock_until_ms > \?`).
|
||||
WithArgs(domain.ClaimStatusGranted, "grant_1", nowMS, nowMS, "hyapp", "job_taken_over", domain.ClaimStatusRunning, "stale-worker", nowMS).
|
||||
WillReturnResult(sqlmock.NewResult(0, 0))
|
||||
err = repository.MarkActivityTemplateRewardJobGrantedWithLease(ctx, "job_taken_over", "stale-worker", "grant_1", nowMS)
|
||||
if xerr.CodeOf(err) != xerr.Conflict {
|
||||
t.Fatalf("lost owner error = %v, want conflict", err)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sql expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaimActivityTemplateRewardJobForManualRetryRevivesDeadWithoutChangingWalletCommand(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("sqlmock.New(): %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = db.Close() })
|
||||
repository := &Repository{db: db}
|
||||
ctx := appcode.WithContext(context.Background(), "hyapp")
|
||||
nowMS := int64(80_000)
|
||||
lockUntilMS := nowMS + time.Minute.Milliseconds()
|
||||
|
||||
mock.ExpectBegin()
|
||||
rows := sqlmock.NewRows(activityTemplateRewardLeaseTestColumns).
|
||||
AddRow("hyapp", "job_dead", "tpl_2", "anniversary", 4, "total", "total", 1, 2001, 9_000, 91, "", "null", "wallet-command-never-changes", "", activityTemplateRewardJobStatusDead, "attempts exhausted", 8, 100, 79_000, 0, 8, 0, "", 0)
|
||||
mock.ExpectQuery(`(?s)SELECT app_code, reward_job_id.+FROM activity_template_reward_jobs.+FOR UPDATE`).
|
||||
WithArgs("hyapp", "job_dead").WillReturnRows(rows)
|
||||
mock.ExpectExec(`(?s)UPDATE activity_template_reward_jobs.+max_attempts = \?.+locked_by = \?.+lock_until_ms = \?.+status IN \(\?, \?\)`).
|
||||
WithArgs(domain.ClaimStatusRunning, int32(9), "admin-retry-90001", lockUntilMS, nowMS, "hyapp", "job_dead", domain.ClaimStatusFailed, activityTemplateRewardJobStatusDead).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectCommit()
|
||||
|
||||
job, err := repository.ClaimActivityTemplateRewardJobForManualRetry(ctx, "tpl_2", "job_dead", "admin-retry-90001", nowMS, time.Minute)
|
||||
if err != nil {
|
||||
t.Fatalf("ClaimActivityTemplateRewardJobForManualRetry(): %v", err)
|
||||
}
|
||||
if job.Status != domain.ClaimStatusRunning || job.AttemptCount != 9 || job.MaxAttempts != 9 || job.NextRetryAtMS != 0 ||
|
||||
job.WalletCommandID != "wallet-command-never-changes" {
|
||||
t.Fatalf("manual retry job = %+v", job)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sql expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityTemplateRewardRetryBackoffIsDeterministicBoundedAndIncreasing(t *testing.T) {
|
||||
nowMS := int64(1_000_000)
|
||||
previous := nowMS
|
||||
for attempt := int32(1); attempt <= activityTemplateRewardJobDefaultAttempts; attempt++ {
|
||||
first := activityTemplateRewardRetryAtMS("job-stable", attempt, nowMS)
|
||||
second := activityTemplateRewardRetryAtMS("job-stable", attempt, nowMS)
|
||||
if first != second {
|
||||
t.Fatalf("attempt %d jitter is not deterministic: %d != %d", attempt, first, second)
|
||||
}
|
||||
if first <= previous {
|
||||
t.Fatalf("attempt %d retry %d did not increase after %d", attempt, first, previous)
|
||||
}
|
||||
if first-nowMS > activityTemplateRewardJobRetryHardCap.Milliseconds() {
|
||||
t.Fatalf("attempt %d delay %d exceeds hard cap", attempt, first-nowMS)
|
||||
}
|
||||
previous = first
|
||||
}
|
||||
if capped := activityTemplateRewardRetryAtMS("job-stable", 100, nowMS) - nowMS; capped > activityTemplateRewardJobRetryHardCap.Milliseconds() {
|
||||
t.Fatalf("high-attempt delay %d exceeds hard cap", capped)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,100 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"hyapp/pkg/xerr"
|
||||
domain "hyapp/services/activity-service/internal/domain/activitytemplate"
|
||||
)
|
||||
|
||||
func (r *Repository) ClaimActivityTemplateStatsOutbox(ctx context.Context, workerID string, nowMS int64, lockTTL time.Duration, batchSize int) ([]domain.StatsOutboxRecord, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
}
|
||||
if batchSize <= 0 {
|
||||
batchSize = 100
|
||||
}
|
||||
if lockTTL <= 0 {
|
||||
lockTTL = 30 * time.Second
|
||||
}
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
rows, err := tx.QueryContext(ctx, `
|
||||
SELECT outbox_id, event_type, payload, retry_count
|
||||
FROM activity_template_stats_outbox
|
||||
WHERE ((status IN (?, ?) AND next_retry_at_ms <= ?) OR (status = ? AND lock_until_ms <= ?))
|
||||
ORDER BY created_at_ms, outbox_id
|
||||
LIMIT ? FOR UPDATE SKIP LOCKED`, outboxStatusPending, outboxStatusFailed, nowMS, outboxStatusRunning, nowMS, batchSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]domain.StatsOutboxRecord, 0, batchSize)
|
||||
for rows.Next() {
|
||||
var item domain.StatsOutboxRecord
|
||||
if err := rows.Scan(&item.OutboxID, &item.EventType, &item.Payload, &item.RetryCount); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lockUntil := nowMS + lockTTL.Milliseconds()
|
||||
for _, item := range items {
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
UPDATE activity_template_stats_outbox
|
||||
SET status = ?, locked_by = ?, lock_until_ms = ?, updated_at_ms = ?
|
||||
WHERE outbox_id = ?`, outboxStatusRunning, workerID, lockUntil, nowMS, item.OutboxID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *Repository) MarkActivityTemplateStatsOutboxSent(ctx context.Context, outboxID string, nowMS int64) error {
|
||||
_, err := r.db.ExecContext(ctx, `
|
||||
UPDATE activity_template_stats_outbox
|
||||
SET status = ?, locked_by = '', lock_until_ms = 0, last_error = '', updated_at_ms = ?
|
||||
WHERE outbox_id = ?`, outboxStatusSent, nowMS, outboxID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repository) MarkActivityTemplateStatsOutboxFailed(ctx context.Context, outboxID, reason string, nowMS int64) error {
|
||||
var retryCount int32
|
||||
err := r.db.QueryRowContext(ctx, `SELECT retry_count FROM activity_template_stats_outbox WHERE outbox_id = ?`, outboxID).Scan(&retryCount)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
retryCount++
|
||||
backoff := time.Second * time.Duration(1<<minInt32(retryCount, 8))
|
||||
_, err = r.db.ExecContext(ctx, `
|
||||
UPDATE activity_template_stats_outbox
|
||||
SET status = ?, retry_count = ?, next_retry_at_ms = ?, locked_by = '', lock_until_ms = 0,
|
||||
last_error = ?, updated_at_ms = ?
|
||||
WHERE outbox_id = ?`, outboxStatusFailed, retryCount, nowMS+backoff.Milliseconds(),
|
||||
truncateActivityTemplateRuntimeError(reason), nowMS, outboxID)
|
||||
return err
|
||||
}
|
||||
|
||||
func minInt32(left, right int32) int32 {
|
||||
if left < right {
|
||||
return left
|
||||
}
|
||||
return right
|
||||
}
|
||||
@ -16,7 +16,27 @@ import (
|
||||
|
||||
// Repository 是 activity-service 的 MySQL 存储入口。
|
||||
type Repository struct {
|
||||
db *sql.DB
|
||||
db *sql.DB
|
||||
activityTemplateSettlementLateness time.Duration
|
||||
}
|
||||
|
||||
const defaultActivityTemplateSettlementLateness = 10 * time.Minute
|
||||
|
||||
func (r *Repository) SetActivityTemplateSettlementLateness(value time.Duration) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
if value <= 0 {
|
||||
value = defaultActivityTemplateSettlementLateness
|
||||
}
|
||||
r.activityTemplateSettlementLateness = value
|
||||
}
|
||||
|
||||
func (r *Repository) activityTemplateSettlementLatenessMS() int64 {
|
||||
if r == nil || r.activityTemplateSettlementLateness <= 0 {
|
||||
return defaultActivityTemplateSettlementLateness.Milliseconds()
|
||||
}
|
||||
return r.activityTemplateSettlementLateness.Milliseconds()
|
||||
}
|
||||
|
||||
// Open 创建 MySQL 连接池并做启动 ping。
|
||||
@ -63,6 +83,12 @@ func (r *Repository) Migrate(ctx context.Context) error {
|
||||
if err := r.ensureAgencyOpeningTables(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureActivityTemplateTables(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureActivityTemplateRuntimeTables(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureWheelTables(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -0,0 +1,84 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/xerr"
|
||||
domain "hyapp/services/activity-service/internal/domain/activitytemplate"
|
||||
)
|
||||
|
||||
func (s *AdminActivityTemplateServer) ListActivityTemplateLeaderboard(ctx context.Context, req *activityv1.AdminListActivityTemplateLeaderboardRequest) (*activityv1.AdminListActivityTemplateLeaderboardResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
result, err := s.svc.AdminListRuntimeLeaderboard(ctx, domain.LeaderboardQuery{
|
||||
TemplateID: req.GetTemplateId(), VersionNo: req.GetVersionNo(), BoardType: req.GetBoardType(),
|
||||
PeriodKey: req.GetPeriodKey(), Page: req.GetPage(), PageSize: req.GetPageSize(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
resp := &activityv1.AdminListActivityTemplateLeaderboardResponse{
|
||||
Period: &activityv1.ActivityTemplateRankPeriod{
|
||||
TemplateId: result.Period.TemplateID, TemplateCode: result.Period.TemplateCode,
|
||||
VersionNo: result.Period.VersionNo, BoardType: result.Period.BoardType, PeriodKey: result.Period.PeriodKey,
|
||||
PeriodStartMs: result.Period.StartMS, PeriodEndMs: result.Period.EndMS,
|
||||
Status: result.Period.Status, SettledAtMs: result.Period.SettledAtMS, SettleAfterMs: result.Period.SettleAfterMS,
|
||||
},
|
||||
Entries: make([]*activityv1.ActivityTemplateLeaderboardEntry, 0, len(result.Entries)), Total: result.Total,
|
||||
}
|
||||
for _, item := range result.Entries {
|
||||
resp.Entries = append(resp.Entries, activityTemplateLeaderboardEntryToProto(item))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *AdminActivityTemplateServer) ListActivityTemplateRewardJobs(ctx context.Context, req *activityv1.ListActivityTemplateRewardJobsRequest) (*activityv1.ListActivityTemplateRewardJobsResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
items, total, err := s.svc.ListRewardJobs(ctx, domain.RewardJobQuery{
|
||||
TemplateID: req.GetTemplateId(), VersionNo: req.GetVersionNo(), BoardType: req.GetBoardType(),
|
||||
PeriodKey: req.GetPeriodKey(), Status: req.GetStatus(), Page: req.GetPage(), PageSize: req.GetPageSize(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
resp := &activityv1.ListActivityTemplateRewardJobsResponse{Jobs: make([]*activityv1.ActivityTemplateRewardJob, 0, len(items)), Total: total}
|
||||
for _, item := range items {
|
||||
resp.Jobs = append(resp.Jobs, activityTemplateRewardJobToProto(item))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *AdminActivityTemplateServer) RetryActivityTemplateRewardJob(ctx context.Context, req *activityv1.RetryActivityTemplateRewardJobRequest) (*activityv1.RetryActivityTemplateRewardJobResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
item, err := s.svc.RetryRewardJob(ctx, req.GetTemplateId(), req.GetRewardJobId(), req.GetOperatorAdminId())
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return &activityv1.RetryActivityTemplateRewardJobResponse{Job: activityTemplateRewardJobToProto(item)}, nil
|
||||
}
|
||||
|
||||
func (s *AdminActivityTemplateServer) ListActivityTemplateTaskClaims(ctx context.Context, req *activityv1.ListActivityTemplateTaskClaimsRequest) (*activityv1.ListActivityTemplateTaskClaimsResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
items, total, err := s.svc.ListTaskClaims(ctx, domain.TaskClaimQuery{
|
||||
TemplateID: req.GetTemplateId(), VersionNo: req.GetVersionNo(), TaskDay: req.GetTaskDay(), TaskKey: req.GetTaskKey(),
|
||||
UserID: req.GetUserId(), Status: req.GetStatus(), Page: req.GetPage(), PageSize: req.GetPageSize(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
resp := &activityv1.ListActivityTemplateTaskClaimsResponse{Claims: make([]*activityv1.ActivityTemplateTaskClaim, 0, len(items)), Total: total}
|
||||
for _, item := range items {
|
||||
resp.Claims = append(resp.Claims, activityTemplateTaskClaimToProto(item))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *AdminActivityTemplateServer) RetryActivityTemplateTaskClaim(ctx context.Context, req *activityv1.RetryActivityTemplateTaskClaimRequest) (*activityv1.RetryActivityTemplateTaskClaimResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
item, err := s.svc.RetryTaskClaim(ctx, req.GetTemplateId(), req.GetClaimId(), req.GetOperatorAdminId())
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return &activityv1.RetryActivityTemplateTaskClaimResponse{Claim: activityTemplateTaskClaimToProto(item)}, nil
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
domain "hyapp/services/activity-service/internal/domain/activitytemplate"
|
||||
)
|
||||
|
||||
func TestActivityTemplateRuntimeResponseFillsActualLifecycle(t *testing.T) {
|
||||
nowMS := time.Now().UTC().UnixMilli()
|
||||
tests := []struct {
|
||||
name string
|
||||
runtime domain.Runtime
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "scheduled",
|
||||
runtime: domain.Runtime{
|
||||
Template: domain.Template{TemplateID: "scheduled", Status: domain.StatusPublished, StartMS: nowMS + 60_000, EndMS: nowMS + 120_000},
|
||||
VersionNo: 1, RuntimeFromMS: nowMS + 60_000,
|
||||
},
|
||||
want: domain.LifecycleScheduled,
|
||||
},
|
||||
{
|
||||
name: "ongoing",
|
||||
runtime: domain.Runtime{
|
||||
Template: domain.Template{TemplateID: "ongoing", Status: domain.StatusPublished, StartMS: nowMS - 60_000, EndMS: nowMS + 60_000},
|
||||
VersionNo: 2, RuntimeFromMS: nowMS - 60_000,
|
||||
},
|
||||
want: domain.LifecycleOngoing,
|
||||
},
|
||||
{
|
||||
name: "ended",
|
||||
runtime: domain.Runtime{
|
||||
Template: domain.Template{TemplateID: "ended", Status: domain.StatusPublished, StartMS: nowMS - 120_000, EndMS: nowMS - 60_000},
|
||||
VersionNo: 3, RuntimeFromMS: nowMS - 120_000,
|
||||
},
|
||||
want: domain.LifecycleEnded,
|
||||
},
|
||||
{
|
||||
name: "disabled",
|
||||
runtime: domain.Runtime{
|
||||
Template: domain.Template{TemplateID: "disabled", Status: domain.StatusPublished, StartMS: nowMS - 120_000, EndMS: nowMS + 120_000},
|
||||
VersionNo: 4, RuntimeFromMS: nowMS - 120_000, RuntimeToMS: nowMS - 60_000,
|
||||
},
|
||||
want: domain.LifecycleDisabled,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := activityTemplateRuntimeToProto(tt.runtime)
|
||||
if got == nil || got.GetTemplate().GetLifecycleStatus() != tt.want {
|
||||
t.Fatalf("runtime response lifecycle = %+v, want %q", got, tt.want)
|
||||
}
|
||||
if got.GetRuntimeFromMs() != tt.runtime.RuntimeFromMS || got.GetRuntimeToMs() != tt.runtime.RuntimeToMS || got.GetServerTimeMs() <= 0 {
|
||||
t.Fatalf("runtime response window/server time = %+v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,166 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/xerr"
|
||||
domain "hyapp/services/activity-service/internal/domain/activitytemplate"
|
||||
service "hyapp/services/activity-service/internal/service/activitytemplate"
|
||||
)
|
||||
|
||||
type ActivityTemplateRuntimeServer struct {
|
||||
activityv1.UnimplementedActivityTemplateRuntimeServiceServer
|
||||
svc *service.Service
|
||||
}
|
||||
|
||||
func NewActivityTemplateRuntimeServer(svc *service.Service) *ActivityTemplateRuntimeServer {
|
||||
return &ActivityTemplateRuntimeServer{svc: svc}
|
||||
}
|
||||
|
||||
func (s *ActivityTemplateRuntimeServer) GetCurrentActivityTemplate(ctx context.Context, req *activityv1.GetCurrentActivityTemplateRequest) (*activityv1.GetCurrentActivityTemplateResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
runtime, found, err := s.svc.GetCurrentRuntime(ctx, req.GetRegionId(), "")
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
if !found {
|
||||
return &activityv1.GetCurrentActivityTemplateResponse{Found: false}, nil
|
||||
}
|
||||
return &activityv1.GetCurrentActivityTemplateResponse{Runtime: activityTemplateRuntimeToProto(runtime), Found: true}, nil
|
||||
}
|
||||
|
||||
func (s *ActivityTemplateRuntimeServer) VisitActivityTemplate(ctx context.Context, req *activityv1.VisitActivityTemplateRequest) (*activityv1.VisitActivityTemplateResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
result, err := s.svc.Visit(ctx, domain.VisitCommand{
|
||||
UserID: req.GetUserId(), RegionID: req.GetRegionId(), TemplateCode: req.GetTemplateCode(), CommandID: req.GetCommandId(), VersionNo: req.GetVersionNo(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return &activityv1.VisitActivityTemplateResponse{
|
||||
Runtime: activityTemplateRuntimeToProto(result.Runtime), Tasks: activityTemplateTasksToProto(result.Tasks), FirstVisit: result.FirstVisit,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ActivityTemplateRuntimeServer) ListActivityTemplateTasks(ctx context.Context, req *activityv1.ListActivityTemplateTasksRequest) (*activityv1.ListActivityTemplateTasksResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
runtime, items, nextRefreshMS, err := s.svc.ListRuntimeTasks(ctx, req.GetUserId(), req.GetRegionId(), req.GetVersionNo(), req.GetTemplateCode(), req.GetTaskDay())
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return &activityv1.ListActivityTemplateTasksResponse{
|
||||
Runtime: activityTemplateRuntimeToProto(runtime), Tasks: activityTemplateTasksToProto(items), NextRefreshAtMs: nextRefreshMS,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ActivityTemplateRuntimeServer) ClaimActivityTemplateTaskReward(ctx context.Context, req *activityv1.ClaimActivityTemplateTaskRewardRequest) (*activityv1.ClaimActivityTemplateTaskRewardResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
claim, err := s.svc.ClaimRuntimeTaskReward(ctx, domain.ClaimCommand{
|
||||
UserID: req.GetUserId(), RegionID: req.GetRegionId(), TemplateCode: req.GetTemplateCode(), VersionNo: req.GetVersionNo(), TaskDay: req.GetTaskDay(),
|
||||
TaskKey: req.GetTaskKey(), CommandID: req.GetCommandId(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return &activityv1.ClaimActivityTemplateTaskRewardResponse{Claim: activityTemplateTaskClaimToProto(claim), Claimed: claim.Status == domain.ClaimStatusGranted}, nil
|
||||
}
|
||||
|
||||
func (s *ActivityTemplateRuntimeServer) ListActivityTemplateLeaderboard(ctx context.Context, req *activityv1.ListActivityTemplateLeaderboardRequest) (*activityv1.ListActivityTemplateLeaderboardResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
result, err := s.svc.ListRuntimeLeaderboard(ctx, domain.LeaderboardQuery{
|
||||
UserID: req.GetUserId(), RegionID: req.GetRegionId(), TemplateCode: req.GetTemplateCode(), VersionNo: req.GetVersionNo(), BoardType: req.GetBoardType(),
|
||||
PeriodKey: req.GetPeriodKey(), Page: req.GetPage(), PageSize: req.GetPageSize(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return activityTemplateLeaderboardResultToProto(result), nil
|
||||
}
|
||||
|
||||
func activityTemplateRuntimeToProto(item domain.Runtime) *activityv1.ActivityTemplateRuntime {
|
||||
if item.Template.TemplateID == "" {
|
||||
return nil
|
||||
}
|
||||
nowMS := time.Now().UTC().UnixMilli()
|
||||
item.Template.LifecycleStatus = service.RuntimeLifecycleStatus(item.Template, item.RuntimeFromMS, item.RuntimeToMS, nowMS)
|
||||
return &activityv1.ActivityTemplateRuntime{
|
||||
Template: activityTemplateToProto(item.Template), VersionNo: item.VersionNo,
|
||||
RuntimeFromMs: item.RuntimeFromMS, RuntimeToMs: item.RuntimeToMS, ServerTimeMs: nowMS,
|
||||
}
|
||||
}
|
||||
|
||||
func activityTemplateTasksToProto(items []domain.TaskProgress) []*activityv1.ActivityTemplateRuntimeTask {
|
||||
out := make([]*activityv1.ActivityTemplateRuntimeTask, 0, len(items))
|
||||
for _, item := range items {
|
||||
out = append(out, &activityv1.ActivityTemplateRuntimeTask{
|
||||
TemplateId: item.TemplateID, TemplateCode: item.TemplateCode, VersionNo: item.VersionNo,
|
||||
TaskDay: item.TaskDay, TaskKey: item.TaskKey, TaskType: item.TaskType, TargetValue: item.TargetValue,
|
||||
ProgressValue: item.ProgressValue, RewardResourceGroupId: item.RewardResourceGroupID,
|
||||
Status: item.Status, Claimable: item.Status == domain.TaskStatusCompleted,
|
||||
CompletedAtMs: item.CompletedAtMS, ClaimedAtMs: item.ClaimedAtMS, SortOrder: item.SortOrder,
|
||||
RewardSnapshotId: item.RewardSnapshot.SnapshotID, RewardSnapshotHash: item.RewardSnapshot.SnapshotHash,
|
||||
RewardName: item.RewardSnapshot.Name, RewardItems: activityTemplateRewardItemsToProto(item.RewardSnapshot.Items),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func activityTemplateTaskClaimToProto(item domain.TaskClaim) *activityv1.ActivityTemplateTaskClaim {
|
||||
if item.ClaimID == "" {
|
||||
return nil
|
||||
}
|
||||
return &activityv1.ActivityTemplateTaskClaim{
|
||||
ClaimId: item.ClaimID, CommandId: item.CommandID, TemplateId: item.TemplateID, TemplateCode: item.TemplateCode,
|
||||
VersionNo: item.VersionNo, TaskDay: item.TaskDay, TaskKey: item.TaskKey, UserId: item.UserID,
|
||||
RewardResourceGroupId: item.RewardResourceGroupID, WalletCommandId: item.WalletCommandID,
|
||||
WalletGrantId: item.WalletGrantID, Status: item.Status, FailureReason: item.FailureReason,
|
||||
AttemptCount: item.AttemptCount, CreatedAtMs: item.CreatedAtMS, UpdatedAtMs: item.UpdatedAtMS, GrantedAtMs: item.GrantedAtMS,
|
||||
RewardSnapshotId: item.RewardSnapshot.SnapshotID, RewardName: item.RewardSnapshot.Name,
|
||||
RewardSnapshotHash: item.RewardSnapshot.SnapshotHash, EarningRegionId: item.EarningRegionID,
|
||||
AttributionAtMs: item.AttributionAtMS,
|
||||
RewardItems: activityTemplateRewardItemsToProto(item.RewardSnapshot.Items),
|
||||
}
|
||||
}
|
||||
|
||||
func activityTemplateLeaderboardResultToProto(result domain.LeaderboardResult) *activityv1.ListActivityTemplateLeaderboardResponse {
|
||||
resp := &activityv1.ListActivityTemplateLeaderboardResponse{
|
||||
Runtime: activityTemplateRuntimeToProto(result.Runtime), BoardType: result.Period.BoardType, PeriodKey: result.Period.PeriodKey,
|
||||
PeriodStartMs: result.Period.StartMS, PeriodEndMs: result.Period.EndMS, PeriodStatus: result.Period.Status,
|
||||
Entries: make([]*activityv1.ActivityTemplateLeaderboardEntry, 0, len(result.Entries)), Total: result.Total,
|
||||
SettleAfterMs: result.Period.SettleAfterMS,
|
||||
}
|
||||
for _, item := range result.Entries {
|
||||
resp.Entries = append(resp.Entries, activityTemplateLeaderboardEntryToProto(item))
|
||||
}
|
||||
if result.MyFound {
|
||||
resp.MyEntry = activityTemplateLeaderboardEntryToProto(result.MyEntry)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func activityTemplateLeaderboardEntryToProto(item domain.LeaderboardEntry) *activityv1.ActivityTemplateLeaderboardEntry {
|
||||
return &activityv1.ActivityTemplateLeaderboardEntry{
|
||||
RankNo: item.RankNo, UserId: item.UserID, Score: item.Score, GiftCount: item.GiftCount,
|
||||
CoinAmount: item.CoinAmount, FirstScoredAtMs: item.FirstScoredAtMS, Snapshot: item.Snapshot,
|
||||
}
|
||||
}
|
||||
|
||||
func activityTemplateRewardJobToProto(item domain.RewardJob) *activityv1.ActivityTemplateRewardJob {
|
||||
if item.RewardJobID == "" {
|
||||
return nil
|
||||
}
|
||||
return &activityv1.ActivityTemplateRewardJob{
|
||||
RewardJobId: item.RewardJobID, TemplateId: item.TemplateID, TemplateCode: item.TemplateCode,
|
||||
VersionNo: item.VersionNo, BoardType: item.BoardType, PeriodKey: item.PeriodKey,
|
||||
RankNo: item.RankNo, UserId: item.UserID, Score: item.Score, ResourceGroupId: item.ResourceGroupID,
|
||||
WalletCommandId: item.WalletCommandID, WalletGrantId: item.WalletGrantID, Status: item.Status,
|
||||
FailureReason: item.FailureReason, AttemptCount: item.AttemptCount, CreatedAtMs: item.CreatedAtMS,
|
||||
UpdatedAtMs: item.UpdatedAtMS, GrantedAtMs: item.GrantedAtMS,
|
||||
RewardSnapshotId: item.RewardSnapshot.SnapshotID, RewardSnapshotHash: item.RewardSnapshot.SnapshotHash,
|
||||
RewardName: item.RewardSnapshot.Name, RewardItems: activityTemplateRewardItemsToProto(item.RewardSnapshot.Items),
|
||||
MaxAttempts: item.MaxAttempts, NextRetryAtMs: item.NextRetryAtMS,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,325 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/xerr"
|
||||
domain "hyapp/services/activity-service/internal/domain/activitytemplate"
|
||||
service "hyapp/services/activity-service/internal/service/activitytemplate"
|
||||
)
|
||||
|
||||
// AdminActivityTemplateServer 只把 protobuf 转为领域命令;发布规则、版本和事务都由 service/repository 拥有。
|
||||
type AdminActivityTemplateServer struct {
|
||||
activityv1.UnimplementedAdminActivityTemplateServiceServer
|
||||
svc *service.Service
|
||||
}
|
||||
|
||||
func NewAdminActivityTemplateServer(svc *service.Service) *AdminActivityTemplateServer {
|
||||
return &AdminActivityTemplateServer{svc: svc}
|
||||
}
|
||||
|
||||
func (s *AdminActivityTemplateServer) ListActivityTemplates(ctx context.Context, req *activityv1.ListActivityTemplatesRequest) (*activityv1.ListActivityTemplatesResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
query := domain.ListQuery{
|
||||
Keyword: req.GetKeyword(),
|
||||
Status: req.GetStatus(),
|
||||
StartMS: req.GetStartMs(),
|
||||
EndMS: req.GetEndMs(),
|
||||
Page: req.GetPage(),
|
||||
PageSize: req.GetPageSize(),
|
||||
}
|
||||
if req.RegionId != nil {
|
||||
value := req.GetRegionId()
|
||||
query.RegionID = &value
|
||||
}
|
||||
if req.AllRegions != nil {
|
||||
value := req.GetAllRegions()
|
||||
query.AllRegions = &value
|
||||
}
|
||||
items, total, err := s.svc.List(ctx, query)
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
resp := &activityv1.ListActivityTemplatesResponse{
|
||||
Templates: make([]*activityv1.ActivityTemplateSummary, 0, len(items)),
|
||||
Total: total,
|
||||
ServerTimeMs: time.Now().UTC().UnixMilli(),
|
||||
}
|
||||
for _, item := range items {
|
||||
resp.Templates = append(resp.Templates, activityTemplateSummaryToProto(item))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *AdminActivityTemplateServer) CreateActivityTemplate(ctx context.Context, req *activityv1.CreateActivityTemplateRequest) (*activityv1.CreateActivityTemplateResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
item, err := s.svc.Create(ctx, activityTemplateFromProto(req.GetTemplate()), req.GetOperatorAdminId())
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return &activityv1.CreateActivityTemplateResponse{Template: activityTemplateToProto(item)}, nil
|
||||
}
|
||||
|
||||
func (s *AdminActivityTemplateServer) GetActivityTemplate(ctx context.Context, req *activityv1.GetActivityTemplateRequest) (*activityv1.GetActivityTemplateResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
item, err := s.svc.Get(ctx, req.GetTemplateId())
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return &activityv1.GetActivityTemplateResponse{Template: activityTemplateToProto(item)}, nil
|
||||
}
|
||||
|
||||
func (s *AdminActivityTemplateServer) UpdateActivityTemplate(ctx context.Context, req *activityv1.UpdateActivityTemplateRequest) (*activityv1.UpdateActivityTemplateResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
item, err := s.svc.Update(ctx, domain.UpdateCommand{
|
||||
Template: activityTemplateFromProto(req.GetTemplate()),
|
||||
ExpectedRevision: req.GetExpectedRevision(),
|
||||
OperatorAdminID: req.GetOperatorAdminId(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return &activityv1.UpdateActivityTemplateResponse{Template: activityTemplateToProto(item)}, nil
|
||||
}
|
||||
|
||||
func (s *AdminActivityTemplateServer) SetActivityTemplateStatus(ctx context.Context, req *activityv1.SetActivityTemplateStatusRequest) (*activityv1.SetActivityTemplateStatusResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
item, err := s.svc.SetStatus(ctx, domain.StatusCommand{
|
||||
TemplateID: req.GetTemplateId(),
|
||||
Status: req.GetStatus(),
|
||||
ExpectedRevision: req.GetExpectedRevision(),
|
||||
OperatorAdminID: req.GetOperatorAdminId(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return &activityv1.SetActivityTemplateStatusResponse{Template: activityTemplateToProto(item)}, nil
|
||||
}
|
||||
|
||||
func (s *AdminActivityTemplateServer) DeleteActivityTemplate(ctx context.Context, req *activityv1.DeleteActivityTemplateRequest) (*activityv1.DeleteActivityTemplateResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
item, archived, err := s.svc.Delete(ctx, domain.StatusCommand{
|
||||
TemplateID: req.GetTemplateId(),
|
||||
ExpectedRevision: req.GetExpectedRevision(),
|
||||
OperatorAdminID: req.GetOperatorAdminId(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return &activityv1.DeleteActivityTemplateResponse{Template: activityTemplateToProto(item), Archived: archived}, nil
|
||||
}
|
||||
|
||||
func (s *AdminActivityTemplateServer) ListActivityTemplateVersions(ctx context.Context, req *activityv1.ListActivityTemplateVersionsRequest) (*activityv1.ListActivityTemplateVersionsResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
items, total, err := s.svc.ListVersions(ctx, req.GetTemplateId(), req.GetPage(), req.GetPageSize())
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
resp := &activityv1.ListActivityTemplateVersionsResponse{Versions: make([]*activityv1.ActivityTemplateVersion, 0, len(items)), Total: total}
|
||||
for _, item := range items {
|
||||
resp.Versions = append(resp.Versions, &activityv1.ActivityTemplateVersion{
|
||||
TemplateId: item.TemplateID,
|
||||
VersionNo: item.VersionNo,
|
||||
Snapshot: activityTemplateToProto(item.Snapshot),
|
||||
PublishedByAdminId: item.PublishedByAdminID,
|
||||
PublishedAtMs: item.PublishedAtMS,
|
||||
RuntimeFromMs: item.RuntimeFromMS,
|
||||
RuntimeToMs: item.RuntimeToMS,
|
||||
})
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *AdminActivityTemplateServer) CloneActivityTemplate(ctx context.Context, req *activityv1.CloneActivityTemplateRequest) (*activityv1.CloneActivityTemplateResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
item, err := s.svc.Clone(ctx, domain.CloneCommand{
|
||||
SourceTemplateID: req.GetSourceTemplateId(),
|
||||
SourceVersion: req.GetSourceVersion(),
|
||||
TemplateCode: req.GetTemplateCode(),
|
||||
Name: req.GetName(),
|
||||
OperatorAdminID: req.GetOperatorAdminId(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return &activityv1.CloneActivityTemplateResponse{Template: activityTemplateToProto(item)}, nil
|
||||
}
|
||||
|
||||
func (s *AdminActivityTemplateServer) ValidateActivityTemplate(ctx context.Context, req *activityv1.ValidateActivityTemplateRequest) (*activityv1.ValidateActivityTemplateResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
issues, err := s.svc.Validate(ctx, activityTemplateFromProto(req.GetTemplate()), req.GetForPublish())
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
resp := &activityv1.ValidateActivityTemplateResponse{Valid: len(issues) == 0, Issues: make([]*activityv1.ActivityTemplateValidationIssue, 0, len(issues))}
|
||||
for _, issue := range issues {
|
||||
resp.Issues = append(resp.Issues, &activityv1.ActivityTemplateValidationIssue{Field: issue.Field, Code: issue.Code, Message: issue.Message})
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func activityTemplateFromProto(item *activityv1.ActivityTemplate) domain.Template {
|
||||
if item == nil {
|
||||
return domain.Template{}
|
||||
}
|
||||
out := domain.Template{
|
||||
AppCode: item.GetAppCode(),
|
||||
TemplateID: item.GetTemplateId(),
|
||||
TemplateCode: item.GetTemplateCode(),
|
||||
Name: item.GetName(),
|
||||
ActivityType: item.GetActivityType(),
|
||||
Status: item.GetStatus(),
|
||||
LifecycleStatus: item.GetLifecycleStatus(),
|
||||
StartMS: item.GetStartMs(),
|
||||
EndMS: item.GetEndMs(),
|
||||
AllRegions: item.GetAllRegions(),
|
||||
RegionIDs: append([]int64(nil), item.GetRegionIds()...),
|
||||
DailyLeaderboardSize: item.GetDailyLeaderboardSize(),
|
||||
TotalLeaderboardSize: item.GetTotalLeaderboardSize(),
|
||||
DisplayConfigJSON: item.GetDisplayConfigJson(),
|
||||
Revision: item.GetRevision(),
|
||||
PublishedVersion: item.GetPublishedVersion(),
|
||||
CreatedByAdminID: item.GetCreatedByAdminId(),
|
||||
UpdatedByAdminID: item.GetUpdatedByAdminId(),
|
||||
PublishedByAdminID: item.GetPublishedByAdminId(),
|
||||
CreatedAtMS: item.GetCreatedAtMs(),
|
||||
UpdatedAtMS: item.GetUpdatedAtMs(),
|
||||
PublishedAtMS: item.GetPublishedAtMs(),
|
||||
ArchivedAtMS: item.GetArchivedAtMs(),
|
||||
}
|
||||
for _, locale := range item.GetLocales() {
|
||||
out.Locales = append(out.Locales, domain.Locale{Locale: locale.GetLocale(), Title: locale.GetTitle(), Rules: locale.GetRules()})
|
||||
}
|
||||
for _, gift := range item.GetGifts() {
|
||||
out.Gifts = append(out.Gifts, domain.Gift{
|
||||
GiftID: gift.GetGiftId(), SortOrder: gift.GetSortOrder(), Name: gift.GetName(), IconURL: gift.GetIconUrl(),
|
||||
CoinPrice: gift.GetCoinPrice(), PriceVersion: gift.GetPriceVersion(),
|
||||
})
|
||||
}
|
||||
for _, task := range item.GetTasks() {
|
||||
out.Tasks = append(out.Tasks, domain.Task{
|
||||
TaskKey: task.GetTaskKey(), TaskType: task.GetTaskType(), TargetValue: task.GetTargetValue(),
|
||||
RewardResourceGroupID: task.GetRewardResourceGroupId(), SortOrder: task.GetSortOrder(),
|
||||
RewardSnapshot: activityTemplateRewardSnapshotFromProto(task.GetRewardSnapshotId(), task.GetRewardSnapshotHash(), task.GetRewardName(), task.GetRewardItems()),
|
||||
})
|
||||
}
|
||||
for _, reward := range item.GetRankRewards() {
|
||||
out.RankRewards = append(out.RankRewards, domain.RankReward{
|
||||
BoardType: reward.GetBoardType(), RankFrom: reward.GetRankFrom(), RankTo: reward.GetRankTo(),
|
||||
ResourceGroupID: reward.GetResourceGroupId(), SortOrder: reward.GetSortOrder(),
|
||||
RewardSnapshot: activityTemplateRewardSnapshotFromProto(reward.GetRewardSnapshotId(), reward.GetRewardSnapshotHash(), reward.GetRewardName(), reward.GetRewardItems()),
|
||||
})
|
||||
}
|
||||
for _, asset := range item.GetAssets() {
|
||||
out.Assets = append(out.Assets, domain.Asset{
|
||||
AssetKey: asset.GetAssetKey(), Locale: asset.GetLocale(), URL: asset.GetUrl(),
|
||||
MediaType: asset.GetMediaType(), SortOrder: asset.GetSortOrder(),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func activityTemplateToProto(item domain.Template) *activityv1.ActivityTemplate {
|
||||
if item.TemplateID == "" && item.TemplateCode == "" && item.Name == "" {
|
||||
return nil
|
||||
}
|
||||
out := &activityv1.ActivityTemplate{
|
||||
AppCode: item.AppCode,
|
||||
TemplateId: item.TemplateID,
|
||||
TemplateCode: item.TemplateCode,
|
||||
Name: item.Name,
|
||||
ActivityType: item.ActivityType,
|
||||
Status: item.Status,
|
||||
LifecycleStatus: item.LifecycleStatus,
|
||||
StartMs: item.StartMS,
|
||||
EndMs: item.EndMS,
|
||||
AllRegions: item.AllRegions,
|
||||
RegionIds: append([]int64(nil), item.RegionIDs...),
|
||||
Locales: make([]*activityv1.ActivityTemplateLocale, 0, len(item.Locales)),
|
||||
Gifts: make([]*activityv1.ActivityTemplateGift, 0, len(item.Gifts)),
|
||||
Tasks: make([]*activityv1.ActivityTemplateTask, 0, len(item.Tasks)),
|
||||
DailyLeaderboardSize: item.DailyLeaderboardSize,
|
||||
TotalLeaderboardSize: item.TotalLeaderboardSize,
|
||||
RankRewards: make([]*activityv1.ActivityTemplateRankReward, 0, len(item.RankRewards)),
|
||||
Assets: make([]*activityv1.ActivityTemplateAsset, 0, len(item.Assets)),
|
||||
DisplayConfigJson: item.DisplayConfigJSON,
|
||||
Revision: item.Revision,
|
||||
PublishedVersion: item.PublishedVersion,
|
||||
CreatedByAdminId: item.CreatedByAdminID,
|
||||
UpdatedByAdminId: item.UpdatedByAdminID,
|
||||
PublishedByAdminId: item.PublishedByAdminID,
|
||||
CreatedAtMs: item.CreatedAtMS,
|
||||
UpdatedAtMs: item.UpdatedAtMS,
|
||||
PublishedAtMs: item.PublishedAtMS,
|
||||
ArchivedAtMs: item.ArchivedAtMS,
|
||||
}
|
||||
for _, locale := range item.Locales {
|
||||
out.Locales = append(out.Locales, &activityv1.ActivityTemplateLocale{Locale: locale.Locale, Title: locale.Title, Rules: locale.Rules})
|
||||
}
|
||||
for _, gift := range item.Gifts {
|
||||
out.Gifts = append(out.Gifts, &activityv1.ActivityTemplateGift{
|
||||
GiftId: gift.GiftID, SortOrder: gift.SortOrder, Name: gift.Name, IconUrl: gift.IconURL,
|
||||
CoinPrice: gift.CoinPrice, PriceVersion: gift.PriceVersion,
|
||||
})
|
||||
}
|
||||
for _, task := range item.Tasks {
|
||||
out.Tasks = append(out.Tasks, &activityv1.ActivityTemplateTask{
|
||||
TaskKey: task.TaskKey, TaskType: task.TaskType, TargetValue: task.TargetValue,
|
||||
RewardResourceGroupId: task.RewardResourceGroupID, SortOrder: task.SortOrder,
|
||||
RewardSnapshotId: task.RewardSnapshot.SnapshotID, RewardSnapshotHash: task.RewardSnapshot.SnapshotHash,
|
||||
RewardName: task.RewardSnapshot.Name, RewardItems: activityTemplateRewardItemsToProto(task.RewardSnapshot.Items),
|
||||
})
|
||||
}
|
||||
for _, reward := range item.RankRewards {
|
||||
out.RankRewards = append(out.RankRewards, &activityv1.ActivityTemplateRankReward{
|
||||
BoardType: reward.BoardType, RankFrom: reward.RankFrom, RankTo: reward.RankTo,
|
||||
ResourceGroupId: reward.ResourceGroupID, SortOrder: reward.SortOrder,
|
||||
RewardSnapshotId: reward.RewardSnapshot.SnapshotID, RewardSnapshotHash: reward.RewardSnapshot.SnapshotHash,
|
||||
RewardName: reward.RewardSnapshot.Name, RewardItems: activityTemplateRewardItemsToProto(reward.RewardSnapshot.Items),
|
||||
})
|
||||
}
|
||||
for _, asset := range item.Assets {
|
||||
out.Assets = append(out.Assets, &activityv1.ActivityTemplateAsset{
|
||||
AssetKey: asset.AssetKey, Locale: asset.Locale, Url: asset.URL, MediaType: asset.MediaType, SortOrder: asset.SortOrder,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func activityTemplateRewardItemsToProto(items []domain.RewardItem) []*activityv1.ActivityTemplateRewardItem {
|
||||
out := make([]*activityv1.ActivityTemplateRewardItem, 0, len(items))
|
||||
for _, item := range items {
|
||||
out = append(out, &activityv1.ActivityTemplateRewardItem{
|
||||
ItemType: item.ItemType, ResourceId: item.ResourceID, ResourceType: item.ResourceType,
|
||||
Name: item.Name, IconUrl: item.IconURL, Quantity: item.Quantity, DurationMs: item.DurationMS,
|
||||
WalletAssetType: item.WalletAssetType, WalletAssetAmount: item.WalletAssetAmount, SortOrder: item.SortOrder,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func activityTemplateRewardSnapshotFromProto(snapshotID, snapshotHash, name string, items []*activityv1.ActivityTemplateRewardItem) domain.RewardSnapshot {
|
||||
out := domain.RewardSnapshot{SnapshotID: snapshotID, SnapshotHash: snapshotHash, Name: name, Items: make([]domain.RewardItem, 0, len(items))}
|
||||
for _, item := range items {
|
||||
out.Items = append(out.Items, domain.RewardItem{
|
||||
ItemType: item.GetItemType(), ResourceID: item.GetResourceId(), ResourceType: item.GetResourceType(),
|
||||
Name: item.GetName(), IconURL: item.GetIconUrl(), Quantity: item.GetQuantity(), DurationMS: item.GetDurationMs(),
|
||||
WalletAssetType: item.GetWalletAssetType(), WalletAssetAmount: item.GetWalletAssetAmount(), SortOrder: item.GetSortOrder(),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func activityTemplateSummaryToProto(item domain.Summary) *activityv1.ActivityTemplateSummary {
|
||||
return &activityv1.ActivityTemplateSummary{
|
||||
AppCode: item.AppCode, TemplateId: item.TemplateID, TemplateCode: item.TemplateCode, Name: item.Name,
|
||||
ActivityType: item.ActivityType, Status: item.Status, LifecycleStatus: item.LifecycleStatus,
|
||||
StartMs: item.StartMS, EndMs: item.EndMS, AllRegions: item.AllRegions, RegionIds: append([]int64(nil), item.RegionIDs...),
|
||||
Revision: item.Revision, PublishedVersion: item.PublishedVersion,
|
||||
CreatedByAdminId: item.CreatedByAdminID, UpdatedByAdminId: item.UpdatedByAdminID,
|
||||
CreatedAtMs: item.CreatedAtMS, UpdatedAtMs: item.UpdatedAtMS,
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,7 @@ import (
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/xerr"
|
||||
activitytemplateservice "hyapp/services/activity-service/internal/service/activitytemplate"
|
||||
agencyopeningservice "hyapp/services/activity-service/internal/service/agencyopening"
|
||||
broadcastservice "hyapp/services/activity-service/internal/service/broadcast"
|
||||
growthservice "hyapp/services/activity-service/internal/service/growth"
|
||||
@ -21,25 +22,29 @@ type BroadcastServer struct {
|
||||
activityv1.UnimplementedBroadcastServiceServer
|
||||
activityv1.UnimplementedRoomEventConsumerServiceServer
|
||||
|
||||
svc *broadcastservice.Service
|
||||
growth *growthservice.Service
|
||||
task *taskservice.Service
|
||||
roomReward *roomturnoverrewardservice.Service
|
||||
weeklyStar *weeklystarservice.Service
|
||||
agencyOpen *agencyopeningservice.Service
|
||||
svc *broadcastservice.Service
|
||||
growth *growthservice.Service
|
||||
task *taskservice.Service
|
||||
roomReward *roomturnoverrewardservice.Service
|
||||
weeklyStar *weeklystarservice.Service
|
||||
agencyOpen *agencyopeningservice.Service
|
||||
activityTemplate *activitytemplateservice.Service
|
||||
}
|
||||
|
||||
// NewBroadcastServer 创建播报和 room event 消费共用的 gRPC adapter。
|
||||
// room-service 的 outbox 事件只投递一次,但 activity-service 内部有多条独立投影:
|
||||
// 播报负责 IM 展示,growth 负责等级进度,weekly-star 负责指定礼物榜单,roomReward 负责房间流水。
|
||||
// 这里把这些模块都挂到同一个 gRPC 入口,保证本地直连投递和 MQ 投递使用同一组业务副作用。
|
||||
func NewBroadcastServer(svc *broadcastservice.Service, growthSvc *growthservice.Service, weeklyStarSvc *weeklystarservice.Service, taskSvc *taskservice.Service, roomRewardSvc *roomturnoverrewardservice.Service, agencyOpeningSvc *agencyopeningservice.Service) *BroadcastServer {
|
||||
func NewBroadcastServer(svc *broadcastservice.Service, growthSvc *growthservice.Service, weeklyStarSvc *weeklystarservice.Service, taskSvc *taskservice.Service, roomRewardSvc *roomturnoverrewardservice.Service, agencyOpeningSvc *agencyopeningservice.Service, activityTemplateSvc ...*activitytemplateservice.Service) *BroadcastServer {
|
||||
server := &BroadcastServer{svc: svc}
|
||||
server.growth = growthSvc
|
||||
server.weeklyStar = weeklyStarSvc
|
||||
server.task = taskSvc
|
||||
server.roomReward = roomRewardSvc
|
||||
server.agencyOpen = agencyOpeningSvc
|
||||
if len(activityTemplateSvc) > 0 {
|
||||
server.activityTemplate = activityTemplateSvc[0]
|
||||
}
|
||||
return server
|
||||
}
|
||||
|
||||
@ -130,6 +135,11 @@ func (s *BroadcastServer) ConsumeRoomEvent(ctx context.Context, req *activityv1.
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
}
|
||||
if s.activityTemplate != nil {
|
||||
if _, err := s.activityTemplate.HandleRoomEvent(ctx, req.GetEnvelope()); err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
}
|
||||
if s.agencyOpen != nil {
|
||||
if _, err := s.agencyOpen.HandleRoomEvent(ctx, req.GetEnvelope()); err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/xerr"
|
||||
achievementservice "hyapp/services/activity-service/internal/service/achievement"
|
||||
activitytemplateservice "hyapp/services/activity-service/internal/service/activitytemplate"
|
||||
agencyopeningservice "hyapp/services/activity-service/internal/service/agencyopening"
|
||||
cpweeklyrankservice "hyapp/services/activity-service/internal/service/cpweeklyrank"
|
||||
growthservice "hyapp/services/activity-service/internal/service/growth"
|
||||
@ -20,17 +21,18 @@ import (
|
||||
type CronServer struct {
|
||||
activityv1.UnimplementedActivityCronServiceServer
|
||||
|
||||
message *messageservice.Service
|
||||
growth *growthservice.Service
|
||||
achievement *achievementservice.Service
|
||||
weeklyStar *weeklystarservice.Service
|
||||
roomReward *roomturnoverrewardservice.Service
|
||||
agencyOpen *agencyopeningservice.Service
|
||||
cpWeekly *cpweeklyrankservice.Service
|
||||
message *messageservice.Service
|
||||
growth *growthservice.Service
|
||||
achievement *achievementservice.Service
|
||||
weeklyStar *weeklystarservice.Service
|
||||
roomReward *roomturnoverrewardservice.Service
|
||||
agencyOpen *agencyopeningservice.Service
|
||||
cpWeekly *cpweeklyrankservice.Service
|
||||
activityTemplate *activitytemplateservice.Service
|
||||
}
|
||||
|
||||
// NewCronServer creates the internal cron adapter without exposing message storage.
|
||||
func NewCronServer(messageSvc *messageservice.Service, growthSvc *growthservice.Service, achievementSvc *achievementservice.Service, weeklyStarSvc *weeklystarservice.Service, roomRewardSvc *roomturnoverrewardservice.Service, agencyOpeningSvc *agencyopeningservice.Service, cpWeeklyRankSvc *cpweeklyrankservice.Service) *CronServer {
|
||||
func NewCronServer(messageSvc *messageservice.Service, growthSvc *growthservice.Service, achievementSvc *achievementservice.Service, weeklyStarSvc *weeklystarservice.Service, roomRewardSvc *roomturnoverrewardservice.Service, agencyOpeningSvc *agencyopeningservice.Service, cpWeeklyRankSvc *cpweeklyrankservice.Service, activityTemplateSvc ...*activitytemplateservice.Service) *CronServer {
|
||||
server := &CronServer{message: messageSvc}
|
||||
server.growth = growthSvc
|
||||
server.achievement = achievementSvc
|
||||
@ -38,9 +40,27 @@ func NewCronServer(messageSvc *messageservice.Service, growthSvc *growthservice.
|
||||
server.roomReward = roomRewardSvc
|
||||
server.agencyOpen = agencyOpeningSvc
|
||||
server.cpWeekly = cpWeeklyRankSvc
|
||||
if len(activityTemplateSvc) > 0 {
|
||||
server.activityTemplate = activityTemplateSvc[0]
|
||||
}
|
||||
return server
|
||||
}
|
||||
|
||||
// ProcessActivityTemplateSettlementBatch closes due UTC daily/total boards, freezes immutable
|
||||
// snapshots and retries wallet resource-group grants with stable commands.
|
||||
func (s *CronServer) ProcessActivityTemplateSettlementBatch(ctx context.Context, req *activityv1.CronBatchRequest) (*activityv1.CronBatchResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
if s.activityTemplate == nil {
|
||||
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "activity template runtime service is not configured"))
|
||||
}
|
||||
claimed, processed, success, failure, hasMore, err := s.activityTemplate.ProcessRuntimeSettlementBatch(
|
||||
ctx, req.GetRunId(), req.GetWorkerId(), req.GetBatchSize(), durationFromMillis(req.GetLockTtlMs()))
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return &activityv1.CronBatchResponse{ClaimedCount: claimed, ProcessedCount: processed, SuccessCount: success, FailureCount: failure, HasMore: hasMore}, nil
|
||||
}
|
||||
|
||||
// ProcessMessageFanoutBatch claims and processes one message fanout page.
|
||||
func (s *CronServer) ProcessMessageFanoutBatch(ctx context.Context, req *activityv1.CronBatchRequest) (*activityv1.CronBatchResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
|
||||
@ -64,6 +64,13 @@ tasks:
|
||||
lock_ttl: "1m"
|
||||
batch_size: 50
|
||||
app_codes: ["lalu","fami","huwaa"]
|
||||
activity_template_settlement:
|
||||
enabled: true
|
||||
interval: "5s"
|
||||
timeout: "30s"
|
||||
lock_ttl: "1m"
|
||||
batch_size: 50
|
||||
app_codes: ["lalu","fami","huwaa"]
|
||||
agency_opening_settlement:
|
||||
enabled: true
|
||||
interval: "5s"
|
||||
|
||||
@ -64,6 +64,13 @@ tasks:
|
||||
lock_ttl: "1m"
|
||||
batch_size: 50
|
||||
app_codes: ["lalu","fami","huwaa"]
|
||||
activity_template_settlement:
|
||||
enabled: true
|
||||
interval: "5s"
|
||||
timeout: "30s"
|
||||
lock_ttl: "1m"
|
||||
batch_size: 50
|
||||
app_codes: ["lalu","fami","huwaa"]
|
||||
agency_opening_settlement:
|
||||
enabled: true
|
||||
interval: "5s"
|
||||
|
||||
@ -64,6 +64,13 @@ tasks:
|
||||
lock_ttl: "1m"
|
||||
batch_size: 50
|
||||
app_codes: ["lalu","fami","huwaa"]
|
||||
activity_template_settlement:
|
||||
enabled: true
|
||||
interval: "5s"
|
||||
timeout: "30s"
|
||||
lock_ttl: "1m"
|
||||
batch_size: 50
|
||||
app_codes: ["lalu","fami","huwaa"]
|
||||
agency_opening_settlement:
|
||||
enabled: true
|
||||
interval: "5s"
|
||||
|
||||
@ -114,12 +114,12 @@ func New(cfg config.Config) (*App, error) {
|
||||
gameCron := integration.NewGameCronClient(gamev1.NewGameCronServiceClient(gameConn))
|
||||
walletCron := integration.NewWalletCronClient(walletv1.NewWalletCronServiceClient(walletConn))
|
||||
taskScheduler := scheduler.New(cfg.NodeID, cfg.Tasks, repository, map[string]scheduler.Handler{
|
||||
"login_ip_risk": userCron.ProcessLoginIPRiskBatch,
|
||||
"user_region_rebuild": userCron.ProcessRegionRebuildBatch,
|
||||
"login_ip_risk": userCron.ProcessLoginIPRiskBatch,
|
||||
"user_region_rebuild": userCron.ProcessRegionRebuildBatch,
|
||||
"mic_open_session_compensation": userCron.CompensateMicOpenSessions,
|
||||
"room_open_session_compensation": userCron.CompensateRoomOpenSessions,
|
||||
"manager_user_block_expiry": userCron.ExpireManagerUserBlocks,
|
||||
"admin_user_ban_expiry": userCron.ExpireAdminUserBans,
|
||||
"manager_user_block_expiry": userCron.ExpireManagerUserBlocks,
|
||||
"admin_user_ban_expiry": userCron.ExpireAdminUserBans,
|
||||
// CP 榜单任务只占 cron 租约并调用 user-service;真实 MySQL 读取、wallet 头像框补齐和 Redis 替换都在 user-service 内完成。
|
||||
"cp_intimacy_leaderboard": userCron.RefreshCPIntimacyLeaderboard,
|
||||
"message_fanout": activityCron.ProcessMessageFanoutBatch,
|
||||
@ -127,6 +127,7 @@ func New(cfg config.Config) (*App, error) {
|
||||
"temporary_growth_level": activityCron.ProcessTemporaryLevelBatch,
|
||||
"achievement_reward": activityCron.ProcessAchievementRewardBatch,
|
||||
"weekly_star_settlement": activityCron.ProcessWeeklyStarSettlementBatch,
|
||||
"activity_template_settlement": activityCron.ProcessActivityTemplateSettlementBatch,
|
||||
"agency_opening_settlement": activityCron.ProcessAgencyOpeningSettlementBatch,
|
||||
"cp_weekly_rank_settlement": activityCron.ProcessCPWeeklyRankSettlementBatch,
|
||||
"room_turnover_reward_settlement": activityCron.ProcessRoomTurnoverRewardSettlementBatch,
|
||||
|
||||
@ -192,6 +192,14 @@ func defaultTasks() map[string]TaskConfig {
|
||||
BatchSize: 50,
|
||||
AppCodes: []string{defaultAppCode},
|
||||
},
|
||||
"activity_template_settlement": {
|
||||
Enabled: true,
|
||||
Interval: "5s",
|
||||
Timeout: "30s",
|
||||
LockTTL: "1m",
|
||||
BatchSize: 50,
|
||||
AppCodes: []string{defaultAppCode},
|
||||
},
|
||||
"agency_opening_settlement": {
|
||||
Enabled: true,
|
||||
Interval: "5s",
|
||||
|
||||
@ -63,6 +63,15 @@ func (c *ActivityCronClient) ProcessWeeklyStarSettlementBatch(ctx context.Contex
|
||||
return activityCronBatchResult(resp), nil
|
||||
}
|
||||
|
||||
// ProcessActivityTemplateSettlementBatch handles immutable-version daily and total leaderboard rewards.
|
||||
func (c *ActivityCronClient) ProcessActivityTemplateSettlementBatch(ctx context.Context, req scheduler.BatchRequest) (scheduler.BatchResult, error) {
|
||||
resp, err := c.client.ProcessActivityTemplateSettlementBatch(ctx, activityCronBatchRequest(req))
|
||||
if err != nil {
|
||||
return scheduler.BatchResult{}, err
|
||||
}
|
||||
return activityCronBatchResult(resp), nil
|
||||
}
|
||||
|
||||
// ProcessAgencyOpeningSettlementBatch handles ended agency-opening leaderboard rewards.
|
||||
func (c *ActivityCronClient) ProcessAgencyOpeningSettlementBatch(ctx context.Context, req scheduler.BatchRequest) (scheduler.BatchResult, error) {
|
||||
resp, err := c.client.ProcessAgencyOpeningSettlementBatch(ctx, activityCronBatchRequest(req))
|
||||
|
||||
@ -125,6 +125,7 @@ func New(cfg config.Config) (*App, error) {
|
||||
var weeklyStarClient client.WeeklyStarClient = client.NewGRPCWeeklyStarClient(activityConn)
|
||||
var cpWeeklyRankClient client.CPWeeklyRankClient = client.NewGRPCCPWeeklyRankClient(activityConn)
|
||||
var agencyOpeningClient client.AgencyOpeningClient = client.NewGRPCAgencyOpeningClient(activityConn)
|
||||
var activityTemplateRuntimeClient client.ActivityTemplateRuntimeClient = client.NewGRPCActivityTemplateRuntimeClient(activityConn)
|
||||
var wheelClient client.WheelClient = client.NewGRPCWheelClient(activityConn)
|
||||
var broadcastClient client.BroadcastClient = client.NewGRPCBroadcastClient(activityConn)
|
||||
var gameClient client.GameClient = client.NewGRPCGameClient(gameConn)
|
||||
@ -228,6 +229,7 @@ func New(cfg config.Config) (*App, error) {
|
||||
handler.SetWeeklyStarClient(weeklyStarClient)
|
||||
handler.SetCPWeeklyRankClient(cpWeeklyRankClient)
|
||||
handler.SetAgencyOpeningClient(agencyOpeningClient)
|
||||
handler.SetActivityTemplateRuntimeClient(activityTemplateRuntimeClient)
|
||||
handler.SetWheelClient(wheelClient)
|
||||
handler.SetBroadcastClient(broadcastClient)
|
||||
handler.SetGameClient(gameClient)
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
)
|
||||
|
||||
// ActivityTemplateRuntimeClient 是 gateway 对活动模版 App 运行态的唯一内部依赖。
|
||||
// gateway 只负责鉴权身份、可信区域和 HTTP 协议转换;版本命中、UTC 日任务、领奖幂等和榜单事实都由 activity-service 决定。
|
||||
type ActivityTemplateRuntimeClient interface {
|
||||
GetCurrentActivityTemplate(ctx context.Context, req *activityv1.GetCurrentActivityTemplateRequest) (*activityv1.GetCurrentActivityTemplateResponse, error)
|
||||
VisitActivityTemplate(ctx context.Context, req *activityv1.VisitActivityTemplateRequest) (*activityv1.VisitActivityTemplateResponse, error)
|
||||
ListActivityTemplateTasks(ctx context.Context, req *activityv1.ListActivityTemplateTasksRequest) (*activityv1.ListActivityTemplateTasksResponse, error)
|
||||
ClaimActivityTemplateTaskReward(ctx context.Context, req *activityv1.ClaimActivityTemplateTaskRewardRequest) (*activityv1.ClaimActivityTemplateTaskRewardResponse, error)
|
||||
ListActivityTemplateLeaderboard(ctx context.Context, req *activityv1.ListActivityTemplateLeaderboardRequest) (*activityv1.ListActivityTemplateLeaderboardResponse, error)
|
||||
}
|
||||
|
||||
type grpcActivityTemplateRuntimeClient struct {
|
||||
client activityv1.ActivityTemplateRuntimeServiceClient
|
||||
}
|
||||
|
||||
func NewGRPCActivityTemplateRuntimeClient(conn *grpc.ClientConn) ActivityTemplateRuntimeClient {
|
||||
return &grpcActivityTemplateRuntimeClient{client: activityv1.NewActivityTemplateRuntimeServiceClient(conn)}
|
||||
}
|
||||
|
||||
func (c *grpcActivityTemplateRuntimeClient) GetCurrentActivityTemplate(ctx context.Context, req *activityv1.GetCurrentActivityTemplateRequest) (*activityv1.GetCurrentActivityTemplateResponse, error) {
|
||||
return c.client.GetCurrentActivityTemplate(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcActivityTemplateRuntimeClient) VisitActivityTemplate(ctx context.Context, req *activityv1.VisitActivityTemplateRequest) (*activityv1.VisitActivityTemplateResponse, error) {
|
||||
return c.client.VisitActivityTemplate(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcActivityTemplateRuntimeClient) ListActivityTemplateTasks(ctx context.Context, req *activityv1.ListActivityTemplateTasksRequest) (*activityv1.ListActivityTemplateTasksResponse, error) {
|
||||
return c.client.ListActivityTemplateTasks(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcActivityTemplateRuntimeClient) ClaimActivityTemplateTaskReward(ctx context.Context, req *activityv1.ClaimActivityTemplateTaskRewardRequest) (*activityv1.ClaimActivityTemplateTaskRewardResponse, error) {
|
||||
return c.client.ClaimActivityTemplateTaskReward(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcActivityTemplateRuntimeClient) ListActivityTemplateLeaderboard(ctx context.Context, req *activityv1.ListActivityTemplateLeaderboardRequest) (*activityv1.ListActivityTemplateLeaderboardResponse, error) {
|
||||
return c.client.ListActivityTemplateLeaderboard(ctx, req)
|
||||
}
|
||||
@ -0,0 +1,780 @@
|
||||
package activityapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
userv1 "hyapp.local/api/proto/user/v1"
|
||||
"hyapp/services/gateway-service/internal/auth"
|
||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||
)
|
||||
|
||||
const activityTemplateUTCDayLayout = "2006-01-02"
|
||||
|
||||
type activityTemplateLocaleData struct {
|
||||
Locale string `json:"locale"`
|
||||
Title string `json:"title"`
|
||||
Rules string `json:"rules"`
|
||||
}
|
||||
|
||||
type activityTemplateGiftData struct {
|
||||
GiftID string `json:"gift_id"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
Name string `json:"name"`
|
||||
IconURL string `json:"icon_url"`
|
||||
CoinPrice int64 `json:"coin_price"`
|
||||
PriceVersion string `json:"price_version"`
|
||||
}
|
||||
|
||||
type activityTemplateRewardItemData struct {
|
||||
ItemType string `json:"item_type"`
|
||||
ResourceID int64 `json:"resource_id"`
|
||||
ResourceType string `json:"resource_type"`
|
||||
Name string `json:"name"`
|
||||
IconURL string `json:"icon_url"`
|
||||
Quantity int64 `json:"quantity"`
|
||||
DurationMS int64 `json:"duration_ms"`
|
||||
WalletAssetType string `json:"wallet_asset_type"`
|
||||
WalletAssetAmount int64 `json:"wallet_asset_amount"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
}
|
||||
|
||||
type activityTemplateTaskDefinitionData struct {
|
||||
TaskKey string `json:"task_key"`
|
||||
TaskType string `json:"task_type"`
|
||||
TargetValue int64 `json:"target_value"`
|
||||
RewardResourceGroupID int64 `json:"reward_resource_group_id"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
RewardSnapshotID string `json:"reward_snapshot_id"`
|
||||
RewardName string `json:"reward_name"`
|
||||
RewardItems []activityTemplateRewardItemData `json:"reward_items"`
|
||||
}
|
||||
|
||||
type activityTemplateRankRewardData struct {
|
||||
BoardType string `json:"board_type"`
|
||||
RankFrom int32 `json:"rank_from"`
|
||||
RankTo int32 `json:"rank_to"`
|
||||
ResourceGroupID int64 `json:"resource_group_id"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
RewardSnapshotID string `json:"reward_snapshot_id"`
|
||||
RewardName string `json:"reward_name"`
|
||||
RewardItems []activityTemplateRewardItemData `json:"reward_items"`
|
||||
}
|
||||
|
||||
type activityTemplateAssetData struct {
|
||||
AssetKey string `json:"asset_key"`
|
||||
Locale string `json:"locale"`
|
||||
URL string `json:"url"`
|
||||
MediaType string `json:"media_type"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
}
|
||||
|
||||
// activityTemplateRuntimeData is deliberately smaller than the admin aggregate. App clients receive
|
||||
// only the immutable published presentation/rule snapshot and runtime window, never admin IDs,
|
||||
// optimistic revisions or mutable draft metadata.
|
||||
type activityTemplateRuntimeData struct {
|
||||
TemplateID string `json:"template_id"`
|
||||
TemplateCode string `json:"template_code"`
|
||||
ActivityType string `json:"activity_type"`
|
||||
Status string `json:"status"`
|
||||
LifecycleStatus string `json:"lifecycle_status"`
|
||||
StartMS int64 `json:"start_ms"`
|
||||
EndMS int64 `json:"end_ms"`
|
||||
AllRegions bool `json:"all_regions"`
|
||||
RegionIDs []int64 `json:"region_ids"`
|
||||
Locales []activityTemplateLocaleData `json:"locales"`
|
||||
Locale *activityTemplateLocaleData `json:"locale,omitempty"`
|
||||
Gifts []activityTemplateGiftData `json:"gifts"`
|
||||
Tasks []activityTemplateTaskDefinitionData `json:"tasks"`
|
||||
DailyLeaderboardSize int32 `json:"daily_leaderboard_size"`
|
||||
TotalLeaderboardSize int32 `json:"total_leaderboard_size"`
|
||||
RankRewards []activityTemplateRankRewardData `json:"rank_rewards"`
|
||||
Assets []activityTemplateAssetData `json:"assets"`
|
||||
DisplayConfig json.RawMessage `json:"display_config"`
|
||||
VersionNo int64 `json:"version_no"`
|
||||
RuntimeFromMS int64 `json:"runtime_from_ms"`
|
||||
RuntimeToMS int64 `json:"runtime_to_ms"`
|
||||
ServerTimeMS int64 `json:"server_time_ms"`
|
||||
}
|
||||
|
||||
type activityTemplateRuntimeTaskData struct {
|
||||
TemplateID string `json:"template_id"`
|
||||
TemplateCode string `json:"template_code"`
|
||||
VersionNo int64 `json:"version_no"`
|
||||
TaskDay string `json:"task_day"`
|
||||
TaskKey string `json:"task_key"`
|
||||
TaskType string `json:"task_type"`
|
||||
TargetValue int64 `json:"target_value"`
|
||||
ProgressValue int64 `json:"progress_value"`
|
||||
RewardResourceGroupID int64 `json:"reward_resource_group_id"`
|
||||
Status string `json:"status"`
|
||||
Claimable bool `json:"claimable"`
|
||||
CompletedAtMS int64 `json:"completed_at_ms"`
|
||||
ClaimedAtMS int64 `json:"claimed_at_ms"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
RewardSnapshotID string `json:"reward_snapshot_id"`
|
||||
RewardName string `json:"reward_name"`
|
||||
RewardItems []activityTemplateRewardItemData `json:"reward_items"`
|
||||
}
|
||||
|
||||
type activityTemplateTaskClaimData struct {
|
||||
ClaimID string `json:"claim_id"`
|
||||
TemplateID string `json:"template_id"`
|
||||
TemplateCode string `json:"template_code"`
|
||||
VersionNo int64 `json:"version_no"`
|
||||
TaskDay string `json:"task_day"`
|
||||
TaskKey string `json:"task_key"`
|
||||
UserID string `json:"user_id"`
|
||||
RewardResourceGroupID int64 `json:"reward_resource_group_id"`
|
||||
Status string `json:"status"`
|
||||
CreatedAtMS int64 `json:"created_at_ms"`
|
||||
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||
GrantedAtMS int64 `json:"granted_at_ms"`
|
||||
RewardSnapshotID string `json:"reward_snapshot_id"`
|
||||
RewardName string `json:"reward_name"`
|
||||
RewardItems []activityTemplateRewardItemData `json:"reward_items"`
|
||||
}
|
||||
|
||||
type activityTemplateLeaderboardUserData struct {
|
||||
UserID string `json:"user_id"`
|
||||
DisplayUserID string `json:"display_user_id"`
|
||||
Username string `json:"username"`
|
||||
Avatar string `json:"avatar"`
|
||||
}
|
||||
|
||||
type activityTemplateLeaderboardEntryData struct {
|
||||
RankNo int32 `json:"rank_no"`
|
||||
UserID string `json:"user_id"`
|
||||
Score int64 `json:"score"`
|
||||
GiftCount int64 `json:"gift_count"`
|
||||
CoinAmount int64 `json:"coin_amount"`
|
||||
FirstScoredAtMS int64 `json:"first_scored_at_ms"`
|
||||
Snapshot bool `json:"snapshot"`
|
||||
User *activityTemplateLeaderboardUserData `json:"user,omitempty"`
|
||||
}
|
||||
|
||||
type activityTemplateVisitBody struct {
|
||||
CommandID string `json:"command_id"`
|
||||
VersionNo int64 `json:"version_no"`
|
||||
}
|
||||
|
||||
type activityTemplateTaskClaimBody struct {
|
||||
TaskDay string `json:"task_day"`
|
||||
CommandID string `json:"command_id"`
|
||||
VersionNo int64 `json:"version_no"`
|
||||
}
|
||||
|
||||
type activityTemplateCurrentData struct {
|
||||
Found bool `json:"found"`
|
||||
Runtime *activityTemplateRuntimeData `json:"runtime,omitempty"`
|
||||
}
|
||||
|
||||
type activityTemplateVisitData struct {
|
||||
Runtime *activityTemplateRuntimeData `json:"runtime,omitempty"`
|
||||
Tasks []activityTemplateRuntimeTaskData `json:"tasks"`
|
||||
FirstVisit bool `json:"first_visit"`
|
||||
}
|
||||
|
||||
type activityTemplateTasksData struct {
|
||||
Runtime *activityTemplateRuntimeData `json:"runtime,omitempty"`
|
||||
Tasks []activityTemplateRuntimeTaskData `json:"tasks"`
|
||||
NextRefreshAtMS int64 `json:"next_refresh_at_ms"`
|
||||
}
|
||||
|
||||
type activityTemplateClaimData struct {
|
||||
Claim *activityTemplateTaskClaimData `json:"claim,omitempty"`
|
||||
Claimed bool `json:"claimed"`
|
||||
}
|
||||
|
||||
type activityTemplateLeaderboardData struct {
|
||||
Runtime *activityTemplateRuntimeData `json:"runtime,omitempty"`
|
||||
BoardType string `json:"board_type"`
|
||||
PeriodKey string `json:"period_key"`
|
||||
PeriodStartMS int64 `json:"period_start_ms"`
|
||||
PeriodEndMS int64 `json:"period_end_ms"`
|
||||
SettleAfterMS int64 `json:"settle_after_ms"`
|
||||
PeriodStatus string `json:"period_status"`
|
||||
Entries []activityTemplateLeaderboardEntryData `json:"entries"`
|
||||
MyEntry *activityTemplateLeaderboardEntryData `json:"my_entry,omitempty"`
|
||||
Total int64 `json:"total"`
|
||||
Page int32 `json:"page"`
|
||||
PageSize int32 `json:"page_size"`
|
||||
}
|
||||
|
||||
// getCurrentActivityTemplate resolves region from the authenticated user's current profile. A client
|
||||
// cannot probe another region by crafting a query parameter, which keeps rollout targeting identical
|
||||
// between page configuration, task progress and gift-event attribution.
|
||||
func (h *Handler) getCurrentActivityTemplate(writer http.ResponseWriter, request *http.Request) {
|
||||
if h.activityTemplateRuntime == nil || h.userProfileClient == nil {
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
return
|
||||
}
|
||||
userID := auth.UserIDFromContext(request.Context())
|
||||
regionID, ok := h.resolveActivityTemplateRegionID(writer, request, userID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
locale := activityTemplateRequestLocale(request)
|
||||
resp, err := h.activityTemplateRuntime.GetCurrentActivityTemplate(request.Context(), &activityv1.GetCurrentActivityTemplateRequest{
|
||||
Meta: httpkit.ActivityMeta(request), UserId: userID, RegionId: regionID, Locale: locale,
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
data := activityTemplateCurrentData{Found: resp.GetFound()}
|
||||
if resp.GetRuntime() != nil {
|
||||
runtime := activityTemplateRuntimeFromProto(resp.GetRuntime(), locale)
|
||||
data.Runtime = &runtime
|
||||
}
|
||||
httpkit.WriteOK(writer, request, data)
|
||||
}
|
||||
|
||||
// visitActivityTemplate records the explicit daily_visit fact. request_id remains tracing metadata;
|
||||
// only the client-provided command_id is allowed to define replay identity across HTTP retries.
|
||||
func (h *Handler) visitActivityTemplate(writer http.ResponseWriter, request *http.Request) {
|
||||
if h.activityTemplateRuntime == nil || h.userProfileClient == nil {
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
return
|
||||
}
|
||||
templateCode := strings.TrimSpace(request.PathValue("template_code"))
|
||||
var body activityTemplateVisitBody
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
body.CommandID = strings.TrimSpace(body.CommandID)
|
||||
if templateCode == "" || body.CommandID == "" || body.VersionNo <= 0 {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
userID := auth.UserIDFromContext(request.Context())
|
||||
regionID, ok := h.resolveActivityTemplateRegionID(writer, request, userID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
resp, err := h.activityTemplateRuntime.VisitActivityTemplate(request.Context(), &activityv1.VisitActivityTemplateRequest{
|
||||
Meta: httpkit.ActivityMeta(request), UserId: userID, RegionId: regionID, TemplateCode: templateCode,
|
||||
CommandId: body.CommandID, VersionNo: body.VersionNo,
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
httpkit.WriteOK(writer, request, activityTemplateVisitData{
|
||||
Runtime: activityTemplateRuntimePointerFromProto(resp.GetRuntime(), activityTemplateRequestLocale(request)),
|
||||
Tasks: activityTemplateRuntimeTasksFromProto(resp.GetTasks()), FirstVisit: resp.GetFirstVisit(),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) listActivityTemplateTasks(writer http.ResponseWriter, request *http.Request) {
|
||||
if h.activityTemplateRuntime == nil {
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
return
|
||||
}
|
||||
templateCode := strings.TrimSpace(request.PathValue("template_code"))
|
||||
taskDay, ok := normalizeOptionalActivityTemplateUTCDay(request.URL.Query().Get("task_day"))
|
||||
versionNo, versionOK := optionalActivityTemplateVersionQuery(request)
|
||||
if templateCode == "" || !ok || !versionOK {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
userID := auth.UserIDFromContext(request.Context())
|
||||
regionID := int64(0)
|
||||
if versionNo == 0 {
|
||||
var regionOK bool
|
||||
regionID, regionOK = h.resolveActivityTemplateRegionID(writer, request, userID)
|
||||
if !regionOK {
|
||||
return
|
||||
}
|
||||
}
|
||||
resp, err := h.activityTemplateRuntime.ListActivityTemplateTasks(request.Context(), &activityv1.ListActivityTemplateTasksRequest{
|
||||
Meta: httpkit.ActivityMeta(request), UserId: userID, RegionId: regionID, TemplateCode: templateCode,
|
||||
TaskDay: taskDay, VersionNo: versionNo,
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
httpkit.WriteOK(writer, request, activityTemplateTasksData{
|
||||
Runtime: activityTemplateRuntimePointerFromProto(resp.GetRuntime(), activityTemplateRequestLocale(request)),
|
||||
Tasks: activityTemplateRuntimeTasksFromProto(resp.GetTasks()), NextRefreshAtMS: resp.GetNextRefreshAtMs(),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) claimActivityTemplateTask(writer http.ResponseWriter, request *http.Request) {
|
||||
if h.activityTemplateRuntime == nil {
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
return
|
||||
}
|
||||
templateCode := strings.TrimSpace(request.PathValue("template_code"))
|
||||
taskKey := strings.TrimSpace(request.PathValue("task_key"))
|
||||
var body activityTemplateTaskClaimBody
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
taskDay, validDay := normalizeRequiredActivityTemplateUTCDay(body.TaskDay)
|
||||
body.CommandID = strings.TrimSpace(body.CommandID)
|
||||
if templateCode == "" || taskKey == "" || !validDay || body.CommandID == "" || body.VersionNo <= 0 {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
userID := auth.UserIDFromContext(request.Context())
|
||||
resp, err := h.activityTemplateRuntime.ClaimActivityTemplateTaskReward(request.Context(), &activityv1.ClaimActivityTemplateTaskRewardRequest{
|
||||
Meta: httpkit.ActivityMeta(request), UserId: userID, RegionId: 0, TemplateCode: templateCode,
|
||||
TaskDay: taskDay, TaskKey: taskKey, CommandId: body.CommandID, VersionNo: body.VersionNo,
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
httpkit.WriteOK(writer, request, activityTemplateClaimData{
|
||||
Claim: activityTemplateTaskClaimPointerFromProto(resp.GetClaim()), Claimed: resp.GetClaimed(),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) listActivityTemplateDailyLeaderboard(writer http.ResponseWriter, request *http.Request) {
|
||||
h.listActivityTemplateLeaderboard(writer, request, "daily")
|
||||
}
|
||||
|
||||
func (h *Handler) listActivityTemplateTotalLeaderboard(writer http.ResponseWriter, request *http.Request) {
|
||||
h.listActivityTemplateLeaderboard(writer, request, "total")
|
||||
}
|
||||
|
||||
func (h *Handler) listActivityTemplateLeaderboard(writer http.ResponseWriter, request *http.Request, boardType string) {
|
||||
if h.activityTemplateRuntime == nil {
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
return
|
||||
}
|
||||
templateCode := strings.TrimSpace(request.PathValue("template_code"))
|
||||
page, pageOK := httpkit.PositiveInt32Query(request, "page", 1)
|
||||
pageSize, pageSizeOK := httpkit.PositiveInt32Query(request, "page_size", 50)
|
||||
versionNo, versionOK := optionalActivityTemplateVersionQuery(request)
|
||||
if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
periodKey := "total"
|
||||
periodOK := true
|
||||
if boardType == "daily" {
|
||||
periodKey, periodOK = normalizeOptionalActivityTemplateUTCDay(request.URL.Query().Get("period_key"))
|
||||
}
|
||||
if templateCode == "" || !pageOK || !pageSizeOK || !periodOK || !versionOK {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
userID := auth.UserIDFromContext(request.Context())
|
||||
regionID := int64(0)
|
||||
if versionNo == 0 {
|
||||
var regionOK bool
|
||||
regionID, regionOK = h.resolveActivityTemplateRegionID(writer, request, userID)
|
||||
if !regionOK {
|
||||
return
|
||||
}
|
||||
}
|
||||
resp, err := h.activityTemplateRuntime.ListActivityTemplateLeaderboard(request.Context(), &activityv1.ListActivityTemplateLeaderboardRequest{
|
||||
Meta: httpkit.ActivityMeta(request), UserId: userID, RegionId: regionID, TemplateCode: templateCode,
|
||||
BoardType: boardType, PeriodKey: periodKey, Page: page, PageSize: pageSize, VersionNo: versionNo,
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
ids := activityTemplateLeaderboardUserIDs(resp.GetEntries(), resp.GetMyEntry())
|
||||
users := h.resolveActivityTemplateLeaderboardUsers(request, ids)
|
||||
data := activityTemplateLeaderboardData{
|
||||
Runtime: activityTemplateRuntimePointerFromProto(resp.GetRuntime(), activityTemplateRequestLocale(request)),
|
||||
BoardType: resp.GetBoardType(), PeriodKey: resp.GetPeriodKey(), PeriodStartMS: resp.GetPeriodStartMs(),
|
||||
PeriodEndMS: resp.GetPeriodEndMs(), SettleAfterMS: resp.GetSettleAfterMs(), PeriodStatus: resp.GetPeriodStatus(),
|
||||
Entries: activityTemplateLeaderboardEntriesFromProto(resp.GetEntries(), users),
|
||||
Total: resp.GetTotal(), Page: page, PageSize: pageSize,
|
||||
}
|
||||
if resp.GetMyEntry() != nil {
|
||||
entry := activityTemplateLeaderboardEntryFromProto(resp.GetMyEntry(), users)
|
||||
data.MyEntry = &entry
|
||||
}
|
||||
httpkit.WriteOK(writer, request, data)
|
||||
}
|
||||
|
||||
func optionalActivityTemplateVersionQuery(request *http.Request) (int64, bool) {
|
||||
raw := strings.TrimSpace(request.URL.Query().Get("version_no"))
|
||||
if raw == "" {
|
||||
return 0, true
|
||||
}
|
||||
versionNo, err := strconv.ParseInt(raw, 10, 64)
|
||||
return versionNo, err == nil && versionNo > 0
|
||||
}
|
||||
|
||||
func (h *Handler) resolveActivityTemplateRegionID(writer http.ResponseWriter, request *http.Request, userID int64) (int64, bool) {
|
||||
if h.userProfileClient == nil {
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
return 0, false
|
||||
}
|
||||
resp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||
Meta: httpkit.UserMeta(request, ""), UserId: userID,
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return 0, false
|
||||
}
|
||||
regionID := resp.GetUser().GetRegionId()
|
||||
if regionID <= 0 {
|
||||
// A completed profile without a canonical region is inconsistent upstream state. Do not accept a
|
||||
// caller supplied fallback, because that would bypass regional publication targeting.
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
return 0, false
|
||||
}
|
||||
return regionID, true
|
||||
}
|
||||
|
||||
func (h *Handler) resolveActivityTemplateLeaderboardUsers(request *http.Request, ids []int64) map[int64]activityTemplateLeaderboardUserData {
|
||||
users := make(map[int64]activityTemplateLeaderboardUserData)
|
||||
ids = uniquePositiveIDs(ids)
|
||||
if len(ids) == 0 || h.userProfileClient == nil {
|
||||
return users
|
||||
}
|
||||
resp, err := h.userProfileClient.BatchGetUsers(request.Context(), &userv1.BatchGetUsersRequest{
|
||||
Meta: httpkit.UserMeta(request, ""), UserIds: ids,
|
||||
})
|
||||
if err != nil {
|
||||
// Ranking score/order are owned by activity-service. Profile decoration is best effort so a
|
||||
// transient user-service read failure cannot make an otherwise valid ranking page unavailable.
|
||||
return users
|
||||
}
|
||||
for _, item := range resp.GetUsers() {
|
||||
users[item.GetUserId()] = activityTemplateLeaderboardUserData{
|
||||
UserID: strconv.FormatInt(item.GetUserId(), 10), DisplayUserID: item.GetDisplayUserId(),
|
||||
Username: item.GetUsername(), Avatar: item.GetAvatar(),
|
||||
}
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
func activityTemplateRuntimeFromProto(runtime *activityv1.ActivityTemplateRuntime, requestedLocale string) activityTemplateRuntimeData {
|
||||
data := activityTemplateRuntimeData{
|
||||
RegionIDs: []int64{}, Locales: []activityTemplateLocaleData{}, Gifts: []activityTemplateGiftData{},
|
||||
Tasks: []activityTemplateTaskDefinitionData{}, RankRewards: []activityTemplateRankRewardData{},
|
||||
Assets: []activityTemplateAssetData{}, DisplayConfig: json.RawMessage(`{}`),
|
||||
}
|
||||
if runtime == nil || runtime.GetTemplate() == nil {
|
||||
return data
|
||||
}
|
||||
template := runtime.GetTemplate()
|
||||
data.TemplateID = template.GetTemplateId()
|
||||
data.TemplateCode = template.GetTemplateCode()
|
||||
data.ActivityType = template.GetActivityType()
|
||||
data.Status = template.GetStatus()
|
||||
data.LifecycleStatus = template.GetLifecycleStatus()
|
||||
data.StartMS = template.GetStartMs()
|
||||
data.EndMS = template.GetEndMs()
|
||||
data.AllRegions = template.GetAllRegions()
|
||||
data.RegionIDs = append(data.RegionIDs, template.GetRegionIds()...)
|
||||
seenLocales := make(map[string]struct{}, len(template.GetLocales()))
|
||||
for _, item := range template.GetLocales() {
|
||||
locale, valid := canonicalActivityTemplateLanguageTag(item.GetLocale())
|
||||
if !valid {
|
||||
// 发布写路径会校验 locale;读取侧仍需失败关闭,避免旧数据或跨版本脏数据把非法 tag 暴露给 App。
|
||||
continue
|
||||
}
|
||||
key := strings.ToLower(locale)
|
||||
if _, exists := seenLocales[key]; exists {
|
||||
continue
|
||||
}
|
||||
seenLocales[key] = struct{}{}
|
||||
data.Locales = append(data.Locales, activityTemplateLocaleData{Locale: locale, Title: item.GetTitle(), Rules: item.GetRules()})
|
||||
}
|
||||
if selected, ok := selectActivityTemplateLocale(data.Locales, requestedLocale); ok {
|
||||
data.Locale = &selected
|
||||
}
|
||||
for _, item := range template.GetGifts() {
|
||||
data.Gifts = append(data.Gifts, activityTemplateGiftData{
|
||||
GiftID: item.GetGiftId(), SortOrder: item.GetSortOrder(), Name: item.GetName(), IconURL: item.GetIconUrl(),
|
||||
CoinPrice: item.GetCoinPrice(), PriceVersion: item.GetPriceVersion(),
|
||||
})
|
||||
}
|
||||
for _, item := range template.GetTasks() {
|
||||
data.Tasks = append(data.Tasks, activityTemplateTaskDefinitionData{
|
||||
TaskKey: item.GetTaskKey(), TaskType: item.GetTaskType(), TargetValue: item.GetTargetValue(),
|
||||
RewardResourceGroupID: item.GetRewardResourceGroupId(), SortOrder: item.GetSortOrder(),
|
||||
RewardSnapshotID: item.GetRewardSnapshotId(), RewardName: item.GetRewardName(),
|
||||
RewardItems: activityTemplateRewardItemsFromProto(item.GetRewardItems()),
|
||||
})
|
||||
}
|
||||
data.DailyLeaderboardSize = template.GetDailyLeaderboardSize()
|
||||
data.TotalLeaderboardSize = template.GetTotalLeaderboardSize()
|
||||
for _, item := range template.GetRankRewards() {
|
||||
data.RankRewards = append(data.RankRewards, activityTemplateRankRewardData{
|
||||
BoardType: item.GetBoardType(), RankFrom: item.GetRankFrom(), RankTo: item.GetRankTo(),
|
||||
ResourceGroupID: item.GetResourceGroupId(), SortOrder: item.GetSortOrder(),
|
||||
RewardSnapshotID: item.GetRewardSnapshotId(), RewardName: item.GetRewardName(),
|
||||
RewardItems: activityTemplateRewardItemsFromProto(item.GetRewardItems()),
|
||||
})
|
||||
}
|
||||
for _, item := range template.GetAssets() {
|
||||
assetLocale := strings.TrimSpace(item.GetLocale())
|
||||
if assetLocale != "*" {
|
||||
var valid bool
|
||||
assetLocale, valid = canonicalActivityTemplateLanguageTag(assetLocale)
|
||||
if !valid {
|
||||
continue
|
||||
}
|
||||
}
|
||||
data.Assets = append(data.Assets, activityTemplateAssetData{
|
||||
AssetKey: item.GetAssetKey(), Locale: assetLocale, URL: item.GetUrl(), MediaType: item.GetMediaType(), SortOrder: item.GetSortOrder(),
|
||||
})
|
||||
}
|
||||
if raw := []byte(strings.TrimSpace(template.GetDisplayConfigJson())); len(raw) > 0 && json.Valid(raw) {
|
||||
data.DisplayConfig = append(json.RawMessage(nil), raw...)
|
||||
}
|
||||
data.VersionNo = runtime.GetVersionNo()
|
||||
data.RuntimeFromMS = runtime.GetRuntimeFromMs()
|
||||
data.RuntimeToMS = runtime.GetRuntimeToMs()
|
||||
data.ServerTimeMS = runtime.GetServerTimeMs()
|
||||
return data
|
||||
}
|
||||
|
||||
func activityTemplateRuntimePointerFromProto(runtime *activityv1.ActivityTemplateRuntime, requestedLocale string) *activityTemplateRuntimeData {
|
||||
if runtime == nil {
|
||||
return nil
|
||||
}
|
||||
data := activityTemplateRuntimeFromProto(runtime, requestedLocale)
|
||||
return &data
|
||||
}
|
||||
|
||||
func activityTemplateRuntimeTasksFromProto(items []*activityv1.ActivityTemplateRuntimeTask) []activityTemplateRuntimeTaskData {
|
||||
result := make([]activityTemplateRuntimeTaskData, 0, len(items))
|
||||
for _, item := range items {
|
||||
result = append(result, activityTemplateRuntimeTaskData{
|
||||
TemplateID: item.GetTemplateId(), TemplateCode: item.GetTemplateCode(), VersionNo: item.GetVersionNo(),
|
||||
TaskDay: item.GetTaskDay(), TaskKey: item.GetTaskKey(), TaskType: item.GetTaskType(), TargetValue: item.GetTargetValue(),
|
||||
ProgressValue: item.GetProgressValue(), RewardResourceGroupID: item.GetRewardResourceGroupId(),
|
||||
Status: item.GetStatus(), Claimable: item.GetClaimable(), CompletedAtMS: item.GetCompletedAtMs(),
|
||||
ClaimedAtMS: item.GetClaimedAtMs(), SortOrder: item.GetSortOrder(),
|
||||
RewardSnapshotID: item.GetRewardSnapshotId(), RewardName: item.GetRewardName(),
|
||||
RewardItems: activityTemplateRewardItemsFromProto(item.GetRewardItems()),
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func activityTemplateTaskClaimPointerFromProto(item *activityv1.ActivityTemplateTaskClaim) *activityTemplateTaskClaimData {
|
||||
if item == nil {
|
||||
return nil
|
||||
}
|
||||
return &activityTemplateTaskClaimData{
|
||||
ClaimID: item.GetClaimId(), TemplateID: item.GetTemplateId(),
|
||||
TemplateCode: item.GetTemplateCode(), VersionNo: item.GetVersionNo(), TaskDay: item.GetTaskDay(),
|
||||
TaskKey: item.GetTaskKey(), UserID: strconv.FormatInt(item.GetUserId(), 10),
|
||||
RewardResourceGroupID: item.GetRewardResourceGroupId(), Status: item.GetStatus(),
|
||||
CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), GrantedAtMS: item.GetGrantedAtMs(),
|
||||
RewardSnapshotID: item.GetRewardSnapshotId(), RewardName: item.GetRewardName(),
|
||||
RewardItems: activityTemplateRewardItemsFromProto(item.GetRewardItems()),
|
||||
}
|
||||
}
|
||||
|
||||
func activityTemplateRewardItemsFromProto(items []*activityv1.ActivityTemplateRewardItem) []activityTemplateRewardItemData {
|
||||
out := make([]activityTemplateRewardItemData, 0, len(items))
|
||||
for _, item := range items {
|
||||
out = append(out, activityTemplateRewardItemData{
|
||||
ItemType: item.GetItemType(), ResourceID: item.GetResourceId(), ResourceType: item.GetResourceType(),
|
||||
Name: item.GetName(), IconURL: item.GetIconUrl(), Quantity: item.GetQuantity(), DurationMS: item.GetDurationMs(),
|
||||
WalletAssetType: item.GetWalletAssetType(), WalletAssetAmount: item.GetWalletAssetAmount(), SortOrder: item.GetSortOrder(),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func activityTemplateLeaderboardEntriesFromProto(items []*activityv1.ActivityTemplateLeaderboardEntry, users map[int64]activityTemplateLeaderboardUserData) []activityTemplateLeaderboardEntryData {
|
||||
result := make([]activityTemplateLeaderboardEntryData, 0, len(items))
|
||||
for _, item := range items {
|
||||
result = append(result, activityTemplateLeaderboardEntryFromProto(item, users))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func activityTemplateLeaderboardEntryFromProto(item *activityv1.ActivityTemplateLeaderboardEntry, users map[int64]activityTemplateLeaderboardUserData) activityTemplateLeaderboardEntryData {
|
||||
data := activityTemplateLeaderboardEntryData{}
|
||||
if item == nil {
|
||||
return data
|
||||
}
|
||||
data.RankNo = item.GetRankNo()
|
||||
data.UserID = strconv.FormatInt(item.GetUserId(), 10)
|
||||
data.Score = item.GetScore()
|
||||
data.GiftCount = item.GetGiftCount()
|
||||
data.CoinAmount = item.GetCoinAmount()
|
||||
data.FirstScoredAtMS = item.GetFirstScoredAtMs()
|
||||
data.Snapshot = item.GetSnapshot()
|
||||
if user, ok := users[item.GetUserId()]; ok {
|
||||
data.User = &user
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func activityTemplateLeaderboardUserIDs(items []*activityv1.ActivityTemplateLeaderboardEntry, myEntry *activityv1.ActivityTemplateLeaderboardEntry) []int64 {
|
||||
ids := make([]int64, 0, len(items)+1)
|
||||
for _, item := range items {
|
||||
ids = append(ids, item.GetUserId())
|
||||
}
|
||||
if myEntry != nil {
|
||||
ids = append(ids, myEntry.GetUserId())
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func normalizeOptionalActivityTemplateUTCDay(raw string) (string, bool) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return "", true
|
||||
}
|
||||
return normalizeRequiredActivityTemplateUTCDay(raw)
|
||||
}
|
||||
|
||||
func normalizeRequiredActivityTemplateUTCDay(raw string) (string, bool) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
parsed, err := time.Parse(activityTemplateUTCDayLayout, raw)
|
||||
if err != nil || parsed.Location() != time.UTC || parsed.Format(activityTemplateUTCDayLayout) != raw {
|
||||
return "", false
|
||||
}
|
||||
return raw, true
|
||||
}
|
||||
|
||||
func activityTemplateRequestLocale(request *http.Request) string {
|
||||
// 显式 query 优先于客户端语言头;每一层只有合法 BCP-47 才能截断回退链,非法输入不会原样进入 gRPC 或响应。
|
||||
if locale, valid := canonicalActivityTemplateLanguageTag(request.URL.Query().Get("locale")); valid {
|
||||
return locale
|
||||
}
|
||||
if locale, valid := canonicalActivityTemplateLanguageTag(request.Header.Get("X-App-Language")); valid {
|
||||
return locale
|
||||
}
|
||||
// X-Language 是旧客户端兼容入口,优先级低于规范的 X-App-Language,但仍高于浏览器协商头。
|
||||
if locale, valid := canonicalActivityTemplateLanguageTag(request.Header.Get("X-Language")); valid {
|
||||
return locale
|
||||
}
|
||||
if locale, valid := preferredActivityTemplateAcceptLanguage(request.Header.Get("Accept-Language")); valid {
|
||||
return locale
|
||||
}
|
||||
return "en"
|
||||
}
|
||||
|
||||
func selectActivityTemplateLocale(locales []activityTemplateLocaleData, requested string) (activityTemplateLocaleData, bool) {
|
||||
requested, valid := canonicalActivityTemplateLanguageTag(requested)
|
||||
if !valid {
|
||||
requested = "en"
|
||||
}
|
||||
// exact 保留地区、脚本和扩展差异,例如 zh-CN 与 zh-TW、pt-BR 与 pt-PT 不能提前折叠为基础语言。
|
||||
for _, item := range locales {
|
||||
if strings.EqualFold(item.Locale, requested) {
|
||||
return item, true
|
||||
}
|
||||
}
|
||||
requestedBase := activityTemplateLanguageBase(requested)
|
||||
if requestedBase != "" {
|
||||
// 优先使用运营明确配置的 language-only 文案,再选择同语言的第一个候选;配置顺序即稳定决胜顺序。
|
||||
for _, item := range locales {
|
||||
if strings.EqualFold(item.Locale, requestedBase) {
|
||||
return item, true
|
||||
}
|
||||
}
|
||||
for _, item := range locales {
|
||||
if activityTemplateLanguageBase(item.Locale) == requestedBase {
|
||||
return item, true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, item := range locales {
|
||||
if strings.EqualFold(strings.TrimSpace(item.Locale), "en") {
|
||||
return item, true
|
||||
}
|
||||
}
|
||||
for _, item := range locales {
|
||||
if activityTemplateLanguageBase(item.Locale) == "en" {
|
||||
return item, true
|
||||
}
|
||||
}
|
||||
if len(locales) > 0 {
|
||||
return locales[0], true
|
||||
}
|
||||
return activityTemplateLocaleData{}, false
|
||||
}
|
||||
|
||||
// canonicalActivityTemplateLanguageTag 在 language.Parse 前做 ASCII/分段约束。x/text 会为兼容 POSIX
|
||||
// 接受下划线并尝试修复;HTTP 边界不能把这类非 BCP-47 输入静默改写后继续暴露。
|
||||
func canonicalActivityTemplateLanguageTag(raw string) (string, bool) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" || len(raw) > 255 || raw == "*" || raw[0] == '-' || raw[len(raw)-1] == '-' {
|
||||
return "", false
|
||||
}
|
||||
segmentLength := 0
|
||||
for index := 0; index < len(raw); index++ {
|
||||
char := raw[index]
|
||||
if char == '-' {
|
||||
if segmentLength == 0 || segmentLength > 8 {
|
||||
return "", false
|
||||
}
|
||||
segmentLength = 0
|
||||
continue
|
||||
}
|
||||
if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9')) {
|
||||
return "", false
|
||||
}
|
||||
segmentLength++
|
||||
}
|
||||
if segmentLength == 0 || segmentLength > 8 {
|
||||
return "", false
|
||||
}
|
||||
tag, err := language.Parse(raw)
|
||||
if err != nil || tag.String() == "" || tag.String() == "mul" {
|
||||
return "", false
|
||||
}
|
||||
return tag.String(), true
|
||||
}
|
||||
|
||||
func activityTemplateLanguageBase(locale string) string {
|
||||
tag, err := language.Parse(locale)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
base, confidence := tag.Base()
|
||||
if confidence == language.No || base.String() == "und" {
|
||||
return ""
|
||||
}
|
||||
return base.String()
|
||||
}
|
||||
|
||||
// preferredActivityTemplateAcceptLanguage applies RFC quality weights without accepting wildcard or malformed
|
||||
// language ranges. Equal weights preserve header order; q=0 means explicitly unacceptable and is skipped.
|
||||
func preferredActivityTemplateAcceptLanguage(raw string) (string, bool) {
|
||||
bestLocale := ""
|
||||
bestQuality := -1.0
|
||||
for _, entry := range strings.Split(raw, ",") {
|
||||
parts := strings.Split(entry, ";")
|
||||
locale, valid := canonicalActivityTemplateLanguageTag(parts[0])
|
||||
if !valid {
|
||||
continue
|
||||
}
|
||||
quality := 1.0
|
||||
validQuality := true
|
||||
for _, parameter := range parts[1:] {
|
||||
name, value, found := strings.Cut(strings.TrimSpace(parameter), "=")
|
||||
if !found || !strings.EqualFold(strings.TrimSpace(name), "q") {
|
||||
continue
|
||||
}
|
||||
parsed, err := strconv.ParseFloat(strings.TrimSpace(value), 64)
|
||||
if err != nil || math.IsNaN(parsed) || math.IsInf(parsed, 0) || parsed < 0 || parsed > 1 {
|
||||
validQuality = false
|
||||
break
|
||||
}
|
||||
quality = parsed
|
||||
}
|
||||
if !validQuality || quality <= 0 || quality <= bestQuality {
|
||||
continue
|
||||
}
|
||||
bestLocale = locale
|
||||
bestQuality = quality
|
||||
}
|
||||
return bestLocale, bestLocale != ""
|
||||
}
|
||||
@ -0,0 +1,567 @@
|
||||
package activityapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
userv1 "hyapp.local/api/proto/user/v1"
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/services/gateway-service/internal/auth"
|
||||
)
|
||||
|
||||
func TestGetCurrentActivityTemplateUsesAuthenticatedProfileRegionAndPublishedDTO(t *testing.T) {
|
||||
runtimeClient := &fakeActivityTemplateRuntimeClient{
|
||||
currentResp: &activityv1.GetCurrentActivityTemplateResponse{
|
||||
Found: true,
|
||||
Runtime: &activityv1.ActivityTemplateRuntime{
|
||||
VersionNo: 4, RuntimeFromMs: 1000, RuntimeToMs: 9000, ServerTimeMs: 2000,
|
||||
Template: &activityv1.ActivityTemplate{
|
||||
TemplateId: "tpl-1", TemplateCode: "ramadan-2026", Name: "admin-only-name",
|
||||
ActivityType: "gift_challenge", Status: "published", LifecycleStatus: "running",
|
||||
StartMs: 1000, EndMs: 9000, AllRegions: false, RegionIds: []int64{901},
|
||||
Locales: []*activityv1.ActivityTemplateLocale{
|
||||
{Locale: "en", Title: "Gift challenge", Rules: "rules-en"},
|
||||
{Locale: "ar", Title: "تحدي الهدايا", Rules: "rules-ar"},
|
||||
},
|
||||
Gifts: []*activityv1.ActivityTemplateGift{{GiftId: "gift-1", Name: "Rose", IconUrl: "rose.png", CoinPrice: 20, PriceVersion: "v3"}},
|
||||
Tasks: []*activityv1.ActivityTemplateTask{{TaskKey: "visit", TaskType: "daily_visit", TargetValue: 1, RewardResourceGroupId: 81}},
|
||||
RankRewards: []*activityv1.ActivityTemplateRankReward{{BoardType: "daily", RankFrom: 1, RankTo: 1, ResourceGroupId: 82}},
|
||||
Assets: []*activityv1.ActivityTemplateAsset{{AssetKey: "hero", Locale: "ar", Url: "hero.webp", MediaType: "webp"}},
|
||||
DisplayConfigJson: `{"theme":"gold"}`, Revision: 99, CreatedByAdminId: 7,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
profileClient := &fakeActivityTemplateProfileClient{regionID: 901}
|
||||
handler := New(Config{ActivityTemplateRuntime: runtimeClient, UserProfileClient: profileClient})
|
||||
request := activityTemplateRequest(http.MethodGet, "/api/v1/activities/templates/current?locale=ar-EG®ion_id=999999", "", 42001)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handler.getCurrentActivityTemplate(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status mismatch: got=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if profileClient.lastGet.GetUserId() != 42001 || profileClient.lastGet.GetMeta().GetAppCode() != "lalu" {
|
||||
t.Fatalf("trusted region profile request mismatch: %+v", profileClient.lastGet)
|
||||
}
|
||||
if runtimeClient.lastCurrent.GetUserId() != 42001 || runtimeClient.lastCurrent.GetRegionId() != 901 || runtimeClient.lastCurrent.GetLocale() != "ar-EG" {
|
||||
t.Fatalf("runtime request mismatch: %+v", runtimeClient.lastCurrent)
|
||||
}
|
||||
if runtimeClient.lastCurrent.GetMeta().GetAppCode() != "lalu" || runtimeClient.lastCurrent.GetMeta().GetRequestId() == "" {
|
||||
t.Fatalf("activity meta mismatch: %+v", runtimeClient.lastCurrent.GetMeta())
|
||||
}
|
||||
|
||||
var envelope struct {
|
||||
Code string `json:"code"`
|
||||
Data struct {
|
||||
Found bool `json:"found"`
|
||||
Runtime struct {
|
||||
TemplateID string `json:"template_id"`
|
||||
VersionNo int64 `json:"version_no"`
|
||||
Locale struct {
|
||||
Locale string `json:"locale"`
|
||||
Title string `json:"title"`
|
||||
} `json:"locale"`
|
||||
DisplayConfig map[string]any `json:"display_config"`
|
||||
} `json:"runtime"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if envelope.Code != "OK" || !envelope.Data.Found || envelope.Data.Runtime.TemplateID != "tpl-1" || envelope.Data.Runtime.VersionNo != 4 {
|
||||
t.Fatalf("runtime envelope mismatch: %+v", envelope)
|
||||
}
|
||||
if envelope.Data.Runtime.Locale.Locale != "ar" || envelope.Data.Runtime.Locale.Title != "تحدي الهدايا" || envelope.Data.Runtime.DisplayConfig["theme"] != "gold" {
|
||||
t.Fatalf("locale/display config mismatch: %+v", envelope.Data.Runtime)
|
||||
}
|
||||
var rawEnvelope map[string]any
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &rawEnvelope); err != nil {
|
||||
t.Fatalf("decode raw response: %v", err)
|
||||
}
|
||||
runtime := rawEnvelope["data"].(map[string]any)["runtime"].(map[string]any)
|
||||
for _, adminField := range []string{"name", "revision", "created_by_admin_id", "updated_by_admin_id", "published_by_admin_id"} {
|
||||
if _, exists := runtime[adminField]; exists {
|
||||
t.Fatalf("app runtime leaked admin field %q: %+v", adminField, runtime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentActivityTemplateLocaleFallsBackToEnglish(t *testing.T) {
|
||||
runtimeClient := &fakeActivityTemplateRuntimeClient{
|
||||
currentResp: &activityv1.GetCurrentActivityTemplateResponse{
|
||||
Found: true,
|
||||
Runtime: &activityv1.ActivityTemplateRuntime{Template: &activityv1.ActivityTemplate{
|
||||
TemplateId: "tpl-1",
|
||||
Locales: []*activityv1.ActivityTemplateLocale{
|
||||
{Locale: "ar", Title: "العربية"},
|
||||
{Locale: "en", Title: "English fallback"},
|
||||
},
|
||||
}},
|
||||
},
|
||||
}
|
||||
profileClient := &fakeActivityTemplateProfileClient{regionID: 901}
|
||||
handler := New(Config{ActivityTemplateRuntime: runtimeClient, UserProfileClient: profileClient})
|
||||
request := activityTemplateRequest(http.MethodGet, "/api/v1/activities/templates/current", "", 42001)
|
||||
request.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handler.getCurrentActivityTemplate(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK || runtimeClient.lastCurrent.GetLocale() != "zh-CN" {
|
||||
t.Fatalf("requested locale should be normalized before owner call: code=%d req=%+v body=%s", recorder.Code, runtimeClient.lastCurrent, recorder.Body.String())
|
||||
}
|
||||
var envelope struct {
|
||||
Data struct {
|
||||
Runtime struct {
|
||||
Locale struct {
|
||||
Locale string `json:"locale"`
|
||||
Title string `json:"title"`
|
||||
} `json:"locale"`
|
||||
} `json:"runtime"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if envelope.Data.Runtime.Locale.Locale != "en" || envelope.Data.Runtime.Locale.Title != "English fallback" {
|
||||
t.Fatalf("unsupported locale must fall back to published English copy: %+v", envelope.Data.Runtime.Locale)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityTemplateRequestLocalePreservesCanonicalBCP47AndSourcePriority(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
target string
|
||||
appLanguage string
|
||||
acceptLanguage string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "query keeps Chinese region", target: "/current?locale=zh-cn",
|
||||
appLanguage: "pt-BR", acceptLanguage: "ar;q=1", want: "zh-CN",
|
||||
},
|
||||
{
|
||||
name: "app header keeps Portuguese region", target: "/current",
|
||||
appLanguage: "pt-br", acceptLanguage: "zh-CN;q=1", want: "pt-BR",
|
||||
},
|
||||
{
|
||||
name: "accept language uses highest quality", target: "/current",
|
||||
acceptLanguage: "fr-FR;q=0.2, zh-CN;q=0.7, pt-BR;q=0.9", want: "pt-BR",
|
||||
},
|
||||
{
|
||||
name: "equal quality keeps client order", target: "/current",
|
||||
acceptLanguage: "zh-CN;q=0.8, pt-BR;q=0.8", want: "zh-CN",
|
||||
},
|
||||
{
|
||||
name: "invalid tags and q zero are never forwarded", target: "/current?locale=bad_tag",
|
||||
appLanguage: "pt@@BR", acceptLanguage: "zh_CN;q=1, pt-BR;q=0, ar;q=NaN, en-US;q=0.5", want: "en-US",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
request := httptest.NewRequest(http.MethodGet, tt.target, nil)
|
||||
request.Header.Set("X-App-Language", tt.appLanguage)
|
||||
request.Header.Set("Accept-Language", tt.acceptLanguage)
|
||||
if got := activityTemplateRequestLocale(request); got != tt.want {
|
||||
t.Fatalf("activityTemplateRequestLocale() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectActivityTemplateLocaleUsesExactBaseSameLanguageThenEnglish(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
requested string
|
||||
locales []activityTemplateLocaleData
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "exact region", requested: "zh-CN",
|
||||
locales: []activityTemplateLocaleData{{Locale: "zh-TW"}, {Locale: "zh-CN"}, {Locale: "en"}}, want: "zh-CN",
|
||||
},
|
||||
{
|
||||
name: "language only before regional candidate", requested: "pt-AO",
|
||||
locales: []activityTemplateLocaleData{{Locale: "pt-BR"}, {Locale: "pt"}, {Locale: "en"}}, want: "pt",
|
||||
},
|
||||
{
|
||||
name: "same language uses configured order", requested: "pt-AO",
|
||||
locales: []activityTemplateLocaleData{{Locale: "pt-BR"}, {Locale: "pt-PT"}, {Locale: "en"}}, want: "pt-BR",
|
||||
},
|
||||
{
|
||||
name: "English after no language match", requested: "zh-CN",
|
||||
locales: []activityTemplateLocaleData{{Locale: "ar"}, {Locale: "en"}}, want: "en",
|
||||
},
|
||||
{
|
||||
name: "first after no English", requested: "zh-CN",
|
||||
locales: []activityTemplateLocaleData{{Locale: "ar"}, {Locale: "pt-BR"}}, want: "ar",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
selected, found := selectActivityTemplateLocale(tt.locales, tt.requested)
|
||||
if !found || selected.Locale != tt.want {
|
||||
t.Fatalf("selectActivityTemplateLocale() = (%+v, %v), want locale %q", selected, found, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityTemplateRuntimeDropsInvalidPublishedLocaleTags(t *testing.T) {
|
||||
runtime := activityTemplateRuntimeFromProto(&activityv1.ActivityTemplateRuntime{Template: &activityv1.ActivityTemplate{
|
||||
TemplateId: "tpl-1",
|
||||
Locales: []*activityv1.ActivityTemplateLocale{
|
||||
{Locale: "bad_tag", Title: "must not leak"},
|
||||
{Locale: "pt-br", Title: "Português"},
|
||||
},
|
||||
Assets: []*activityv1.ActivityTemplateAsset{
|
||||
{AssetKey: "invalid", Locale: "zh_CN", Url: "invalid.webp"},
|
||||
{AssetKey: "valid", Locale: "pt-br", Url: "valid.webp"},
|
||||
},
|
||||
}}, "pt-BR")
|
||||
if len(runtime.Locales) != 1 || runtime.Locales[0].Locale != "pt-BR" || runtime.Locale == nil || runtime.Locale.Locale != "pt-BR" {
|
||||
t.Fatalf("runtime locale sanitization mismatch: %+v", runtime)
|
||||
}
|
||||
if len(runtime.Assets) != 1 || runtime.Assets[0].Locale != "pt-BR" || runtime.Assets[0].AssetKey != "valid" {
|
||||
t.Fatalf("runtime asset locale sanitization mismatch: %+v", runtime.Assets)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVisitActivityTemplateKeepsClientCommandSeparateFromRequestID(t *testing.T) {
|
||||
runtimeClient := &fakeActivityTemplateRuntimeClient{visitResp: &activityv1.VisitActivityTemplateResponse{
|
||||
FirstVisit: true,
|
||||
Tasks: []*activityv1.ActivityTemplateRuntimeTask{{TemplateId: "tpl-1", TemplateCode: "summer", TaskDay: "2026-07-14", TaskKey: "visit", Status: "completed", Claimable: true}},
|
||||
}}
|
||||
handler := New(Config{ActivityTemplateRuntime: runtimeClient, UserProfileClient: &fakeActivityTemplateProfileClient{regionID: 66}})
|
||||
request := activityTemplateRequest(http.MethodPost, "/api/v1/activities/templates/summer/visit", `{"command_id":" visit-device-7 ","version_no":4}`, 777)
|
||||
request.SetPathValue("template_code", "summer")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handler.visitActivityTemplate(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status mismatch: got=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
req := runtimeClient.lastVisit
|
||||
if req.GetUserId() != 777 || req.GetRegionId() != 66 || req.GetTemplateCode() != "summer" || req.GetCommandId() != "visit-device-7" || req.GetVersionNo() != 4 {
|
||||
t.Fatalf("visit request mismatch: %+v", req)
|
||||
}
|
||||
if req.GetMeta().GetRequestId() == "" || req.GetMeta().GetRequestId() == req.GetCommandId() {
|
||||
t.Fatalf("request_id and command_id must have independent semantics: meta=%+v command=%q", req.GetMeta(), req.GetCommandId())
|
||||
}
|
||||
var envelope struct {
|
||||
Data struct {
|
||||
FirstVisit bool `json:"first_visit"`
|
||||
Tasks []struct {
|
||||
TaskDay string `json:"task_day"`
|
||||
Claimable bool `json:"claimable"`
|
||||
} `json:"tasks"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil || !envelope.Data.FirstVisit || len(envelope.Data.Tasks) != 1 || !envelope.Data.Tasks[0].Claimable {
|
||||
t.Fatalf("visit response mismatch: err=%v data=%+v", err, envelope.Data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityTemplateTasksAcceptOnlyCanonicalUTCDay(t *testing.T) {
|
||||
t.Run("forwards canonical UTC day", func(t *testing.T) {
|
||||
runtimeClient := &fakeActivityTemplateRuntimeClient{tasksResp: &activityv1.ListActivityTemplateTasksResponse{NextRefreshAtMs: 1784159999000}}
|
||||
handler := New(Config{ActivityTemplateRuntime: runtimeClient, UserProfileClient: &fakeActivityTemplateProfileClient{regionID: 22}})
|
||||
request := activityTemplateRequest(http.MethodGet, "/api/v1/activities/templates/summer/tasks?task_day=2026-07-14", "", 88)
|
||||
request.SetPathValue("template_code", "summer")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handler.listActivityTemplateTasks(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK || runtimeClient.lastTasks.GetTaskDay() != "2026-07-14" {
|
||||
t.Fatalf("canonical UTC day mismatch: code=%d req=%+v body=%s", recorder.Code, runtimeClient.lastTasks, recorder.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
for _, invalid := range []string{"2026-7-14", "2026-07-14T00:00:00Z", "2026-02-30"} {
|
||||
t.Run(invalid, func(t *testing.T) {
|
||||
runtimeClient := &fakeActivityTemplateRuntimeClient{}
|
||||
handler := New(Config{ActivityTemplateRuntime: runtimeClient, UserProfileClient: &fakeActivityTemplateProfileClient{regionID: 22}})
|
||||
request := activityTemplateRequest(http.MethodGet, "/api/v1/activities/templates/summer/tasks?task_day="+invalid, "", 88)
|
||||
request.SetPathValue("template_code", "summer")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handler.listActivityTemplateTasks(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusBadRequest || runtimeClient.lastTasks != nil {
|
||||
t.Fatalf("invalid day should stop before gRPC: code=%d req=%+v body=%s", recorder.Code, runtimeClient.lastTasks, recorder.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHistoricalActivityTemplateTasksUseOwnedProgressAttributionInsteadOfCurrentProfileRegion(t *testing.T) {
|
||||
runtimeClient := &fakeActivityTemplateRuntimeClient{tasksResp: &activityv1.ListActivityTemplateTasksResponse{}}
|
||||
profileClient := &fakeActivityTemplateProfileClient{regionID: 999}
|
||||
handler := New(Config{ActivityTemplateRuntime: runtimeClient, UserProfileClient: profileClient})
|
||||
request := activityTemplateRequest(http.MethodGet, "/api/v1/activities/templates/summer/tasks?version_no=3&task_day=2026-07-14", "", 88)
|
||||
request.SetPathValue("template_code", "summer")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handler.listActivityTemplateTasks(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK || runtimeClient.lastTasks.GetVersionNo() != 3 || runtimeClient.lastTasks.GetRegionId() != 0 {
|
||||
t.Fatalf("historical task lookup must be owner-attributed: code=%d req=%+v body=%s", recorder.Code, runtimeClient.lastTasks, recorder.Body.String())
|
||||
}
|
||||
if profileClient.lastGet != nil {
|
||||
t.Fatalf("historical task lookup must not resolve mutable current region: %+v", profileClient.lastGet)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaimActivityTemplateTaskUsesAuthenticatedUserAndExplicitIdempotency(t *testing.T) {
|
||||
runtimeClient := &fakeActivityTemplateRuntimeClient{claimResp: &activityv1.ClaimActivityTemplateTaskRewardResponse{
|
||||
Claimed: true,
|
||||
Claim: &activityv1.ActivityTemplateTaskClaim{ClaimId: "claim-1", CommandId: "claim-device-1", UserId: 908, TaskDay: "2026-07-14", TaskKey: "gift-10", Status: "granted"},
|
||||
}}
|
||||
handler := New(Config{ActivityTemplateRuntime: runtimeClient, UserProfileClient: &fakeActivityTemplateProfileClient{regionID: 33}})
|
||||
request := activityTemplateRequest(http.MethodPost, "/api/v1/activities/templates/summer/tasks/gift-10/claim", `{"task_day":"2026-07-14","command_id":"claim-device-1","version_no":3,"user_id":1,"region_id":1}`, 908)
|
||||
request.SetPathValue("template_code", "summer")
|
||||
request.SetPathValue("task_key", "gift-10")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handler.claimActivityTemplateTask(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status mismatch: got=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
req := runtimeClient.lastClaim
|
||||
if req.GetUserId() != 908 || req.GetRegionId() != 0 || req.GetVersionNo() != 3 || req.GetTaskDay() != "2026-07-14" || req.GetTaskKey() != "gift-10" || req.GetCommandId() != "claim-device-1" {
|
||||
t.Fatalf("claim request trusted fields mismatch: %+v", req)
|
||||
}
|
||||
if profile := handler.userProfileClient.(*fakeActivityTemplateProfileClient); profile.lastGet != nil {
|
||||
t.Fatalf("exact-version claim must not resolve mutable current region: %+v", profile.lastGet)
|
||||
}
|
||||
var envelope struct {
|
||||
Data struct {
|
||||
Claimed bool `json:"claimed"`
|
||||
Claim struct {
|
||||
UserID string `json:"user_id"`
|
||||
Status string `json:"status"`
|
||||
} `json:"claim"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil || !envelope.Data.Claimed || envelope.Data.Claim.UserID != "908" || envelope.Data.Claim.Status != "granted" {
|
||||
t.Fatalf("claim response mismatch: err=%v data=%+v", err, envelope.Data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityTemplateLeaderboardClampsPageAndBatchEnrichesProfiles(t *testing.T) {
|
||||
runtimeClient := &fakeActivityTemplateRuntimeClient{leaderboardResp: &activityv1.ListActivityTemplateLeaderboardResponse{
|
||||
BoardType: "daily", PeriodKey: "2026-07-14", PeriodStartMs: 1000, PeriodEndMs: 2000, PeriodStatus: "pending", Total: 2,
|
||||
Entries: []*activityv1.ActivityTemplateLeaderboardEntry{{RankNo: 1, UserId: 1001, Score: 800, GiftCount: 8, CoinAmount: 800, FirstScoredAtMs: 1100}},
|
||||
MyEntry: &activityv1.ActivityTemplateLeaderboardEntry{RankNo: 2, UserId: 2002, Score: 700, GiftCount: 7, CoinAmount: 700, FirstScoredAtMs: 1200},
|
||||
}}
|
||||
profileClient := &fakeActivityTemplateProfileClient{
|
||||
regionID: 45,
|
||||
users: map[int64]*userv1.User{
|
||||
1001: {UserId: 1001, DisplayUserId: "L1001", Username: "first", Avatar: "first.png"},
|
||||
2002: {UserId: 2002, DisplayUserId: "L2002", Username: "me", Avatar: "me.png"},
|
||||
},
|
||||
}
|
||||
handler := New(Config{ActivityTemplateRuntime: runtimeClient, UserProfileClient: profileClient})
|
||||
request := activityTemplateRequest(http.MethodGet, "/api/v1/activities/templates/summer/leaderboards/daily?version_no=4&period_key=2026-07-14&page=2&page_size=999", "", 2002)
|
||||
request.SetPathValue("template_code", "summer")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handler.listActivityTemplateDailyLeaderboard(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status mismatch: got=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
req := runtimeClient.lastLeaderboard
|
||||
if req.GetUserId() != 2002 || req.GetRegionId() != 0 || req.GetVersionNo() != 4 || req.GetBoardType() != "daily" || req.GetPeriodKey() != "2026-07-14" || req.GetPage() != 2 || req.GetPageSize() != 100 {
|
||||
t.Fatalf("leaderboard request mismatch: %+v", req)
|
||||
}
|
||||
if profileClient.lastGet != nil {
|
||||
t.Fatalf("exact-version leaderboard must not resolve mutable current region: %+v", profileClient.lastGet)
|
||||
}
|
||||
if got := profileClient.lastBatch.GetUserIds(); len(got) != 2 || got[0] != 1001 || got[1] != 2002 {
|
||||
t.Fatalf("profile batch should contain page and my-entry users once: %+v", got)
|
||||
}
|
||||
var envelope struct {
|
||||
Data struct {
|
||||
Page int32 `json:"page"`
|
||||
PageSize int32 `json:"page_size"`
|
||||
Entries []struct {
|
||||
UserID string `json:"user_id"`
|
||||
User struct {
|
||||
DisplayUserID string `json:"display_user_id"`
|
||||
Username string `json:"username"`
|
||||
} `json:"user"`
|
||||
} `json:"entries"`
|
||||
MyEntry struct {
|
||||
User struct {
|
||||
Avatar string `json:"avatar"`
|
||||
} `json:"user"`
|
||||
} `json:"my_entry"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if envelope.Data.Page != 2 || envelope.Data.PageSize != 100 || len(envelope.Data.Entries) != 1 || envelope.Data.Entries[0].UserID != "1001" || envelope.Data.Entries[0].User.DisplayUserID != "L1001" || envelope.Data.MyEntry.User.Avatar != "me.png" {
|
||||
t.Fatalf("leaderboard response mismatch: %+v", envelope.Data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDailyLeaderboardForwardsExplicitHistoricalUTCPeriod(t *testing.T) {
|
||||
runtimeClient := &fakeActivityTemplateRuntimeClient{leaderboardResp: &activityv1.ListActivityTemplateLeaderboardResponse{
|
||||
BoardType: "daily", PeriodKey: "2026-06-01", PeriodStatus: "settled",
|
||||
Entries: []*activityv1.ActivityTemplateLeaderboardEntry{{RankNo: 1, UserId: 1001, Snapshot: true}},
|
||||
}}
|
||||
handler := New(Config{ActivityTemplateRuntime: runtimeClient, UserProfileClient: &fakeActivityTemplateProfileClient{regionID: 45}})
|
||||
request := activityTemplateRequest(http.MethodGet, "/api/v1/activities/templates/summer/leaderboards/daily?period_key=2026-06-01", "", 2002)
|
||||
request.SetPathValue("template_code", "summer")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handler.listActivityTemplateDailyLeaderboard(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("settled historical period should reach owner: code=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if runtimeClient.lastLeaderboard.GetPeriodKey() != "2026-06-01" || runtimeClient.lastLeaderboard.GetBoardType() != "daily" {
|
||||
t.Fatalf("gateway must preserve explicit historical UTC period: %+v", runtimeClient.lastLeaderboard)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDailyLeaderboardRejectsNonCanonicalUTCPeriod(t *testing.T) {
|
||||
for _, invalid := range []string{"2026-6-1", "2026-06-01T00:00:00Z", "2026-02-30"} {
|
||||
t.Run(invalid, func(t *testing.T) {
|
||||
runtimeClient := &fakeActivityTemplateRuntimeClient{}
|
||||
handler := New(Config{ActivityTemplateRuntime: runtimeClient, UserProfileClient: &fakeActivityTemplateProfileClient{regionID: 45}})
|
||||
request := activityTemplateRequest(http.MethodGet, "/api/v1/activities/templates/summer/leaderboards/daily?period_key="+invalid, "", 2002)
|
||||
request.SetPathValue("template_code", "summer")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handler.listActivityTemplateDailyLeaderboard(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusBadRequest || runtimeClient.lastLeaderboard != nil {
|
||||
t.Fatalf("non-canonical UTC period must stop before owner: code=%d req=%+v body=%s", recorder.Code, runtimeClient.lastLeaderboard, recorder.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityTemplateLeaderboardProfileDecorationFailureDegradesToFacts(t *testing.T) {
|
||||
runtimeClient := &fakeActivityTemplateRuntimeClient{leaderboardResp: &activityv1.ListActivityTemplateLeaderboardResponse{
|
||||
BoardType: "total", PeriodKey: "total", Total: 1,
|
||||
Entries: []*activityv1.ActivityTemplateLeaderboardEntry{{RankNo: 1, UserId: 1001, Score: 800}},
|
||||
}}
|
||||
profileClient := &fakeActivityTemplateProfileClient{regionID: 45, batchErr: errors.New("user profile unavailable")}
|
||||
handler := New(Config{ActivityTemplateRuntime: runtimeClient, UserProfileClient: profileClient})
|
||||
request := activityTemplateRequest(http.MethodGet, "/api/v1/activities/templates/summer/leaderboards/total", "", 2002)
|
||||
request.SetPathValue("template_code", "summer")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handler.listActivityTemplateTotalLeaderboard(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK || runtimeClient.lastLeaderboard.GetPeriodKey() != "total" {
|
||||
t.Fatalf("profile decoration must not hide leaderboard facts: code=%d req=%+v body=%s", recorder.Code, runtimeClient.lastLeaderboard, recorder.Body.String())
|
||||
}
|
||||
var envelope struct {
|
||||
Data struct {
|
||||
Entries []map[string]any `json:"entries"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil || len(envelope.Data.Entries) != 1 {
|
||||
t.Fatalf("decode facts response: err=%v data=%+v", err, envelope.Data)
|
||||
}
|
||||
if _, decorated := envelope.Data.Entries[0]["user"]; decorated {
|
||||
t.Fatalf("failed best-effort profile lookup should omit user decoration: %+v", envelope.Data.Entries[0])
|
||||
}
|
||||
}
|
||||
|
||||
func activityTemplateRequest(method, target, body string, userID int64) *http.Request {
|
||||
request := httptest.NewRequest(method, target, strings.NewReader(body))
|
||||
ctx := appcode.WithContext(request.Context(), "lalu")
|
||||
ctx = auth.WithUserID(ctx, userID)
|
||||
return request.WithContext(ctx)
|
||||
}
|
||||
|
||||
type fakeActivityTemplateRuntimeClient struct {
|
||||
currentResp *activityv1.GetCurrentActivityTemplateResponse
|
||||
visitResp *activityv1.VisitActivityTemplateResponse
|
||||
tasksResp *activityv1.ListActivityTemplateTasksResponse
|
||||
claimResp *activityv1.ClaimActivityTemplateTaskRewardResponse
|
||||
leaderboardResp *activityv1.ListActivityTemplateLeaderboardResponse
|
||||
|
||||
lastCurrent *activityv1.GetCurrentActivityTemplateRequest
|
||||
lastVisit *activityv1.VisitActivityTemplateRequest
|
||||
lastTasks *activityv1.ListActivityTemplateTasksRequest
|
||||
lastClaim *activityv1.ClaimActivityTemplateTaskRewardRequest
|
||||
lastLeaderboard *activityv1.ListActivityTemplateLeaderboardRequest
|
||||
}
|
||||
|
||||
func (f *fakeActivityTemplateRuntimeClient) GetCurrentActivityTemplate(_ context.Context, req *activityv1.GetCurrentActivityTemplateRequest) (*activityv1.GetCurrentActivityTemplateResponse, error) {
|
||||
f.lastCurrent = req
|
||||
if f.currentResp != nil {
|
||||
return f.currentResp, nil
|
||||
}
|
||||
return &activityv1.GetCurrentActivityTemplateResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeActivityTemplateRuntimeClient) VisitActivityTemplate(_ context.Context, req *activityv1.VisitActivityTemplateRequest) (*activityv1.VisitActivityTemplateResponse, error) {
|
||||
f.lastVisit = req
|
||||
if f.visitResp != nil {
|
||||
return f.visitResp, nil
|
||||
}
|
||||
return &activityv1.VisitActivityTemplateResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeActivityTemplateRuntimeClient) ListActivityTemplateTasks(_ context.Context, req *activityv1.ListActivityTemplateTasksRequest) (*activityv1.ListActivityTemplateTasksResponse, error) {
|
||||
f.lastTasks = req
|
||||
if f.tasksResp != nil {
|
||||
return f.tasksResp, nil
|
||||
}
|
||||
return &activityv1.ListActivityTemplateTasksResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeActivityTemplateRuntimeClient) ClaimActivityTemplateTaskReward(_ context.Context, req *activityv1.ClaimActivityTemplateTaskRewardRequest) (*activityv1.ClaimActivityTemplateTaskRewardResponse, error) {
|
||||
f.lastClaim = req
|
||||
if f.claimResp != nil {
|
||||
return f.claimResp, nil
|
||||
}
|
||||
return &activityv1.ClaimActivityTemplateTaskRewardResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeActivityTemplateRuntimeClient) ListActivityTemplateLeaderboard(_ context.Context, req *activityv1.ListActivityTemplateLeaderboardRequest) (*activityv1.ListActivityTemplateLeaderboardResponse, error) {
|
||||
f.lastLeaderboard = req
|
||||
if f.leaderboardResp != nil {
|
||||
return f.leaderboardResp, nil
|
||||
}
|
||||
return &activityv1.ListActivityTemplateLeaderboardResponse{}, nil
|
||||
}
|
||||
|
||||
type fakeActivityTemplateProfileClient struct {
|
||||
regionID int64
|
||||
users map[int64]*userv1.User
|
||||
batchErr error
|
||||
lastGet *userv1.GetUserRequest
|
||||
lastBatch *userv1.BatchGetUsersRequest
|
||||
}
|
||||
|
||||
func (f *fakeActivityTemplateProfileClient) GetUser(_ context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) {
|
||||
f.lastGet = req
|
||||
return &userv1.GetUserResponse{User: &userv1.User{UserId: req.GetUserId(), RegionId: f.regionID}}, nil
|
||||
}
|
||||
|
||||
func (f *fakeActivityTemplateProfileClient) BatchGetUsers(_ context.Context, req *userv1.BatchGetUsersRequest) (*userv1.BatchGetUsersResponse, error) {
|
||||
f.lastBatch = req
|
||||
if f.batchErr != nil {
|
||||
return nil, f.batchErr
|
||||
}
|
||||
return &userv1.BatchGetUsersResponse{Users: f.users}, nil
|
||||
}
|
||||
@ -3,6 +3,7 @@ package activityapi
|
||||
import (
|
||||
"context"
|
||||
|
||||
userv1 "hyapp.local/api/proto/user/v1"
|
||||
"hyapp/pkg/userleaderboard"
|
||||
"hyapp/services/gateway-service/internal/client"
|
||||
"hyapp/services/gateway-service/internal/transport/http/httproutes"
|
||||
@ -14,7 +15,7 @@ type Handler struct {
|
||||
taskClient client.TaskClient
|
||||
growthLevelClient client.GrowthLevelClient
|
||||
achievementClient client.AchievementClient
|
||||
userProfileClient client.UserProfileClient
|
||||
userProfileClient UserProfileReader
|
||||
roomQueryClient client.RoomQueryClient
|
||||
walletClient client.WalletClient
|
||||
userLeaderboard UserLeaderboardStore
|
||||
@ -29,6 +30,8 @@ type Handler struct {
|
||||
cpWeeklyRank client.CPWeeklyRankClient
|
||||
agencyOpening client.AgencyOpeningClient
|
||||
wheel client.WheelClient
|
||||
|
||||
activityTemplateRuntime client.ActivityTemplateRuntimeClient
|
||||
}
|
||||
|
||||
type UserLeaderboardStore interface {
|
||||
@ -36,11 +39,19 @@ type UserLeaderboardStore interface {
|
||||
RankForUser(ctx context.Context, query userleaderboard.Query, userID int64) (userleaderboard.Entry, bool, error)
|
||||
}
|
||||
|
||||
// UserProfileReader keeps activity HTTP composition on the two profile operations it actually needs.
|
||||
// The concrete gRPC client exposes more user commands, but widening this boundary would let activity handlers
|
||||
// accidentally mutate user ownership while only resolving a trusted region and leaderboard presentation data.
|
||||
type UserProfileReader interface {
|
||||
GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error)
|
||||
BatchGetUsers(ctx context.Context, req *userv1.BatchGetUsersRequest) (*userv1.BatchGetUsersResponse, error)
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
TaskClient client.TaskClient
|
||||
GrowthLevelClient client.GrowthLevelClient
|
||||
AchievementClient client.AchievementClient
|
||||
UserProfileClient client.UserProfileClient
|
||||
UserProfileClient UserProfileReader
|
||||
RoomQueryClient client.RoomQueryClient
|
||||
WalletClient client.WalletClient
|
||||
UserLeaderboard UserLeaderboardStore
|
||||
@ -55,6 +66,8 @@ type Config struct {
|
||||
CPWeeklyRank client.CPWeeklyRankClient
|
||||
AgencyOpening client.AgencyOpeningClient
|
||||
Wheel client.WheelClient
|
||||
|
||||
ActivityTemplateRuntime client.ActivityTemplateRuntimeClient
|
||||
}
|
||||
|
||||
func New(config Config) *Handler {
|
||||
@ -77,6 +90,8 @@ func New(config Config) *Handler {
|
||||
cpWeeklyRank: config.CPWeeklyRank,
|
||||
agencyOpening: config.AgencyOpening,
|
||||
wheel: config.Wheel,
|
||||
|
||||
activityTemplateRuntime: config.ActivityTemplateRuntime,
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,6 +121,13 @@ func (h *Handler) TaskHandlers() httproutes.TaskHandlers {
|
||||
DrawWheel: h.drawWheel,
|
||||
ListWheelHistory: h.listWheelHistory,
|
||||
ListWheelHints: h.listWheelHints,
|
||||
|
||||
GetCurrentActivityTemplate: h.getCurrentActivityTemplate,
|
||||
VisitActivityTemplate: h.visitActivityTemplate,
|
||||
ListActivityTemplateTasks: h.listActivityTemplateTasks,
|
||||
ClaimActivityTemplateTask: h.claimActivityTemplateTask,
|
||||
ListActivityTemplateDailyLeaderboard: h.listActivityTemplateDailyLeaderboard,
|
||||
ListActivityTemplateTotalLeaderboard: h.listActivityTemplateTotalLeaderboard,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -78,6 +78,8 @@ type Handler struct {
|
||||
giftMaxUnits int
|
||||
loginRiskConfig LoginRiskConfig
|
||||
loginRiskCache loginRiskCache
|
||||
|
||||
activityTemplateRuntime client.ActivityTemplateRuntimeClient
|
||||
}
|
||||
|
||||
// TencentIMConfig 是 gateway transport 层签发客户端腾讯云 IM 登录票据所需的配置。
|
||||
@ -344,6 +346,12 @@ func (h *Handler) SetWheelClient(wheel client.WheelClient) {
|
||||
h.wheel = wheel
|
||||
}
|
||||
|
||||
// SetActivityTemplateRuntimeClient 注入已发布活动模版的 App 运行态入口。
|
||||
// gateway 只增加鉴权和资料展示编排,不在 HTTP 层复制版本、任务、榜单或发奖状态机。
|
||||
func (h *Handler) SetActivityTemplateRuntimeClient(runtime client.ActivityTemplateRuntimeClient) {
|
||||
h.activityTemplateRuntime = runtime
|
||||
}
|
||||
|
||||
// SetBroadcastClient 注入 activity-service 播报群成员关系 client。
|
||||
func (h *Handler) SetBroadcastClient(broadcastClient client.BroadcastClient) {
|
||||
h.broadcastClient = broadcastClient
|
||||
|
||||
@ -0,0 +1,69 @@
|
||||
package httproutes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestActivityTemplateRuntimeRoutesAreProfileProtectedAndPreservePathValues(t *testing.T) {
|
||||
profileCalls := 0
|
||||
profileWrap := func(next http.HandlerFunc) http.Handler {
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
profileCalls++
|
||||
next(writer, request)
|
||||
})
|
||||
}
|
||||
identityWrap := func(next http.HandlerFunc) http.Handler { return next }
|
||||
marker := func(name string) http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
writer.Header().Set("X-Handler", name)
|
||||
writer.Header().Set("X-Template-Code", request.PathValue("template_code"))
|
||||
writer.Header().Set("X-Task-Key", request.PathValue("task_key"))
|
||||
writer.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
routeSet := routes{mux: mux, config: Config{
|
||||
PublicWrap: identityWrap, AuthWrap: identityWrap, ProfileWrap: profileWrap,
|
||||
Task: TaskHandlers{
|
||||
GetCurrentActivityTemplate: marker("current"),
|
||||
VisitActivityTemplate: marker("visit"),
|
||||
ListActivityTemplateTasks: marker("tasks"),
|
||||
ClaimActivityTemplateTask: marker("claim"),
|
||||
ListActivityTemplateDailyLeaderboard: marker("daily"),
|
||||
ListActivityTemplateTotalLeaderboard: marker("total"),
|
||||
},
|
||||
}}
|
||||
routeSet.registerTaskRoutes()
|
||||
|
||||
tests := []struct {
|
||||
method string
|
||||
path string
|
||||
handler string
|
||||
templateCode string
|
||||
taskKey string
|
||||
}{
|
||||
{http.MethodGet, "/api/v1/activities/templates/current", "current", "", ""},
|
||||
{http.MethodPost, "/api/v1/activities/templates/summer/visit", "visit", "summer", ""},
|
||||
{http.MethodGet, "/api/v1/activities/templates/summer/tasks", "tasks", "summer", ""},
|
||||
{http.MethodPost, "/api/v1/activities/templates/summer/tasks/gift-10/claim", "claim", "summer", "gift-10"},
|
||||
{http.MethodGet, "/api/v1/activities/templates/summer/leaderboards/daily", "daily", "summer", ""},
|
||||
{http.MethodGet, "/api/v1/activities/templates/summer/leaderboards/total", "total", "summer", ""},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.handler, func(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
mux.ServeHTTP(recorder, httptest.NewRequest(test.method, test.path, nil))
|
||||
if recorder.Code != http.StatusNoContent || recorder.Header().Get("X-Handler") != test.handler {
|
||||
t.Fatalf("route mismatch: code=%d handler=%q body=%s", recorder.Code, recorder.Header().Get("X-Handler"), recorder.Body.String())
|
||||
}
|
||||
if recorder.Header().Get("X-Template-Code") != test.templateCode || recorder.Header().Get("X-Task-Key") != test.taskKey {
|
||||
t.Fatalf("path values mismatch: template=%q task=%q", recorder.Header().Get("X-Template-Code"), recorder.Header().Get("X-Task-Key"))
|
||||
}
|
||||
})
|
||||
}
|
||||
if profileCalls != len(tests) {
|
||||
t.Fatalf("all activity template routes must use profile middleware: got=%d want=%d", profileCalls, len(tests))
|
||||
}
|
||||
}
|
||||
@ -237,6 +237,13 @@ type TaskHandlers struct {
|
||||
DrawWheel http.HandlerFunc
|
||||
ListWheelHistory http.HandlerFunc
|
||||
ListWheelHints http.HandlerFunc
|
||||
|
||||
GetCurrentActivityTemplate http.HandlerFunc
|
||||
VisitActivityTemplate http.HandlerFunc
|
||||
ListActivityTemplateTasks http.HandlerFunc
|
||||
ClaimActivityTemplateTask http.HandlerFunc
|
||||
ListActivityTemplateDailyLeaderboard http.HandlerFunc
|
||||
ListActivityTemplateTotalLeaderboard http.HandlerFunc
|
||||
}
|
||||
|
||||
type LevelHandlers struct {
|
||||
@ -619,6 +626,12 @@ func (r routes) registerTaskRoutes() {
|
||||
r.profile("/activities/agency-opening-event", http.MethodGet, h.GetAgencyOpeningStatus)
|
||||
r.profile("/activities/agency-opening-event/apply", http.MethodPost, h.ApplyAgencyOpening)
|
||||
r.profile("/activities/agency-opening-event/leaderboard", http.MethodGet, h.ListAgencyOpeningLeaderboard)
|
||||
r.profile("/activities/templates/current", http.MethodGet, h.GetCurrentActivityTemplate)
|
||||
r.profile("/activities/templates/{template_code}/visit", http.MethodPost, h.VisitActivityTemplate)
|
||||
r.profile("/activities/templates/{template_code}/tasks", http.MethodGet, h.ListActivityTemplateTasks)
|
||||
r.profile("/activities/templates/{template_code}/tasks/{task_key}/claim", http.MethodPost, h.ClaimActivityTemplateTask)
|
||||
r.profile("/activities/templates/{template_code}/leaderboards/daily", http.MethodGet, h.ListActivityTemplateDailyLeaderboard)
|
||||
r.profile("/activities/templates/{template_code}/leaderboards/total", http.MethodGet, h.ListActivityTemplateTotalLeaderboard)
|
||||
r.public("/activity/wheel/{wheel_id}/config", http.MethodGet, h.GetWheelConfig)
|
||||
r.profile("/activity/wheel/{wheel_id}/draw", http.MethodPost, h.DrawWheel)
|
||||
r.profile("/activity/wheel/{wheel_id}/history", http.MethodPost, h.ListWheelHistory)
|
||||
|
||||
@ -213,7 +213,7 @@ func (h *Handler) serveRoomGiftPanel(writer http.ResponseWriter, request *http.R
|
||||
}
|
||||
|
||||
panelGifts := append([]giftConfigData{}, panelConfig.Gifts...)
|
||||
panelGifts = append(panelGifts, roomGiftBagGiftsFromResources(bagResources, panelConfig.Gifts)...)
|
||||
panelGifts = append(panelGifts, roomGiftBagGiftsFromResources(bagResources, panelConfig.Gifts, snapshot.GetVisibleRegionId())...)
|
||||
data := roomGiftPanelData{
|
||||
CoinBalance: coinBalanceFromProto(balances),
|
||||
Recipients: roomGiftRecipients(snapshot, recipientProfiles),
|
||||
|
||||
@ -962,17 +962,6 @@ func (h *Handler) roomGiftCatalogVersion(request *http.Request, app string) (int
|
||||
return resp.GetVersion(), true, nil
|
||||
}
|
||||
|
||||
func (h *Handler) roomGiftBagGifts(request *http.Request, viewerUserID int64, gifts []giftConfigData) ([]giftConfigData, error) {
|
||||
if viewerUserID <= 0 || h.walletClient == nil || len(gifts) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
resp, err := h.roomGiftBagResources(request, viewerUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return roomGiftBagGiftsFromResources(resp, gifts), nil
|
||||
}
|
||||
|
||||
func (h *Handler) roomGiftBagResources(request *http.Request, viewerUserID int64) (*walletv1.ListUserResourcesResponse, error) {
|
||||
if viewerUserID <= 0 || h.walletClient == nil {
|
||||
return &walletv1.ListUserResourcesResponse{}, nil
|
||||
@ -986,8 +975,8 @@ func (h *Handler) roomGiftBagResources(request *http.Request, viewerUserID int64
|
||||
})
|
||||
}
|
||||
|
||||
func roomGiftBagGiftsFromResources(resp *walletv1.ListUserResourcesResponse, gifts []giftConfigData) []giftConfigData {
|
||||
if resp == nil || len(gifts) == 0 {
|
||||
func roomGiftBagGiftsFromResources(resp *walletv1.ListUserResourcesResponse, gifts []giftConfigData, visibleRegionID int64) []giftConfigData {
|
||||
if resp == nil {
|
||||
return nil
|
||||
}
|
||||
giftByResourceID := make(map[int64]giftConfigData, len(gifts))
|
||||
@ -1005,21 +994,51 @@ func roomGiftBagGiftsFromResources(resp *walletv1.ListUserResourcesResponse, gif
|
||||
if remaining <= 0 {
|
||||
continue
|
||||
}
|
||||
if item.GetSourceSnapshotId() != "" {
|
||||
// Pinned activity rewards are wallet-owned promises. Current catalog disable/edit must not hide or
|
||||
// rewrite them; wallet already verified hash/app/resource binding before returning these configs.
|
||||
for _, pinned := range item.GetPinnedGiftConfigs() {
|
||||
if !giftProtoAvailableInRegion(pinned, visibleRegionID) {
|
||||
continue
|
||||
}
|
||||
gift := giftFromProto(pinned)
|
||||
gift = roomGiftBagConfig(gift, item, remaining)
|
||||
bagGifts = append(bagGifts, gift)
|
||||
}
|
||||
continue
|
||||
}
|
||||
gift, ok := giftByResourceID[item.GetResourceId()]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// Bag tab 只承载用户实际拥有的 gift 权益;仍复用 gift 配置的价格/动效,避免背包资源和房间送礼协议出现两套展示口径。
|
||||
gift.GiftTypeCode = "bag"
|
||||
gift.Source = "bag"
|
||||
gift.EntitlementID = item.GetEntitlementId()
|
||||
gift.OwnedQuantity = item.GetQuantity()
|
||||
gift.RemainingQuantity = remaining
|
||||
gift = roomGiftBagConfig(gift, item, remaining)
|
||||
bagGifts = append(bagGifts, gift)
|
||||
}
|
||||
return bagGifts
|
||||
}
|
||||
|
||||
func roomGiftBagConfig(gift giftConfigData, item *walletv1.UserResourceEntitlement, remaining int64) giftConfigData {
|
||||
gift.GiftTypeCode = "bag"
|
||||
gift.Source = "bag"
|
||||
gift.EntitlementID = item.GetEntitlementId()
|
||||
gift.OwnedQuantity = item.GetQuantity()
|
||||
gift.RemainingQuantity = remaining
|
||||
return gift
|
||||
}
|
||||
|
||||
func giftProtoAvailableInRegion(gift *walletv1.GiftConfig, regionID int64) bool {
|
||||
if gift == nil {
|
||||
return false
|
||||
}
|
||||
for _, allowed := range gift.GetRegionIds() {
|
||||
if allowed == 0 || allowed == regionID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getRoomRocket 返回房间火箭物料配置和当前进度。
|
||||
func (h *Handler) getRoomRocket(writer http.ResponseWriter, request *http.Request) {
|
||||
if h.roomQueryClient == nil {
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
roomv1 "hyapp.local/api/proto/room/v1"
|
||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||
)
|
||||
|
||||
func TestCreateRoomRejectsMissingRoomAvatar(t *testing.T) {
|
||||
@ -154,6 +155,46 @@ func TestRoomGiftTabsIncludesBagForOwnedGifts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoomGiftBagPanelUsesPinnedCatalogAfterLiveGiftEditOrDisable(t *testing.T) {
|
||||
pinned := &walletv1.GiftConfig{
|
||||
GiftId: "season-star", ResourceId: 501, Status: "active", Name: "Season Star V1", PriceVersion: "v1",
|
||||
CoinPrice: 7, GiftTypeCode: "cp", CpRelationType: "brother", RegionIds: []int64{1},
|
||||
PresentationJson: `{"theme":"season-one"}`,
|
||||
Resource: &walletv1.Resource{ResourceId: 501, ResourceType: "gift", Status: "active", Name: "Season Star V1",
|
||||
PreviewUrl: "https://static.example.test/season-star-v1.png", AnimationUrl: "https://static.example.test/season-star-v1.svga"},
|
||||
}
|
||||
resources := &walletv1.ListUserResourcesResponse{Resources: []*walletv1.UserResourceEntitlement{{
|
||||
EntitlementId: "ent-pinned-star", ResourceId: 501, Quantity: 3, RemainingQuantity: 2,
|
||||
SourceSnapshotId: "rgs-season-one", PinnedGiftConfigs: []*walletv1.GiftConfig{pinned},
|
||||
}}}
|
||||
liveV2 := giftConfigData{
|
||||
GiftID: "season-star", ResourceID: 501, Name: "Mutable Star V2", PriceVersion: "v2", CoinPrice: 31,
|
||||
Resource: resourceData{ResourceID: 501, ResourceType: "gift", PreviewURL: "https://static.example.test/season-star-v2.png"},
|
||||
}
|
||||
|
||||
panel := roomGiftBagGiftsFromResources(resources, []giftConfigData{liveV2}, 1)
|
||||
if len(panel) != 1 || panel[0].Name != "Season Star V1" || panel[0].PriceVersion != "v1" || panel[0].CoinPrice != 7 ||
|
||||
panel[0].Resource.PreviewURL != "https://static.example.test/season-star-v1.png" || panel[0].EntitlementID != "ent-pinned-star" ||
|
||||
panel[0].Source != "bag" || panel[0].RemainingQuantity != 2 {
|
||||
t.Fatalf("pinned bag panel drifted to live V2 catalog: %+v", panel)
|
||||
}
|
||||
// A disabled gift is absent from the live catalog, but the verified entitlement snapshot remains visible.
|
||||
panel = roomGiftBagGiftsFromResources(resources, nil, 1)
|
||||
if len(panel) != 1 || panel[0].Name != "Season Star V1" {
|
||||
t.Fatalf("disabled pinned gift disappeared from bag panel: %+v", panel)
|
||||
}
|
||||
if hidden := roomGiftBagGiftsFromResources(resources, nil, 2); len(hidden) != 0 {
|
||||
t.Fatalf("pinned gift leaked outside its pin-time region: %+v", hidden)
|
||||
}
|
||||
// Legacy inventory has no immutable gift bytes and therefore deliberately follows the current catalog.
|
||||
resources.Resources[0].SourceSnapshotId = ""
|
||||
resources.Resources[0].PinnedGiftConfigs = nil
|
||||
legacy := roomGiftBagGiftsFromResources(resources, []giftConfigData{liveV2}, 1)
|
||||
if len(legacy) != 1 || legacy[0].Name != "Mutable Star V2" || legacy[0].PriceVersion != "v2" {
|
||||
t.Fatalf("legacy bag entitlement did not fall back to live catalog: %+v", legacy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateRoomDataIncludesExplicitIMGroupID(t *testing.T) {
|
||||
data := createRoomDataFromProto(&roomv1.CreateRoomResponse{
|
||||
Result: &roomv1.CommandResult{Applied: true, RoomVersion: 1, ServerTimeMs: 1700000000000},
|
||||
|
||||
@ -103,6 +103,8 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
|
||||
CPWeeklyRank: h.cpWeeklyRank,
|
||||
AgencyOpening: h.agencyOpening,
|
||||
Wheel: h.wheel,
|
||||
|
||||
ActivityTemplateRuntime: h.activityTemplateRuntime,
|
||||
})
|
||||
gameAPI := gameapi.New(gameapi.Config{
|
||||
GameClient: h.gameClient,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user